I am trying to access the ip-address of the user in the query of graphql. But I cannot reach any header information. How can I access the context I am creating in my factory, inside of my graphql requests?
// app.module.ts
...
#Module({
imports: [
ConfigModule,
GraphQLModule.forRootAsync({
imports: [
LanguageModule,
SearchModule],
inject: [ConfigService],
useFactory: () => ({
autoSchemaFile: 'schema.gql',
debug: true,
fieldResolverEnhancers: ['guards'],
formatError: (error: GraphQLError): GraphQLFormattedError => {
return error.originalError instanceof BaseException
? error.originalError.serialize()
: error;
},
context: ({ req }): object => {
console.log("req.ip: ", req.ip); // Here I have the ip
return { req };
},
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// search.resolver.ts
...
#Resolver(() => Search)
export class SearchResolver {
constructor(private readonly service: service) {}
#Query(() => Search)
async search(#Args() args: SearchArgs): Promise<Search> {
// I want the ip here, I want to send it as an argument into the query function below
const response = await this.service.query(args.query, {
language: args.language,
});
return response;
}
}
According to this thread resolver context parameter should contain req but it depends [on configuration].
Resolvers typically takes (parent, args, context, info) arguments - check if context is defined in yours.
Related
I would like to authorize users in GraphQL in a NestJs project. I use nest-keycloak-connect for this.
Unfortunately, when calling query with "Authorization" set in Headers, I get the error: [Keycloak] Empty JWT, unauthorized.
So it looks like nest-keycloak-connect doesn't recognize that context comes from graphql.
However, when looking at the source code of nest-keycloak-connect, context is checked for http and graphql there.
So what should I do to make nest-keycloak-connect start using graphql correctly?
nest-keycloak-connect context type checking
export const extractRequest = (context: ExecutionContext): [any, any] => {
let request: any, response: any;
// Check if request is coming from graphql or http
if (context.getType() === 'http') {
// http request
const httpContext = context.switchToHttp();
request = httpContext.getRequest();
response = httpContext.getResponse();
} else if (context.getType<GqlContextType>() === 'graphql') {
let gql: any;
// Check if graphql is installed
try {
gql = require('#nestjs/graphql');
} catch (er) {
throw new Error('#nestjs/graphql is not installed, cannot proceed');
}
// graphql request
const gqlContext = gql.GqlExecutionContext.create(context).getContext();
request = gqlContext.req;
response = gqlContext.res;
}
return [request, response];
};
my auth settings
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/Products'),
KeycloakConnectModule.register({
authServerUrl: 'http://localhost:8080/auth',
realm: 'users',
clientId: 'users-service',
secret: 'h1xAJnShNwPmxzySR8Y0d3fLh27iwPPh',
policyEnforcement: PolicyEnforcementMode.PERMISSIVE, // optional
tokenValidation: TokenValidation.ONLINE, // optional
}),
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
ProductModule,
ProductImageModule,
ProductAttributeModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: ResourceGuard,
},
{
provide: APP_GUARD,
useClass: RoleGuard,
},
],
controllers: [],
})
export class AppModule {}
Resolver
//FindAll
#Query(() => [Product])
#Roles({roles: ['user']})
async products() {
const products = await this.productService.findAll();
return products;
}
Header
{
"Authorization":"eyJhbGciOiJSUzI1N..."
}
Solved by adding Bearer to the header
{
"authorization":"Bearer eyJhbGciOiJSUzI1NiIsInR..."
}
I'm playing around with Microservice architecture using NestJs. I've made a simplified repository with a few services that communicate over TCP with a mix of message and event patterns.
I have moved on to writing E2E tests for the using Supertest, and while I'm able to run the needed microservice, the requests respond with {"error": "There is no matching message handler defined in the remote service.", "statusCode": 500}
GatewayService: HTTP Rest Api where the E2E tests are run. Calls the service
AuthService: NestJs microservice running on 0.0.0.0:3001 by default
configService: a simple service that returns information needed to set up the services, like host and port. I have tried eliminating it from the test and hardcoding the values.
The E2E test file
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { ClientProxy, ClientsModule, Transport } from '#nestjs/microservices';
import { Test, TestingModule } from '#nestjs/testing';
import * as request from 'supertest';
import { configService } from '../src/config.service';
import { RpcExceptionFilter } from '../src/filters/rpc-exception.filter';
import { AppModule } from './../src/app.module';
describe('AuthenticationController (e2e)', () => {
let app: INestApplication;
let authClient: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
ClientsModule.register([
{
...configService.getServiceConfigs().authService,
transport: Transport.TCP,
},
]),
],
}).compile();
// Setup the app instance
app = moduleFixture.createNestApplication();
// Setup the relevant micorservice(s)
app.connectMicroservice({
transport: Transport.TCP,
name: configService.getServiceConfigs().authService.name,
options: configService.getServiceConfigs().authService.options,
});
app.startAllMicroservices();
// Add request validation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
// Add needed filters
app.useGlobalFilters(new RpcExceptionFilter());
await app.init();
authClient = app.get(configService.getServiceConfigs().authService.name);
await authClient.connect();
console.log('authClient', authClient);
});
describe('POST /auth/login', () => {
it('Should return status 200 and a user object with access token', () => {
return (
request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'exmple#user.com', password: 'password' })
// .expect(200)
.expect((response) => {
console.log('response', response.body);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('accessToken');
})
);
});
});
afterAll(async () => {
await app.close();
await authClient.close();
});
});
I have attempted adding a provider which I've used before when working with Grpc as the transport layer (this is TCP). Didn't change anything.
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
...
providers: [
{
provide: 'AUTH_SERVICE',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: 'localhost', port: 3001 },
});
},
},
],
I know that the microservice starts up and the gateway service is able to connect to it since when printing the authClient: Client proxy it returns a correct object with URL 0.0.0.0:3001. If I change the URL, or the name of the service in any part of the setup then errors about missing providers show, further confirming that it is supposedly correctly set up.
One of the best guides I've found on this matter. Sadly it doesn't work for my code.
In the test/posts/posts.e2e-spec.ts file
import { INestApplication } from '#nestjs/common';
import { TypeOrmModule } from '#nestjs/typeorm';
import { Test, TestingModule } from '#nestjs/testing';
import request = require('supertest');
import { PostsModule } from '../../src/posts/posts.module';
describe('Posts (e2e)', () => {
const posts = {
id: 1,
name: 'FirstPost #1',
};
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
...
}),
PostModule,
],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterAll(async () => {
await app.close();
});
describe('post', () => {
it('should retrieve all post data', async () => {
request(app.getHttpServer())
.post('/graphql')
.send({
query:
`{findPosts() {
name
}}`,
})
.expect(200)
.expect((res) => {
console.log(res.body.data)
expect(res.body.data.post.length).toEqual(posts.length)
})
})
})
});
I created migration and inserted data into database first, then run this test, it can't go to the expect items. Even set console log I can't see anything in the output.
So maybe the /graphql can't be access in this way? I can access the endpoint from browser as http://localhost:3000/graphql.
If import supertest as
import * as request from 'supertest';
In the line request it showed:
This expression is not callable. Type ‘typeof supertest’ has no call signatures.
The version of them:
supertest: 6.1.3
#types/supertest: 2.0.11
Check out this very useful link https://github.com/jmcdo29/testing-nestjs/tree/main/apps/graphql-sample. It explains a lot of things regarding tests including graphql nestjs testing along with sample application
I have the following NestJS controller:
class PhoneTestDto {
#IsNotEmpty()
#IsPhoneNumber()
phone: string
}
class TestDto {
#IsNotEmpty()
#Type(() => PhoneTestDto)
#ValidateNested({ always: true, each: true })
#ArrayNotEmpty()
phones: PhoneTestDto[]
}
#Controller('v1/user')
export class UserController {
constructor(private readonly userService: UserService) {}
#Post()
async addUser(#Body() body: TestDto): Promise<LoginResponseDto> {
console.log("valid")
return
}
}
When I run the app and send through Postman a request such as:
{
"phones": [{}]
}
I get a correct response (phones.0.phone must be a valid phone number...)
When I try to run an e2e test, it passes without validating the phone.
This is my test file:
describe('Test', () => {
let app: INestApplication
beforeEach(async () => {
jest.resetModules()
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleRef.createNestApplication()
app.useGlobalPipes(new ValidationPipe({transform: true}))
await app.init()
})
it('test1', async () => {
const response = await request(app.getHttpServer())
.post('/v1/user')
.send({phones: [{}]})
expect(response['res']['statusCode']).toEqual(201)
})
})
Sending {} or {[]} does produce a validation error so validation is generally working, but not the validation of nested fields in the context of testing.
ALSO: Removing jest.resetModules() brings correct behavior.
I'm not sure how jest.resetModules() relate to nestjs validation, and how should I use if I do need to reset modules.
I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case