NesJS : using an interceptor for HTTP and WS - node.js

I created an interceptor to edit data after passing the controller.
It works with HTTP but not with WS.
This is the code of my interceptor :
#Injectable()
export class SignFileInterceptor implements NestInterceptor {
constructor(private fileService: FilesService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map(async (data) => {
const paths = getFilesDtoPaths(data);
for (const path of paths) {
const file = get(data, path);
// Returns a promise
const signed = await this.fileService.signFile(file);
set(data, path, signed);
}
return data; // The data is edited and we can return it.
}),
);
}
}
To use it for HTTP, I add the interceptor to the app module :
providers: [
AppService,
{
provide: APP_INTERCEPTOR,
useClass: SignFileInterceptor,
}
]
With this, all my HTTP requests are intercepted, and the response is correct.
I want to make the same thing with WS using the same interceptor.
#WebSocketGateway({
cors,
allowEIO3: true,
})
#UseInterceptors(SignFileInterceptor) // Interceptor added HERE
#Injectable()
export class EventsGateway {
constructor() {}
#WebSocketServer()
server!: Server;
#SubscribeMessage('name1')
async handleJoinRoom(
): Promise<string> {
return 'john doe'
}
#SubscribeMessage('name2')
async handleJoinRoom(
): Promise<string> {
return 'john doe 2'
}
}
When a WS is triggered, the code is executed, but the data is returned BEFORE the end of my interceptor execution.
The data is not edited.
I appreciate your help.

Change map to mergeMap or switchMap to handle the async execution of the code. map from RxJS is a synchronous method.

This interceptor works well for HTTP and WS.
Another issue in my project caused the problem.
Sorry for the inconvenience.

Related

How to excecute guard before injected provider into Scope.Request

I am working on a multi-tenant app using NestJS and I store the tenantId in the token using Jwt, I need to create a database tenant connection before I do database operations but the provider(code below) is being executed before the JwtAuthGuard but I need the guard to be executed first, Is there a way to change the order of execution?
Controller method (uses JwtAuthGuard):
#Post()
#UsePipes(new ValidationPipe())
#UseGuards(JwtAuthGuard)
create(#Body() createUserDto: CreateFruitDto) {
return this.fruitsService.create(createUserDto);
}
Passport strategy (JwtAuthGuard):
export class JwtStrategy extends PassportStrategy(Strategy) {
private logger = new Logger('JwtStrategy');
constructor(private configService: JwtConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: configService.ignoreExpiration,
secretOrKey: configService.options.secret,
});
}
async validate(payload: any) {
//injects user into req
return {
userId: payload.sub,
email: payload.email,
tenantId: payload.tenant,
};
}
}
Provider being injected into FruitsModule:
provide: 'TENANT_CONTEXT',
scope: Scope.REQUEST,
inject: [REQUEST],
useFactory: (req: Request): ITenantContext => {
const { user } = req as any;
Logger.log(user); // is undefined
const tenantContext: ITenantContext = {
user.tenantId,
};
return tenantContext;
},
IMHO best to avoid request scoped providers. That should have never been introduced in Nest. That scope bubbles up and makes everything above it request scoped as well.
You could introduce middleware to work around this. Middlewares are executed before guards. The auth guard validates and extracts data from the JWT token and stores it on req.user. Configure a middleware to prepare a user property on the request. Its setter will be executed when the auth guard sets the user property on the request and it will extract the tenant ID for you.
interface ExecutionMetadata {
tenantId?: number;
}
export class TenantContextMiddleware implements NestMiddleware {
public async use(req: Request, res: Response, next: NextFunction) {
this.metadata: ExecutionMetadata = { tenantId: req.user?.tenantId };
Object.definePropery(req, 'user', {
set(user) {
this._user = user;
this.metadata.tenantId = user?.tenantId;
},
get() {
return this._user;
}
});
next();
}
}
Here I extract the tenant ID from the req.user and store it on the req.metadata property.
Using the createParamdecorator() function from NestJS you could then write a simple parameter decorator to inject this metadata.
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const Metadata = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.metadata;
},
);
You can then use this decorator to inject this metadata into your controller.
#Controller('cats')
export class CatsController {
#Get()
findAll(#Metadata() metadata: ExecutionMedata): string {
...
}
}
Remark: This decorator will only work for controller methods! NestJS is able to resolve the value for you at that stage of the request. Similar to the #Body(), #Param(), #Query()...decorators. Then you can pass this metadata down as an argument. Or you could do something fancy and setup asynchronous context tracking.

Binding interceptor to NestJS microservice method

I just created a simple interceptor to override every error thrown by my application, just like the one on Nest's documentation:
#Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next
.handle()
.pipe(
catchError(err => throwError(() => new ApplicationException())),
);
}
}
And altough exceptions caused by http requests indeed are caught in that interceptor, I just can't make it work with RPC requests (like KafjaJS and events).
Just like the documentation, I've binded it on my app.module:
{
provide: APP_INTERCEPTOR,
useClass: ErrorsInterceptor,
}
I know I'm probably missing something out, can someone clarify where and why what I'm doing is not working and how to make it work?
#Edit: I forgot to mention that I made it work #UseInterceptors() above my controller's method, but I'd like to make it work without it.
#Edit 2: I have a hybrid appplication, this is what my main looks like (as asked by Jay):
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(winstonTransports),
});
app.connectMicroservice<MicroserviceOptions>(kafkaConfig);
const logger = app.get<winston.Logger>(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(logger);
app.enableCors(corsConfig);
await app.startAllMicroservices();
await app.listen(env.PORT);
}
When working with hybrid applications you need to add { inheritAppConfig: true } to the connectMicroservice() method as a second parameter as described in the docs. This means your main.ts should be
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
logger: WinstonModule.createLogger(winstonTransports),
});
app.connectMicroservice<MicroserviceOptions>(kafkaConfig, { inheritAppConfig: true });
const logger = app.get<winston.Logger>(WINSTON_MODULE_NEST_PROVIDER);
app.useLogger(logger);
app.enableCors(corsConfig);
await app.startAllMicroservices();
await app.listen(env.PORT);
}

How to create common class for third-party API requests in NestJS

I am creating NestJS application where I am making third-party API requests. For that I have to write the same thing inside every function in order to get the data.
To make things non-repeating, how can I write on common class that has API request based on GET or POST request and send the response so that I can use that class in every function.
Below is my code:
subscribe.service.ts
#Injectable()
export class SubscribeService {
constructor(#InjectModel('Subscribe') private readonly model:Model<Subscribe>,
#Inject(CACHE_MANAGER) private cacheManager:Cache,
private httpService: HttpService){}
async addSubscriber(subscriberDto:SubscribeDto){
const url = 'https://track.cxipl.com/api/v2/phone-tracking/subscribe';
const headersRequest = {
'content-Type': 'application/json',
'authkey': process.env.AUTHKEY
};
try{
const resp = await this.httpService.post(url,subscriberDto,{ headers: headersRequest }).pipe(
map((response) => {
if(response.data.success == true){
const data = new this.model(subscriberDto);
// return data.save();
const saved = data.save();
if(saved){
const msgSuccess = {
"success":response.data.success,
"status":response.data.data.status
}
return msgSuccess;
}
}
else{
const msgFail = {"success":response.data.success}
return msgFail;
}
}),
);
return resp;
}
catch(err){
return err;
}
}
async getLocation(phoneNumber:PhoneNumber){
try{
const location = await this.cacheManager.get<Coordinates>(phoneNumber.phoneNumber);
if(location){
return location;
}
else{
const resp = await axios.post('https://track.cxipl.com/api/v2/phone-tracking/location',phoneNumber,{headers:{
'content-Type': 'application/json',
'authkey': process.env.AUTHKEY
}});
const msg:Coordinates = {
"location":resp.data.data.location,
"timestamp":resp.data.data.timestamp
}
await this.cacheManager.set<Coordinates>(phoneNumber.phoneNumber,msg, { ttl: 3600 });
return msg;
}
}
catch(err){
console.log(err);
return err;
}
}
}
As in above code in both function addSubscriber() and getLocation() I need to hit the API repeatedly and add request headers again and again is there any way so that I can create one separate class for request and response and utilize in my service.
How can I achieve desired the result?
To create a common class for making third-party API requests in NestJS, you can follow these steps:
Create a new file in your NestJS project to store the common class.
For example, you could create a file called api.service.ts in the
src/common directory.
In the file, create a new class called ApiService that will be responsible for making the API requests. This class should have a
constructor that injects the necessary dependencies, such as the
HttpService provided by NestJS.
import { HttpService, Injectable } from '#nestjs/common';
#Injectable()
export class ApiService {
constructor(private readonly httpService: HttpService) {}
}
Add methods to the ApiService class for each type of API request you want to make. For example, you might have a get() method for making GET requests, a post() method for making POST requests, and so on. Each method should accept the necessary parameters for making the request (such as the URL and any query parameters or request body), and use the HttpService to make the request.
import { HttpService, Injectable } from '#nestjs/common';
#Injectable()
export class ApiService {
constructor(private readonly httpService: HttpService) {}
async get(url: string, params?: object): Promise<any> {
return this.httpService.get(url, { params }).toPromise();
}
async post(url: string, body: object): Promise<any> {
return this.httpService.post(url, body).toPromise();
}
}
Inject the ApiService wherever you need to make API requests. For example, you might inject it into a service or a controller, and use the methods of the ApiService to make the actual API requests.
import { Injectable } from '#nestjs/common';
import { ApiService } from './api.service';
#Injectable()
export class SomeService {
constructor(private readonly apiService: ApiService) {}
async getData(): Promise<any> {
return this.apiService.get('https://some-api.com/endpoint');
}
}
This is just one way you could create a common class for making third-party API requests in NestJS. You can customize the ApiService class to meet the specific needs of your application

Get current user in nestjs on a route without an AuthGuard

I use nestjs with passport with jwt strategy. And I want to get a current user on some of my requests.
Currently, I have a decorator that looks like this:
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const CurrentUser = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const user = ctx.switchToHttp().getRequest().user;
if (!user) {
return null;
}
return data ? user[data] : user; // extract a specific property only if specified or get a user object
},
);
It works as intended when i use it on a route with an AuthGuard:
#Get('test')
#UseGuards(AuthGuard())
testRoute(#CurrentUser() user: User) {
console.log('Current User: ', user);
return { user };
}
But how do i make it work (get current user) on non-guarded routes? I need users to be able to post their comments regardless of if they are authorized or not, however, when they are logged in, i need to get their name.
Basically, I need a way to propagate req.user on every(or at least on some of not AuthGuard'ed request), it is really straight forward to do in express by applying passport middleware, but I'm not sure how to do it with #nestjs/passport.
[EDIT]
Thanks to vpdiongzon for pointing me in the right direction, I chose to make a guard based on his answer, that just populates req.user with either user or null:
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class ApplyUser extends AuthGuard('jwt') {
handleRequest(err: any, user: any) {
if (user) return user;
return null;
}
}
And now I could just use it on any unprotected route that needs to get the current user
#Get('me')
#UseGuards(ApplyUser)
me(#CurrentUser() user: User) {
return { user };
}
You need to apply your AuthGuard to every route regardless but if you have a route that don't require authentication just add a custom decorator, example:
the Auth Guard
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private readonly reflector: Reflector) {
super();
}
handleRequest(err, user, info, context) {
const request = context.switchToHttp().getRequest();
const allowAny = this.reflector.get<string[]>('allow-any', context.getHandler());
if (user) return user;
if (allowAny) return true;
throw new UnauthorizedException();
}
}
Apply globally the AuthGuard in app.module.js
import { APP_GUARD, Reflector } from '#nestjs/core';
import { AppController } from './app.controller';
import { JwtAuthGuard } from './app.guard';
#Module({
imports: ],
controllers: [AppController],
providers: [
{
provide: APP_GUARD,
useFactory: ref => new JwtAuthGuard(ref),
inject: [Reflector],
},
AppService,
],
})
export class AppModule {
}
The custom decorator to allow a route without authentication
import { SetMetadata } from '#nestjs/common';
export const AllowAny = () => SetMetadata('allow-any', true);
Apply AllowAny in a route, if AllowAny decorator is not attached in a controller route it will required a user.
#Post('testPost')
#AllowAny()
async testPost(#Req() request) {
console.log(request.user)
}
"Basically, I need a way to propagate req.user on every(or at least on some of not AuthGuard'ed request), it is realy straight forward to do in express by applying passport middleware, but im not sure how to do it with #nestjs/passport."
To achieve this we write an interceptor because we need to use the UsersService. UserService is part of the dependency injection system. We cannot just import the user service and create a new instance of it ourselves. The service makes use of the users repository and that users repository is setup only through dependency injection.
The thing is we cannot make use of dependency injection with a parameter decorator. This decorator cannot reach into the system in any way and try to get access to some instance of anything inside there. This is how we write the interceptor. I make comments on the code:
// this interceptor will be used by the custom param decoratro to fetch the current User
import {NestInterceptor,ExecutionContext,CallHandler,Injectable} from '#nestjs/common';
import { UsersService } from '../users.service';
#Injectable()
// "implements" guide us how to put together an interceptor
export class CurrentUserInterceptor implements NestInterceptor {
constructor(private userService: UsersService) {}
// handler refers to the route handler
async intercept(context: ExecutionContext, handler: CallHandler) {
const request = context.switchToHttp().getRequest();
const { userId } = request.session || {};
if (userId) {
const user = await this.userService.findOne(userId);
// we need to pass this down to the decorator. SO we assign the user to request because req can be retrieved inside the decorator
// ------THIS IS WHAT YOU WANTED--------
request.currentUser = user;
}
// run the actual route handler
return handler.handle();
}
}
Now you need to register this to the module:
#Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService, AuthService, CurrentUserInterceptor],
})
Inside controller:
#Controller('auth')
#UseInterceptors(CurrentUserInterceptor)
export class UsersController {
constructor("inject services) {}
#Get('/me')
me(#CurrentUser() user: User) {
return user;
}
}
In any route handler you use CurrentUser param decorator, you will have access to "user".
You actually do not need to write a custom param decorator
you could just use the interceptor, its implementation would be different:
#Get('/me')
me(#CurrentUserInterceptor() request: Request) {
// You have access to request.currentUser
return request.currentUser
}
Set interceptor globally
The current setup for the interceptor is tedious. We are applying the interceptor to one controller at a time. (Thats called controlled scope) Instead you could globally make this interceptor available:
users Module:
import { APP_INTERCEPTOR } from '#nestjs/core';
#Module({
// this createes repository
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [
UsersService,
AuthService,
{
provide: APP_INTERCEPTOR,
useClass: CurrentUserInterceptor,
},
],
})
This approach has one downside. Not every controller cares about what the current user is. In those controllers, you still have to make request to the database to fetch the current User.
the parsed userinfo stored in request.user
import {Req} from '#nestjs/common'
import { Request } from 'express'
#Post()
create(#Req() request: Request) {
console.log('user', request.user)
}

Header are not passed through after updating nestjs/graphql

I'm about to update my project dependencies to the next major versions but i can't get nestjs/graphql + nestjs/passport to work.
It looks like the request header is not passed through apollo server. Everytime when passport tries to extract the bearer token from the header i get an exception with the following stacktrace:
TypeError: Cannot read property 'headers' of undefined,
at JwtStrategy._jwtFromRequest (/Users/wowa/workspace/foxcms-backend/node_modules/passport-jwt/lib/extract_jwt.js:58:21),
at JwtStrategy.authenticate (/Users/wowa/workspace/foxcms-backend/node_modules/passport-jwt/lib/strategy.js:93:22),
at attempt (/Users/wowa/workspace/foxcms-backend/node_modules/passport/lib/middleware/authenticate.js:361:16)",
at authenticate (/Users/wowa/workspace/foxcms-backend/node_modules/passport/lib/middleware/authenticate.js:362:7)",
at Promise (/Users/wowa/workspace/foxcms-backend/node_modules/#nestjs/passport/dist/auth.guard.js:77:3)",
at new Promise ()",
at /Users/wowa/workspace/foxcms-backend/node_modules/#nestjs/passport/dist/auth.guard.js:69:83",
at MixinAuthGuard. (/Users/wowa/workspace/foxcms-backend/node_modules/#nestjs/passport/dist/auth.guard.js:44:36)",
at Generator.next ()",
at /Users/wowa/workspace/foxcms-backend/node_modules/#nestjs/passport/dist/auth.guard.js:19:71"
This is how my app.module looks like:
#Module({
imports: [
GraphQLModule.forRoot({
typePaths: ['./src/**/*.graphql'],
}),
UserModule,
ContentTypeModule,
PrismaModule,
ProjectModule,
AuthModule,
],
})
export class AppModule implements NestModule {
constructor(
private readonly graphQLFactory: GraphQLFactory,
#Inject('PrismaBinding') private prismaBinding,
) {}
configure(consumer: MiddlewareConsumer) {}
}
I just wanted to ask here before i open an issue on github. Anyone a idea whats wrong?
You can manage the object request with this form:
GraphQLModule.forRoot({
typePaths: ['./**/*.graphql'],
installSubscriptionHandlers: true,
context: (({ req }) => {
return { request: req }
}),
},
And create your own Guard:
export class CatsGuard implements CanActivate {
constructor(readonly jwtService: JwtService/*, readonly userService: UsersService*/) {}
canActivate(context: ExecutionContext): boolean {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().request;
const Authorization = request.get('Authorization');
if (Authorization) {
const token = Authorization.replace('Bearer ', '');
const { userId } = this.jwtService.verify(token) as { userId: string };
return !!userId;
}
}
}
The provided AuthGuard from the passport module is currently not working with the graphql module.
https://github.com/nestjs/graphql/issues/48

Resources