diff --git a/src/app.module.ts b/src/app.module.ts index 2b1100a..4f3433b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -49,6 +49,7 @@ import { TaskModule } from './task/task.module'; inject: [ConfigService], }), I18nModule.forRoot(buildI18nOptions()), + CacheModule, // App modules diff --git a/src/auth/controllers/auth.controller.ts b/src/auth/controllers/auth.controller.ts index a36d5de..bd0b6cd 100644 --- a/src/auth/controllers/auth.controller.ts +++ b/src/auth/controllers/auth.controller.ts @@ -1,5 +1,6 @@ -import { Body, Controller, Headers, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs/common'; +import { Body, Controller, Headers, HttpCode, HttpStatus, Post, Req, UseGuards } from '@nestjs/common'; import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Request } from 'express'; import { DEVICE_ID_HEADER } from '~/common/constants'; import { AuthenticatedUser, Public } from '~/common/decorators'; import { AccessTokenGuard } from '~/common/guards'; @@ -98,4 +99,11 @@ export class AuthController { const [res, user] = await this.authService.login(loginDto, deviceId); return ResponseFactory.data(new LoginResponseDto(res, user)); } + + @Post('logout') + @HttpCode(HttpStatus.NO_CONTENT) + @UseGuards(AccessTokenGuard) + async logout(@Req() request: Request) { + await this.authService.logout(request); + } } diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index c9f6579..9b9f162 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -2,6 +2,8 @@ import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/ import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import { Request } from 'express'; +import { CacheService } from '~/common/modules/cache/services'; import { OtpScope, OtpType } from '~/common/modules/otp/enums'; import { OtpService } from '~/common/modules/otp/services'; import { JuniorTokenService } from '~/junior/services'; @@ -35,6 +37,7 @@ export class AuthService { private readonly userService: UserService, private readonly deviceService: DeviceService, private readonly juniorTokenService: JuniorTokenService, + private readonly cacheService: CacheService, ) {} async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode }); @@ -213,6 +216,12 @@ export class AuthService { } } + logout(req: Request) { + const accessToken = req.headers.authorization?.split(' ')[1] as string; + const expiryInTtl = this.jwtService.decode(accessToken).exp - Date.now() / ONE_THOUSAND; + return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl); + } + private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise { const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password); diff --git a/src/common/guards/access-token.guard.ts b/src/common/guards/access-token.guard.ts index f8ba82b..74e76a2 100644 --- a/src/common/guards/access-token.guard.ts +++ b/src/common/guards/access-token.guard.ts @@ -1,15 +1,15 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; -import { Observable } from 'rxjs'; import { IS_PUBLIC_KEY } from '../decorators'; +import { CacheService } from '../modules/cache/services'; @Injectable() export class AccessTokenGuard extends AuthGuard('access-token') { - constructor(protected reflector: Reflector) { + constructor(protected reflector: Reflector, private readonly cacheService: CacheService) { super(); } - canActivate(context: ExecutionContext): boolean | Promise | Observable { + async canActivate(context: ExecutionContext) { const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ context.getHandler(), context.getClass(), @@ -18,6 +18,21 @@ export class AccessTokenGuard extends AuthGuard('access-token') { if (isPublic) { return true; } - return super.canActivate(context); + + const isValid = await super.canActivate(context); + + if (!isValid) { + return false; + } + + const token = context.switchToHttp().getRequest().headers['authorization']?.split(' ')[1]; + + const isRevoked = await this.cacheService.get(token); + + if (isRevoked) { + return false; + } + + return !isRevoked; } } diff --git a/src/common/modules/cache/cache.module.ts b/src/common/modules/cache/cache.module.ts index 6717d91..e55abcf 100644 --- a/src/common/modules/cache/cache.module.ts +++ b/src/common/modules/cache/cache.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { buildKeyvOptions } from '~/core/module-options'; import { CacheService } from './services'; @@ -14,4 +14,5 @@ import { CacheService } from './services'; ], exports: ['CACHE_INSTANCE', CacheService], }) +@Global() export class CacheModule {} diff --git a/src/document/document.module.ts b/src/document/document.module.ts index 4d93ecb..c081e31 100644 --- a/src/document/document.module.ts +++ b/src/document/document.module.ts @@ -1,6 +1,5 @@ import { Global, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { CacheModule } from '~/common/modules/cache/cache.module'; import { DocumentController } from './controllers'; import { Document } from './entities'; import { DocumentRepository } from './repositories'; @@ -8,7 +7,7 @@ import { DocumentService, OciService } from './services'; @Global() @Module({ - imports: [TypeOrmModule.forFeature([Document]), CacheModule], + imports: [TypeOrmModule.forFeature([Document])], controllers: [DocumentController], providers: [DocumentService, OciService, DocumentRepository], exports: [DocumentService, OciService],