From c77d81d95a08fa8205c3d8e46dfcd64d78845e4e Mon Sep 17 00:00:00 2001 From: Bogdan Date: Wed, 27 Mar 2024 13:34:51 +0200 Subject: [PATCH 01/20] chore: change node engines version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2a72cf4..6c526cb 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "start:migrate:dev": "npm run prisma:migrate:reset && npm run prisma:migrate && npm run start:dev" }, "engines": { - "node": "20.0.0" + "node": ">=20.0.0" }, "dependencies": { "@nestjs/common": "^10.3.3", From 506857f27f67858cc58191ed6d2d52f45d319a43 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Fri, 29 Mar 2024 17:25:33 +0200 Subject: [PATCH 02/20] feat: implement generation access/refresh tokens --- src/app.controller.ts | 2 + src/app.module.ts | 7 ++- src/auth/auth.controller.ts | 45 ++++++++++++++ src/auth/auth.guard.ts | 49 ++++++++++++++++ src/auth/auth.module.ts | 13 +++++ src/auth/auth.service.ts | 87 ++++++++++++++++++++++++++++ src/auth/dto/create-auth.dto.ts | 3 + src/auth/dto/update-auth.dto.ts | 5 ++ src/auth/entities/auth.entity.ts | 1 + src/lib/const/const.ts | 8 +++ src/lib/shared/skipAuth.decorator.ts | 4 ++ 11 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 src/auth/auth.controller.ts create mode 100644 src/auth/auth.guard.ts create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.ts create mode 100644 src/auth/dto/create-auth.dto.ts create mode 100644 src/auth/dto/update-auth.dto.ts create mode 100644 src/auth/entities/auth.entity.ts create mode 100644 src/lib/shared/skipAuth.decorator.ts diff --git a/src/app.controller.ts b/src/app.controller.ts index 2ea27e9..06b88e8 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,11 +1,13 @@ import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; +import { SkipAuth } from './lib/shared/skipAuth.decorator'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} + @SkipAuth() @Get() getHello(): string { return this.appService.getHello(); diff --git a/src/app.module.ts b/src/app.module.ts index 1a80393..02730ea 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,10 +1,13 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; import { AlbumModule } from './album/album.module'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { ArtistModule } from './artist/artist.module'; +import { AuthGuard } from './auth/auth.guard'; +import { AuthModule } from './auth/auth.module'; import { FavoriteModule } from './favorite/favorite.module'; import { PrismaModule } from './prisma/prisma.module'; import { TrackModule } from './track/track.module'; @@ -19,8 +22,10 @@ import { UserModule } from './user/user.module'; FavoriteModule, ConfigModule.forRoot(), PrismaModule, + AuthModule, + JwtModule, ], controllers: [AppController], - providers: [AppService], + providers: [AppService, { provide: 'APP_GUARD', useClass: AuthGuard }], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts new file mode 100644 index 0000000..5eb50e4 --- /dev/null +++ b/src/auth/auth.controller.ts @@ -0,0 +1,45 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, +} from '@nestjs/common'; + +import { AuthService } from './auth.service'; +import { CreateAuthDto } from './dto/create-auth.dto'; +import { UpdateAuthDto } from './dto/update-auth.dto'; +import { SkipAuth } from '../lib/shared/skipAuth.decorator'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @SkipAuth() + @Post('signup') + async create(@Body() createAuthDto: CreateAuthDto) { + return this.authService.signUp(createAuthDto); + } + + @Get() + findAll() { + return this.authService.findAll(); + } + + @Get(':id') + findOne(@Param('id') id: string) { + return this.authService.findOne(+id); + } + + @Patch(':id') + update(@Param('id') id: string, @Body() updateAuthDto: UpdateAuthDto) { + return this.authService.update(+id, updateAuthDto); + } + + @Delete(':id') + remove(@Param('id') id: string) { + return this.authService.remove(+id); + } +} diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts new file mode 100644 index 0000000..ceea630 --- /dev/null +++ b/src/auth/auth.guard.ts @@ -0,0 +1,49 @@ +import { + CanActivate, + ExecutionContext, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { Observable } from 'rxjs'; + +import { jwtConstants } from '../lib/const/const'; + +@Injectable() +export class AuthGuard implements CanActivate { + constructor(private readonly jwtService: JwtService) {} + + canActivate( + context: ExecutionContext, + ): boolean | Promise | Observable { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + throw new UnauthorizedException(); + } + + try { + const payload = this.jwtService.verifyAsync(token, { + secret: jwtConstants.accessSecret, + }); + + console.log(payload); + + // TODO: remove test + request.test = payload; + } catch (e) { + throw new UnauthorizedException(); + } + + return true; + } + + private extractTokenFromHeader(request: Request): string | undefined { + if (!('authorization' in request.headers)) return undefined; + + const authorization = request.headers.authorization as string | undefined; + const [type, token] = authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..bec767a --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; + +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { PrismaModule } from '../prisma/prisma.module'; + +@Module({ + imports: [PrismaModule, JwtModule], + controllers: [AuthController], + providers: [AuthService], +}) +export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..5166fdb --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,87 @@ +import { Injectable } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import * as bcrypt from 'bcrypt'; + +import { CreateAuthDto } from './dto/create-auth.dto'; +import { UpdateAuthDto } from './dto/update-auth.dto'; +import { CRYPT_SALT, jwtConstants } from '../lib/const/const'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class AuthService { + constructor( + private readonly prismaService: PrismaService, + private readonly jwtService: JwtService, + ) {} + + async signUp({ login, password }: CreateAuthDto) { + const user = await this.prismaService.user.findFirst({ + where: { AND: [{ login }, { password }] }, + }); + + if (user) { + return 'The user is already exists'; + } + + // hash the password + const hash = await bcrypt.hash(password, CRYPT_SALT); + + // save user in the database + const createdUser = await this.prismaService.user.create({ + data: { login, password: hash }, + }); + + const payload = { userId: createdUser.id, login: createdUser.login }; + + // Create access token + const waitAccessToken = this.jwtService.sign(payload, { + secret: jwtConstants.accessSecret, + expiresIn: jwtConstants.accessExpiresIn, + }); + + // Create refresh token + const waitRefreshToken = this.jwtService.sign(payload, { + secret: jwtConstants.refreshSecret, + expiresIn: jwtConstants.refreshExpiresIn, + }); + + const [accessToken, refreshToken] = await Promise.all([ + waitAccessToken, + waitRefreshToken, + ]); + + return { accessToken, refreshToken }; + } + + async signIn({ login, password }: CreateAuthDto) { + const user = await this.prismaService.user.findFirst({ + where: { AND: [{ login }, { password }] }, + }); + + if (!user) { + return 'no user = no token'; + } + + return 'token'; + } + + create(createAuthDto: CreateAuthDto) { + return 'This action adds a new auth'; + } + + findAll() { + return `This action returns all auth`; + } + + findOne(id: number) { + return `This action returns a #${id} auth`; + } + + update(id: number, updateAuthDto: UpdateAuthDto) { + return `This action updates a #${id} auth`; + } + + remove(id: number) { + return `This action removes a #${id} auth`; + } +} diff --git a/src/auth/dto/create-auth.dto.ts b/src/auth/dto/create-auth.dto.ts new file mode 100644 index 0000000..20ac82b --- /dev/null +++ b/src/auth/dto/create-auth.dto.ts @@ -0,0 +1,3 @@ +import { CreateUserDto } from '../../user/dto/create-user.dto'; + +export class CreateAuthDto extends CreateUserDto {} diff --git a/src/auth/dto/update-auth.dto.ts b/src/auth/dto/update-auth.dto.ts new file mode 100644 index 0000000..ca407ce --- /dev/null +++ b/src/auth/dto/update-auth.dto.ts @@ -0,0 +1,5 @@ +import { PartialType } from '@nestjs/swagger'; + +import { CreateAuthDto } from './create-auth.dto'; + +export class UpdateAuthDto extends PartialType(CreateAuthDto) {} diff --git a/src/auth/entities/auth.entity.ts b/src/auth/entities/auth.entity.ts new file mode 100644 index 0000000..15f15a8 --- /dev/null +++ b/src/auth/entities/auth.entity.ts @@ -0,0 +1 @@ +export class Auth {} diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 3e0719b..d3e4782 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -16,3 +16,11 @@ export const SWAGGER_CONFIG = new DocumentBuilder() .build(); export const FAVS_TABLE_ID = 'favs'; +export const CRYPT_SALT = Number(process.env.CRYPT_SALT); + +export const jwtConstants = { + accessSecret: process.env.JWT_SECRET_KEY, + refreshSecret: process.env.JWT_SECRET_KEY, + accessExpiresIn: process.env.TOKEN_EXPIRE_TIME, + refreshExpiresIn: process.env.TOKEN_REFRESH_EXPIRE_TIME, +}; diff --git a/src/lib/shared/skipAuth.decorator.ts b/src/lib/shared/skipAuth.decorator.ts new file mode 100644 index 0000000..f695f75 --- /dev/null +++ b/src/lib/shared/skipAuth.decorator.ts @@ -0,0 +1,4 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; +export const SkipAuth = () => SetMetadata(IS_SKIP_AUTH_KEY, true); From 81a78b79a9ac1c6f97367700617eab6348d0e3c2 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 13:04:20 +0200 Subject: [PATCH 03/20] fix: to skip auth work as expected --- src/auth/auth.guard.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index ceea630..610ef22 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -4,18 +4,27 @@ import { Injectable, UnauthorizedException, } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { Observable } from 'rxjs'; import { jwtConstants } from '../lib/const/const'; +import { IS_SKIP_AUTH_KEY } from '../lib/shared/skipAuth.decorator'; @Injectable() export class AuthGuard implements CanActivate { - constructor(private readonly jwtService: JwtService) {} + constructor( + private readonly jwtService: JwtService, + private readonly reflector: Reflector, + ) {} canActivate( context: ExecutionContext, ): boolean | Promise | Observable { + if (this.isSkipAuth(context)) { + return true; + } + const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); @@ -39,6 +48,13 @@ export class AuthGuard implements CanActivate { return true; } + private isSkipAuth(context: ExecutionContext): boolean { + return this.reflector.getAllAndOverride(IS_SKIP_AUTH_KEY, [ + context.getHandler(), + context.getClass(), + ]); + } + private extractTokenFromHeader(request: Request): string | undefined { if (!('authorization' in request.headers)) return undefined; From 00e5d3ebe10ea9dc00c6adbe3a772f5419b26f3b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 13:31:33 +0200 Subject: [PATCH 04/20] refactor: move isSkipAuth constant to constants file --- src/lib/const/const.ts | 2 ++ src/lib/shared/skipAuth.decorator.ts | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index d3e4782..c225f54 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -24,3 +24,5 @@ export const jwtConstants = { accessExpiresIn: process.env.TOKEN_EXPIRE_TIME, refreshExpiresIn: process.env.TOKEN_REFRESH_EXPIRE_TIME, }; + +export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; diff --git a/src/lib/shared/skipAuth.decorator.ts b/src/lib/shared/skipAuth.decorator.ts index f695f75..a4a6334 100644 --- a/src/lib/shared/skipAuth.decorator.ts +++ b/src/lib/shared/skipAuth.decorator.ts @@ -1,4 +1,5 @@ import { SetMetadata } from '@nestjs/common'; -export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; +import { IS_SKIP_AUTH_KEY } from '../const/const'; + export const SkipAuth = () => SetMetadata(IS_SKIP_AUTH_KEY, true); From 180c7eab8bf3f2ffd978e1c6ce82e1fe3f535efd Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 13:32:00 +0200 Subject: [PATCH 05/20] refactor: rearrange constants --- src/lib/const/const.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index c225f54..2cc0598 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -8,6 +8,13 @@ export const errorMessage = { INVALID_PASSWORD: 'Wrong password provided!', } as const; +export const jwtConstants = { + accessSecret: process.env.JWT_SECRET_KEY, + refreshSecret: process.env.JWT_SECRET_KEY, + accessExpiresIn: process.env.TOKEN_EXPIRE_TIME, + refreshExpiresIn: process.env.TOKEN_REFRESH_EXPIRE_TIME, +} as const; + export const SWAGGER_CONFIG = new DocumentBuilder() .setTitle('Home library') .setDescription('Home library service api') @@ -18,11 +25,4 @@ export const SWAGGER_CONFIG = new DocumentBuilder() export const FAVS_TABLE_ID = 'favs'; export const CRYPT_SALT = Number(process.env.CRYPT_SALT); -export const jwtConstants = { - accessSecret: process.env.JWT_SECRET_KEY, - refreshSecret: process.env.JWT_SECRET_KEY, - accessExpiresIn: process.env.TOKEN_EXPIRE_TIME, - refreshExpiresIn: process.env.TOKEN_REFRESH_EXPIRE_TIME, -}; - export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; From 5dd40013e08b8b18337c7fd1727572265bc95730 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 13:33:03 +0200 Subject: [PATCH 06/20] refactor: rename constants --- src/auth/auth.guard.ts | 2 +- src/auth/auth.service.ts | 8 ++++---- src/lib/const/const.ts | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 610ef22..ba2b626 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -34,7 +34,7 @@ export class AuthGuard implements CanActivate { try { const payload = this.jwtService.verifyAsync(token, { - secret: jwtConstants.accessSecret, + secret: jwtConstants.ACCESS_SECRET, }); console.log(payload); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5166fdb..1495c46 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -35,14 +35,14 @@ export class AuthService { // Create access token const waitAccessToken = this.jwtService.sign(payload, { - secret: jwtConstants.accessSecret, - expiresIn: jwtConstants.accessExpiresIn, + secret: jwtConstants.ACCESS_SECRET, + expiresIn: jwtConstants.ACCESS_EXPIRES_IN, }); // Create refresh token const waitRefreshToken = this.jwtService.sign(payload, { - secret: jwtConstants.refreshSecret, - expiresIn: jwtConstants.refreshExpiresIn, + secret: jwtConstants.REFRESH_SECRET, + expiresIn: jwtConstants.REFRESH_EXPIRES_IN, }); const [accessToken, refreshToken] = await Promise.all([ diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 2cc0598..4c1cb3c 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -9,10 +9,10 @@ export const errorMessage = { } as const; export const jwtConstants = { - accessSecret: process.env.JWT_SECRET_KEY, - refreshSecret: process.env.JWT_SECRET_KEY, - accessExpiresIn: process.env.TOKEN_EXPIRE_TIME, - refreshExpiresIn: process.env.TOKEN_REFRESH_EXPIRE_TIME, + ACCESS_SECRET: process.env.JWT_SECRET_KEY, + REFRESH_SECRET: process.env.JWT_SECRET_KEY, + ACCESS_EXPIRES_IN: process.env.TOKEN_EXPIRE_TIME, + REFRESH_EXPIRES_IN: process.env.TOKEN_REFRESH_EXPIRE_TIME, } as const; export const SWAGGER_CONFIG = new DocumentBuilder() From 50af6c7afc991df3af8a4a176aab7dbecd95798b Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 14:09:26 +0200 Subject: [PATCH 07/20] fix: await async jwt verify --- src/auth/auth.guard.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index ba2b626..0b541e2 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -6,10 +6,8 @@ import { } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; -import { Observable } from 'rxjs'; -import { jwtConstants } from '../lib/const/const'; -import { IS_SKIP_AUTH_KEY } from '../lib/shared/skipAuth.decorator'; +import { IS_SKIP_AUTH_KEY, jwtConstants } from '../lib/const/const'; @Injectable() export class AuthGuard implements CanActivate { @@ -18,9 +16,7 @@ export class AuthGuard implements CanActivate { private readonly reflector: Reflector, ) {} - canActivate( - context: ExecutionContext, - ): boolean | Promise | Observable { + async canActivate(context: ExecutionContext) { if (this.isSkipAuth(context)) { return true; } @@ -33,14 +29,9 @@ export class AuthGuard implements CanActivate { } try { - const payload = this.jwtService.verifyAsync(token, { + await this.jwtService.verifyAsync(token, { secret: jwtConstants.ACCESS_SECRET, }); - - console.log(payload); - - // TODO: remove test - request.test = payload; } catch (e) { throw new UnauthorizedException(); } From 7834026da923c9df851b6991db1cde5f61f71d5f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sat, 30 Mar 2024 14:58:21 +0200 Subject: [PATCH 08/20] feat: implement login --- src/auth/auth.controller.ts | 35 +++--------------- src/auth/auth.service.ts | 73 ++++++++++++++++++------------------- src/lib/const/const.ts | 1 + 3 files changed, 42 insertions(+), 67 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 5eb50e4..9bf041e 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,16 +1,7 @@ -import { - Body, - Controller, - Delete, - Get, - Param, - Patch, - Post, -} from '@nestjs/common'; +import { Body, Controller, Post } from '@nestjs/common'; import { AuthService } from './auth.service'; import { CreateAuthDto } from './dto/create-auth.dto'; -import { UpdateAuthDto } from './dto/update-auth.dto'; import { SkipAuth } from '../lib/shared/skipAuth.decorator'; @Controller('auth') @@ -19,27 +10,13 @@ export class AuthController { @SkipAuth() @Post('signup') - async create(@Body() createAuthDto: CreateAuthDto) { + async signUp(@Body() createAuthDto: CreateAuthDto) { return this.authService.signUp(createAuthDto); } - @Get() - findAll() { - return this.authService.findAll(); - } - - @Get(':id') - findOne(@Param('id') id: string) { - return this.authService.findOne(+id); - } - - @Patch(':id') - update(@Param('id') id: string, @Body() updateAuthDto: UpdateAuthDto) { - return this.authService.update(+id, updateAuthDto); - } - - @Delete(':id') - remove(@Param('id') id: string) { - return this.authService.remove(+id); + @SkipAuth() + @Post('login') + async login(@Body() createAuthDto: CreateAuthDto) { + return this.authService.login(createAuthDto); } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 1495c46..e247940 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,10 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { + ConflictException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import { CreateAuthDto } from './dto/create-auth.dto'; -import { UpdateAuthDto } from './dto/update-auth.dto'; -import { CRYPT_SALT, jwtConstants } from '../lib/const/const'; +import { CRYPT_SALT, errorMessage, jwtConstants } from '../lib/const/const'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() @@ -15,12 +19,13 @@ export class AuthService { ) {} async signUp({ login, password }: CreateAuthDto) { + // Check if user already exists const user = await this.prismaService.user.findFirst({ - where: { AND: [{ login }, { password }] }, + where: { login }, }); if (user) { - return 'The user is already exists'; + throw new ConflictException(errorMessage.USER_ALREADY_EXISTS); } // hash the password @@ -32,7 +37,28 @@ export class AuthService { }); const payload = { userId: createdUser.id, login: createdUser.login }; + return this.genTokens(payload); + } + + async login({ login, password }: CreateAuthDto) { + const user = await this.prismaService.user.findFirst({ + where: { login }, + }); + if (!user) { + throw new NotFoundException(errorMessage.USER_NOT_FOUND); + } + + const isSamePassword = await bcrypt.compare(password, user.password); + if (!isSamePassword) { + throw new ForbiddenException(errorMessage.INVALID_PASSWORD); + } + + const payload = { userId: user.id, login: user.login }; + return this.genTokens(payload); + } + + private async genTokens(payload: { userId: string; login: string }) { // Create access token const waitAccessToken = this.jwtService.sign(payload, { secret: jwtConstants.ACCESS_SECRET, @@ -50,38 +76,9 @@ export class AuthService { waitRefreshToken, ]); - return { accessToken, refreshToken }; - } - - async signIn({ login, password }: CreateAuthDto) { - const user = await this.prismaService.user.findFirst({ - where: { AND: [{ login }, { password }] }, - }); - - if (!user) { - return 'no user = no token'; - } - - return 'token'; - } - - create(createAuthDto: CreateAuthDto) { - return 'This action adds a new auth'; - } - - findAll() { - return `This action returns all auth`; - } - - findOne(id: number) { - return `This action returns a #${id} auth`; - } - - update(id: number, updateAuthDto: UpdateAuthDto) { - return `This action updates a #${id} auth`; - } - - remove(id: number) { - return `This action removes a #${id} auth`; + return { + accessToken, + refreshToken, + }; } } diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 4c1cb3c..9c638a2 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -6,6 +6,7 @@ export const errorMessage = { ARTIST_NOT_FOUND: 'Artist not found!', ALBUM_NOT_FOUND: 'Album not found!', INVALID_PASSWORD: 'Wrong password provided!', + USER_ALREADY_EXISTS: 'The user is already exists', } as const; export const jwtConstants = { From f3e379afe0beee67c63521c8e331e72bce702b13 Mon Sep 17 00:00:00 2001 From: Vadzim Antonau Date: Fri, 8 Mar 2024 12:42:45 +0300 Subject: [PATCH 09/20] chore: change test script --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 6c526cb..12191cf 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ "start:prod": "node dist/src/main.js", "lint": "eslint \"{src,apps,libs,test,types,db}/**/*.ts\" --fix", "type-check": "tsc --noEmit", - "test": "jest --testPathIgnorePatterns refresh.e2e.spec.ts --noStackTrace --runInBand", - "test:noAuth": "jest --testMatch \"/*.spec.ts\" --noStackTrace --runInBand", + "test": "jest --testMatch \"/*.spec.ts\" --noStackTrace --runInBand", "test:auth": "cross-env TEST_MODE=auth jest --testPathIgnorePatterns refresh.e2e.spec.ts --noStackTrace --runInBand", "test:refresh": "cross-env TEST_MODE=auth jest --testPathPattern refresh.e2e.spec.ts --noStackTrace --runInBand", "test:watch": "jest --watch", From 07db642a2b087d4330dd6424c24254523e1ada54 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 31 Mar 2024 14:46:33 +0300 Subject: [PATCH 10/20] fix: to return user id on auth --- src/auth/auth.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index e247940..c055c25 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -37,7 +37,8 @@ export class AuthService { }); const payload = { userId: createdUser.id, login: createdUser.login }; - return this.genTokens(payload); + const tokens = await this.genTokens(payload); + return { ...tokens, id: createdUser.id }; } async login({ login, password }: CreateAuthDto) { @@ -55,7 +56,8 @@ export class AuthService { } const payload = { userId: user.id, login: user.login }; - return this.genTokens(payload); + const tokens = await this.genTokens(payload); + return { ...tokens, id: user.id }; } private async genTokens(payload: { userId: string; login: string }) { From a10ebcd6cdc041616a49350f71c118ef6a09179f Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 31 Mar 2024 16:45:02 +0300 Subject: [PATCH 11/20] feat: implement refresh token --- src/auth/auth.controller.ts | 27 ++++++++++++++++++++++++++- src/auth/auth.guard.ts | 2 ++ src/auth/auth.service.ts | 23 +++++++++++++++++++++++ src/auth/dto/refresh-auth.dto.ts | 9 +++++++++ src/lib/const/const.ts | 1 + 5 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/auth/dto/refresh-auth.dto.ts diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 9bf041e..3184ce1 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,7 +1,15 @@ -import { Body, Controller, Post } from '@nestjs/common'; +import { + Body, + Controller, + Post, + Req, + UnauthorizedException, +} from '@nestjs/common'; import { AuthService } from './auth.service'; import { CreateAuthDto } from './dto/create-auth.dto'; +import { RefreshAuthDto } from './dto/refresh-auth.dto'; +import { errorMessage } from '../lib/const/const'; import { SkipAuth } from '../lib/shared/skipAuth.decorator'; @Controller('auth') @@ -19,4 +27,21 @@ export class AuthController { async login(@Body() createAuthDto: CreateAuthDto) { return this.authService.login(createAuthDto); } + + @SkipAuth() + @Post('refresh') + async refresh( + @Body() { userId, login }: RefreshAuthDto, + @Req() req: Request, + ) { + if (!('token' in req)) { + throw new UnauthorizedException(errorMessage.WRONG_REFRESH_TOKEN); + } + + return this.authService.refreshToken({ + userId, + login, + refreshToken: req.token as string, + }); + } } diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 0b541e2..3aec1e5 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -32,6 +32,8 @@ export class AuthGuard implements CanActivate { await this.jwtService.verifyAsync(token, { secret: jwtConstants.ACCESS_SECRET, }); + + request.token = token; } catch (e) { throw new UnauthorizedException(); } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index c055c25..5073590 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ConflictException, ForbiddenException, Injectable, @@ -60,6 +61,28 @@ export class AuthService { return { ...tokens, id: user.id }; } + async refreshToken({ + userId, + login, + refreshToken, + }: { + userId: string; + login: string; + refreshToken: string; + }) { + try { + await this.jwtService.verifyAsync(refreshToken, { + secret: jwtConstants.REFRESH_SECRET, + }); + + const payload = { userId, login }; + const tokens = await this.genTokens(payload); + return { ...tokens, id: userId }; + } catch (e) { + throw new BadRequestException(errorMessage.WRONG_REFRESH_TOKEN); + } + } + private async genTokens(payload: { userId: string; login: string }) { // Create access token const waitAccessToken = this.jwtService.sign(payload, { diff --git a/src/auth/dto/refresh-auth.dto.ts b/src/auth/dto/refresh-auth.dto.ts new file mode 100644 index 0000000..60bbbda --- /dev/null +++ b/src/auth/dto/refresh-auth.dto.ts @@ -0,0 +1,9 @@ +import { IsString, IsUUID } from 'class-validator'; + +export class RefreshAuthDto { + @IsUUID() + userId: string; + + @IsString() + login: string; +} diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 9c638a2..2d51e36 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -7,6 +7,7 @@ export const errorMessage = { ALBUM_NOT_FOUND: 'Album not found!', INVALID_PASSWORD: 'Wrong password provided!', USER_ALREADY_EXISTS: 'The user is already exists', + WRONG_REFRESH_TOKEN: 'The refresh token is wrong or does not exists', } as const; export const jwtConstants = { From 415f57e0f3f898988d68b4081b3a12087723a772 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 31 Mar 2024 17:21:04 +0300 Subject: [PATCH 12/20] feat: implement basic logging service --- src/app.controller.ts | 4 ++- src/app.module.ts | 2 ++ .../logging-service.controller.ts | 7 ++++ src/logging-service/logging-service.module.ts | 9 ++++++ .../logging-service.service.ts | 4 +++ src/logging-service/logging.interceptor.ts | 32 +++++++++++++++++++ src/user/user.controller.ts | 3 ++ 7 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/logging-service/logging-service.controller.ts create mode 100644 src/logging-service/logging-service.module.ts create mode 100644 src/logging-service/logging-service.service.ts create mode 100644 src/logging-service/logging.interceptor.ts diff --git a/src/app.controller.ts b/src/app.controller.ts index 06b88e8..66fa63d 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,8 +1,10 @@ -import { Controller, Get } from '@nestjs/common'; +import { Controller, Get, UseInterceptors } from '@nestjs/common'; import { AppService } from './app.service'; import { SkipAuth } from './lib/shared/skipAuth.decorator'; +import { LoggingInterceptor } from './logging-service/logging.interceptor'; +@UseInterceptors(LoggingInterceptor) @Controller() export class AppController { constructor(private readonly appService: AppService) {} diff --git a/src/app.module.ts b/src/app.module.ts index 02730ea..a2ecdee 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { FavoriteModule } from './favorite/favorite.module'; import { PrismaModule } from './prisma/prisma.module'; import { TrackModule } from './track/track.module'; import { UserModule } from './user/user.module'; +import { LoggingServiceModule } from './logging-service/logging-service.module'; @Module({ imports: [ @@ -24,6 +25,7 @@ import { UserModule } from './user/user.module'; PrismaModule, AuthModule, JwtModule, + LoggingServiceModule, ], controllers: [AppController], providers: [AppService, { provide: 'APP_GUARD', useClass: AuthGuard }], diff --git a/src/logging-service/logging-service.controller.ts b/src/logging-service/logging-service.controller.ts new file mode 100644 index 0000000..6a0d631 --- /dev/null +++ b/src/logging-service/logging-service.controller.ts @@ -0,0 +1,7 @@ +import { Controller } from '@nestjs/common'; +import { LoggingServiceService } from './logging-service.service'; + +@Controller('logging-service') +export class LoggingServiceController { + constructor(private readonly loggingServiceService: LoggingServiceService) {} +} diff --git a/src/logging-service/logging-service.module.ts b/src/logging-service/logging-service.module.ts new file mode 100644 index 0000000..43e8c30 --- /dev/null +++ b/src/logging-service/logging-service.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { LoggingServiceService } from './logging-service.service'; +import { LoggingServiceController } from './logging-service.controller'; + +@Module({ + controllers: [LoggingServiceController], + providers: [LoggingServiceService], +}) +export class LoggingServiceModule {} diff --git a/src/logging-service/logging-service.service.ts b/src/logging-service/logging-service.service.ts new file mode 100644 index 0000000..05a62e0 --- /dev/null +++ b/src/logging-service/logging-service.service.ts @@ -0,0 +1,4 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class LoggingServiceService {} diff --git a/src/logging-service/logging.interceptor.ts b/src/logging-service/logging.interceptor.ts new file mode 100644 index 0000000..ac13bb1 --- /dev/null +++ b/src/logging-service/logging.interceptor.ts @@ -0,0 +1,32 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const now = Date.now(); + return next.handle().pipe( + tap(() => { + const res = context.switchToHttp().getResponse(); + const { + req: { + baseUrl, + _parsedUrl: { query }, + }, + + statusCode, + } = res; + + console.log( + `Url: ${baseUrl || '/'}, Query: ${query}, Status code: ${statusCode}`, + ); + console.log(`After... ${Date.now() - now}ms`); + }), + ); + } +} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 34f9c39..057a38e 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -11,6 +11,7 @@ import { ParseUUIDPipe, Post, Put, + UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -22,7 +23,9 @@ import { UserService } from './user.service'; import { errorMessage } from '../lib/const/const'; import exclude from '../lib/shared/exclude'; import formatUserDate from '../lib/shared/formatUserDate'; +import { LoggingInterceptor } from '../logging-service/logging.interceptor'; +@UseInterceptors(LoggingInterceptor) @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} From 83c4ee3f26a5ab600bda3bfaa799037051c99946 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 14:31:25 +0300 Subject: [PATCH 13/20] refactor: rename module files --- src/app.controller.ts | 4 +--- src/app.module.ts | 18 +++++++++++++++--- .../logging-service.controller.ts | 7 ------- src/logging-service/logging-service.module.ts | 9 --------- src/logging/logging.controller.ts | 8 ++++++++ src/logging/logging.module.ts | 10 ++++++++++ .../logging.service.ts} | 2 +- src/user/user.controller.ts | 3 --- 8 files changed, 35 insertions(+), 26 deletions(-) delete mode 100644 src/logging-service/logging-service.controller.ts delete mode 100644 src/logging-service/logging-service.module.ts create mode 100644 src/logging/logging.controller.ts create mode 100644 src/logging/logging.module.ts rename src/{logging-service/logging-service.service.ts => logging/logging.service.ts} (61%) diff --git a/src/app.controller.ts b/src/app.controller.ts index 66fa63d..06b88e8 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,10 +1,8 @@ -import { Controller, Get, UseInterceptors } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { AppService } from './app.service'; import { SkipAuth } from './lib/shared/skipAuth.decorator'; -import { LoggingInterceptor } from './logging-service/logging.interceptor'; -@UseInterceptors(LoggingInterceptor) @Controller() export class AppController { constructor(private readonly appService: AppService) {} diff --git a/src/app.module.ts b/src/app.module.ts index a2ecdee..b5f9c41 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; import { JwtModule } from '@nestjs/jwt'; import { AlbumModule } from './album/album.module'; @@ -9,10 +10,11 @@ import { ArtistModule } from './artist/artist.module'; import { AuthGuard } from './auth/auth.guard'; import { AuthModule } from './auth/auth.module'; import { FavoriteModule } from './favorite/favorite.module'; +import { LoggingInterceptor } from './logging/logging.interceptor'; +import { LoggingModule } from './logging/logging.module'; import { PrismaModule } from './prisma/prisma.module'; import { TrackModule } from './track/track.module'; import { UserModule } from './user/user.module'; -import { LoggingServiceModule } from './logging-service/logging-service.module'; @Module({ imports: [ @@ -25,9 +27,19 @@ import { LoggingServiceModule } from './logging-service/logging-service.module'; PrismaModule, AuthModule, JwtModule, - LoggingServiceModule, + LoggingModule, ], controllers: [AppController], - providers: [AppService, { provide: 'APP_GUARD', useClass: AuthGuard }], + providers: [ + AppService, + { + provide: APP_GUARD, + useClass: AuthGuard, + }, + { + provide: APP_INTERCEPTOR, + useClass: LoggingInterceptor, + }, + ], }) export class AppModule {} diff --git a/src/logging-service/logging-service.controller.ts b/src/logging-service/logging-service.controller.ts deleted file mode 100644 index 6a0d631..0000000 --- a/src/logging-service/logging-service.controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Controller } from '@nestjs/common'; -import { LoggingServiceService } from './logging-service.service'; - -@Controller('logging-service') -export class LoggingServiceController { - constructor(private readonly loggingServiceService: LoggingServiceService) {} -} diff --git a/src/logging-service/logging-service.module.ts b/src/logging-service/logging-service.module.ts deleted file mode 100644 index 43e8c30..0000000 --- a/src/logging-service/logging-service.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Module } from '@nestjs/common'; -import { LoggingServiceService } from './logging-service.service'; -import { LoggingServiceController } from './logging-service.controller'; - -@Module({ - controllers: [LoggingServiceController], - providers: [LoggingServiceService], -}) -export class LoggingServiceModule {} diff --git a/src/logging/logging.controller.ts b/src/logging/logging.controller.ts new file mode 100644 index 0000000..bdb2016 --- /dev/null +++ b/src/logging/logging.controller.ts @@ -0,0 +1,8 @@ +import { Controller } from '@nestjs/common'; + +import { LoggingService } from './logging.service'; + +@Controller('logging') +export class LoggingController { + constructor(private readonly loggingServiceService: LoggingService) {} +} diff --git a/src/logging/logging.module.ts b/src/logging/logging.module.ts new file mode 100644 index 0000000..6fdb056 --- /dev/null +++ b/src/logging/logging.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { LoggingController } from './logging.controller'; +import { LoggingService } from './logging.service'; + +@Module({ + controllers: [LoggingController], + providers: [LoggingService], +}) +export class LoggingModule {} diff --git a/src/logging-service/logging-service.service.ts b/src/logging/logging.service.ts similarity index 61% rename from src/logging-service/logging-service.service.ts rename to src/logging/logging.service.ts index 05a62e0..1aeac8a 100644 --- a/src/logging-service/logging-service.service.ts +++ b/src/logging/logging.service.ts @@ -1,4 +1,4 @@ import { Injectable } from '@nestjs/common'; @Injectable() -export class LoggingServiceService {} +export class LoggingService {} diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 057a38e..34f9c39 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -11,7 +11,6 @@ import { ParseUUIDPipe, Post, Put, - UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -23,9 +22,7 @@ import { UserService } from './user.service'; import { errorMessage } from '../lib/const/const'; import exclude from '../lib/shared/exclude'; import formatUserDate from '../lib/shared/formatUserDate'; -import { LoggingInterceptor } from '../logging-service/logging.interceptor'; -@UseInterceptors(LoggingInterceptor) @Controller('user') export class UserController { constructor(private readonly userService: UserService) {} From 3213584edb7f196023f780c1e927424cedcd3551 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 16:22:27 +0300 Subject: [PATCH 14/20] feat: implement logging functionality --- src/app.module.ts | 4 +- src/auth/auth.guard.ts | 12 +++++- src/logging-service/logging.interceptor.ts | 32 ---------------- src/logging/logging.controller.ts | 2 +- src/logging/logging.interceptor.ts | 22 +++++++++++ src/logging/logging.service.ts | 43 +++++++++++++++++++++- 6 files changed, 77 insertions(+), 38 deletions(-) delete mode 100644 src/logging-service/logging.interceptor.ts create mode 100644 src/logging/logging.interceptor.ts diff --git a/src/app.module.ts b/src/app.module.ts index b5f9c41..03efe6c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -11,7 +11,7 @@ import { AuthGuard } from './auth/auth.guard'; import { AuthModule } from './auth/auth.module'; import { FavoriteModule } from './favorite/favorite.module'; import { LoggingInterceptor } from './logging/logging.interceptor'; -import { LoggingModule } from './logging/logging.module'; +import { LoggingService } from './logging/logging.service'; import { PrismaModule } from './prisma/prisma.module'; import { TrackModule } from './track/track.module'; import { UserModule } from './user/user.module'; @@ -27,11 +27,11 @@ import { UserModule } from './user/user.module'; PrismaModule, AuthModule, JwtModule, - LoggingModule, ], controllers: [AppController], providers: [ AppService, + LoggingService, { provide: APP_GUARD, useClass: AuthGuard, diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 3aec1e5..46b5ad0 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -8,12 +8,14 @@ import { Reflector } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { IS_SKIP_AUTH_KEY, jwtConstants } from '../lib/const/const'; +import { LoggingService } from '../logging/logging.service'; @Injectable() export class AuthGuard implements CanActivate { constructor( private readonly jwtService: JwtService, private readonly reflector: Reflector, + private readonly loggingService: LoggingService, ) {} async canActivate(context: ExecutionContext) { @@ -21,10 +23,15 @@ export class AuthGuard implements CanActivate { return true; } - const request = context.switchToHttp().getRequest(); + const ctx = context.switchToHttp(); + const request = ctx.getRequest(); + const response = ctx.getResponse(); const token = this.extractTokenFromHeader(request); if (!token) { + response.on('close', () => { + this.loggingService.logError(context, new Error('unauthorized')); + }); throw new UnauthorizedException(); } @@ -35,6 +42,9 @@ export class AuthGuard implements CanActivate { request.token = token; } catch (e) { + response.on('close', () => { + this.loggingService.logError(context, new Error('unauthorized')); + }); throw new UnauthorizedException(); } diff --git a/src/logging-service/logging.interceptor.ts b/src/logging-service/logging.interceptor.ts deleted file mode 100644 index ac13bb1..0000000 --- a/src/logging-service/logging.interceptor.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { - CallHandler, - ExecutionContext, - Injectable, - NestInterceptor, -} from '@nestjs/common'; -import { Observable, tap } from 'rxjs'; - -@Injectable() -export class LoggingInterceptor implements NestInterceptor { - intercept(context: ExecutionContext, next: CallHandler): Observable { - const now = Date.now(); - return next.handle().pipe( - tap(() => { - const res = context.switchToHttp().getResponse(); - const { - req: { - baseUrl, - _parsedUrl: { query }, - }, - - statusCode, - } = res; - - console.log( - `Url: ${baseUrl || '/'}, Query: ${query}, Status code: ${statusCode}`, - ); - console.log(`After... ${Date.now() - now}ms`); - }), - ); - } -} diff --git a/src/logging/logging.controller.ts b/src/logging/logging.controller.ts index bdb2016..70507a2 100644 --- a/src/logging/logging.controller.ts +++ b/src/logging/logging.controller.ts @@ -4,5 +4,5 @@ import { LoggingService } from './logging.service'; @Controller('logging') export class LoggingController { - constructor(private readonly loggingServiceService: LoggingService) {} + constructor(private readonly loggingService: LoggingService) {} } diff --git a/src/logging/logging.interceptor.ts b/src/logging/logging.interceptor.ts new file mode 100644 index 0000000..10dea87 --- /dev/null +++ b/src/logging/logging.interceptor.ts @@ -0,0 +1,22 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, +} from '@nestjs/common'; +import { Observable, tap } from 'rxjs'; + +import { LoggingService } from './logging.service'; + +@Injectable() +export class LoggingInterceptor implements NestInterceptor { + constructor(private readonly loggingService: LoggingService) {} + + intercept(context: ExecutionContext, next: CallHandler): Observable { + return next + .handle() + .pipe( + tap((responseBody) => this.loggingService.log(context, responseBody)), + ); + } +} diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index 1aeac8a..c797612 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -1,4 +1,43 @@ -import { Injectable } from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() -export class LoggingService {} +export class LoggingService { + log(context: ExecutionContext, responseBody: Record) { + const res = context.switchToHttp().getResponse(); + const { + req: { + url, + _parsedUrl: { query }, + }, + statusCode, + } = res; + const date = this.getFormattedTime(); + const urlStr = url.slice(0, url.indexOf('?')); + const body = JSON.stringify(responseBody); + + process.stdout.write( + `${date}: Url: ${urlStr || '/'}, Query: ${query}, Body: ${body}, Status code: ${statusCode}\n`, + ); + } + + logError(context: ExecutionContext, error: Error) { + const res = context.switchToHttp().getResponse(); + const { + req: { + url, + _parsedUrl: { query }, + }, + + statusCode, + } = res; + const date = this.getFormattedTime(); + + process.stdout.write( + `${date} Url: ${url || '/'}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`, + ); + } + + private getFormattedTime() { + return `${new Date().toLocaleTimeString('en-EU', { timeZone: 'Europe/Kyiv' })}`; + } +} From b97947b20c312f4fdf7c79a8fc7b7348169677a1 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 16:58:59 +0300 Subject: [PATCH 15/20] feat: implement to listen on error events --- src/logging/logging.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index c797612..e4938af 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -2,6 +2,16 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() export class LoggingService { + constructor() { + process.on('uncaughtException', (err) => { + console.log(err.message); + }); + + process.on('unhandledRejection', (err) => { + console.log((err as Error).message); + }); + } + log(context: ExecutionContext, responseBody: Record) { const res = context.switchToHttp().getResponse(); const { From 13577e13045d8f8dff398718a56ba8212a353469 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 17:03:27 +0300 Subject: [PATCH 16/20] chore: add log .env variables --- .env.example | 3 +++ environment.d.ts | 2 ++ 2 files changed, 5 insertions(+) diff --git a/.env.example b/.env.example index fe80dbc..9310758 100644 --- a/.env.example +++ b/.env.example @@ -11,3 +11,6 @@ POSTGRES_PASSWORD=mypassword POSTGRES_DB=home-library POSTGRES_PORT=5432 DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public" + +LOGS_MAX_FILE_SIZE=10 +LOGGING_LEVEL=3 diff --git a/environment.d.ts b/environment.d.ts index c802d9b..ac64137 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -11,5 +11,7 @@ namespace NodeJS { POSTGRES_DB:string; POSTGRES_PORT:string; DATABASE_URL:string; + LOGS_MAX_FILE_SIZE:string; + LOGGING_LEVEL:string; } } From 66bcddd3405489dc58eb705c51c5fa77e9400822 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 17:08:48 +0300 Subject: [PATCH 17/20] refactor: encapsulate ctx preparation --- src/logging/logging.service.ts | 35 ++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index e4938af..22cb85e 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -13,38 +13,41 @@ export class LoggingService { } log(context: ExecutionContext, responseBody: Record) { - const res = context.switchToHttp().getResponse(); - const { - req: { - url, - _parsedUrl: { query }, - }, - statusCode, - } = res; - const date = this.getFormattedTime(); - const urlStr = url.slice(0, url.indexOf('?')); - const body = JSON.stringify(responseBody); + const { date, statusCode, body, url, query } = this.prepareCtx( + context, + responseBody, + ); process.stdout.write( - `${date}: Url: ${urlStr || '/'}, Query: ${query}, Body: ${body}, Status code: ${statusCode}\n`, + `${date}: Url: ${url}, Query: ${query}, Body: ${body}, Status code: ${statusCode}\n`, ); } logError(context: ExecutionContext, error: Error) { + const { date, query, url, statusCode } = this.prepareCtx(context); + + process.stdout.write( + `${date} Url: ${url}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`, + ); + } + + private prepareCtx( + context: ExecutionContext, + responseBody: Record = {}, + ) { const res = context.switchToHttp().getResponse(); const { req: { url, _parsedUrl: { query }, }, - statusCode, } = res; const date = this.getFormattedTime(); + const urlStr = url.slice(0, url.indexOf('?')) || '/'; + const body = JSON.stringify(responseBody); - process.stdout.write( - `${date} Url: ${url || '/'}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`, - ); + return { date, url: urlStr, body, statusCode, query }; } private getFormattedTime() { From 396b594950369dbb6991e59a2d4d25d664c299cc Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 17:32:22 +0300 Subject: [PATCH 18/20] feat: implement writing logs to the file --- src/lib/const/const.ts | 5 ++++- src/logging/logging.service.ts | 32 ++++++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 2d51e36..a843c92 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -1,3 +1,5 @@ +import * as path from 'path'; + import { DocumentBuilder } from '@nestjs/swagger'; export const errorMessage = { @@ -26,5 +28,6 @@ export const SWAGGER_CONFIG = new DocumentBuilder() export const FAVS_TABLE_ID = 'favs'; export const CRYPT_SALT = Number(process.env.CRYPT_SALT); - export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; +export const LOGS_FILE_NAME = 'log.txt'; +export const LOGS_FILE_PATH = path.resolve(LOGS_FILE_NAME); diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index 22cb85e..8caa48e 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -1,14 +1,22 @@ +import * as fs from 'fs'; + import { ExecutionContext, Injectable } from '@nestjs/common'; +import { LOGS_FILE_PATH } from '../lib/const/const'; + @Injectable() export class LoggingService { constructor() { process.on('uncaughtException', (err) => { - console.log(err.message); + const errorMsg = `Error occurred! (${err.message})`; + console.log(errorMsg); + void this.writeLogFile(errorMsg); }); process.on('unhandledRejection', (err) => { - console.log((err as Error).message); + const errorMsg = `Error occurred! (${(err as Error).message})`; + console.log(errorMsg); + void this.writeLogFile(errorMsg); }); } @@ -17,18 +25,26 @@ export class LoggingService { context, responseBody, ); + const logStr = `${date}: Url: ${url}, Query: ${query}, Body: ${body}, Status code: ${statusCode}\n`; - process.stdout.write( - `${date}: Url: ${url}, Query: ${query}, Body: ${body}, Status code: ${statusCode}\n`, - ); + process.stdout.write(logStr); + void this.writeLogFile(logStr); } logError(context: ExecutionContext, error: Error) { const { date, query, url, statusCode } = this.prepareCtx(context); + const logStr = `${date} Url: ${url}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`; - process.stdout.write( - `${date} Url: ${url}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`, - ); + process.stdout.write(logStr); + void this.writeLogFile(logStr); + } + + private async writeLogFile(logStr: string) { + fs.createWriteStream(LOGS_FILE_PATH, { flags: 'a' }) + .on('error', (e) => { + console.log(e.message); + }) + .write(`${logStr}\n`); } private prepareCtx( From d4353e36a8024644f53a416abfdb1625bfe778eb Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 18:12:28 +0300 Subject: [PATCH 19/20] feat: implement logs file rotation --- .env.example | 2 +- environment.d.ts | 2 +- src/lib/const/const.ts | 5 ++--- src/logging/logging.service.ts | 27 +++++++++++++++++++++++++-- tsconfig.json | 2 +- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 9310758..31ab3e2 100644 --- a/.env.example +++ b/.env.example @@ -12,5 +12,5 @@ POSTGRES_DB=home-library POSTGRES_PORT=5432 DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}?schema=public" -LOGS_MAX_FILE_SIZE=10 +LOGS_MAX_FILE_SIZE_KB=10 LOGGING_LEVEL=3 diff --git a/environment.d.ts b/environment.d.ts index ac64137..d70793b 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -11,7 +11,7 @@ namespace NodeJS { POSTGRES_DB:string; POSTGRES_PORT:string; DATABASE_URL:string; - LOGS_MAX_FILE_SIZE:string; + LOGS_MAX_FILE_SIZE_KB:string; LOGGING_LEVEL:string; } } diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index a843c92..09f773f 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -1,5 +1,3 @@ -import * as path from 'path'; - import { DocumentBuilder } from '@nestjs/swagger'; export const errorMessage = { @@ -30,4 +28,5 @@ export const FAVS_TABLE_ID = 'favs'; export const CRYPT_SALT = Number(process.env.CRYPT_SALT); export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; export const LOGS_FILE_NAME = 'log.txt'; -export const LOGS_FILE_PATH = path.resolve(LOGS_FILE_NAME); +export const LOGS_MAX_FILE_SIZE = + Number(process.env.LOGS_MAX_FILE_SIZE_KB) * 1000; diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index 8caa48e..da92775 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -1,11 +1,16 @@ import * as fs from 'fs'; +import * as path from 'path'; import { ExecutionContext, Injectable } from '@nestjs/common'; -import { LOGS_FILE_PATH } from '../lib/const/const'; +import { LOGS_FILE_NAME, LOGS_MAX_FILE_SIZE } from '../lib/const/const'; @Injectable() export class LoggingService { + private logsFileNameTemp = LOGS_FILE_NAME; + + private logsFileVersion = 0; + constructor() { process.on('uncaughtException', (err) => { const errorMsg = `Error occurred! (${err.message})`; @@ -40,7 +45,25 @@ export class LoggingService { } private async writeLogFile(logStr: string) { - fs.createWriteStream(LOGS_FILE_PATH, { flags: 'a' }) + const logsFilePath = path.resolve(this.logsFileNameTemp); + + try { + if (fs.statSync(logsFilePath).size >= LOGS_MAX_FILE_SIZE) { + const name = this.logsFileNameTemp.slice( + 0, + this.logsFileNameTemp.lastIndexOf('.'), + ); + this.logsFileVersion += 1; + const fileNameNoDigits = name.replaceAll(/\d/g, ''); + + this.logsFileNameTemp = `${fileNameNoDigits}${this.logsFileVersion}.txt`; + } + } catch (e) { + // + } + console.log(logsFilePath); + + fs.createWriteStream(logsFilePath, { flags: 'a' }) .on('error', (e) => { console.log(e.message); }) diff --git a/tsconfig.json b/tsconfig.json index 4e9062c..d4931b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, - "target": "es2017", + "target": "es2021", "sourceMap": true, "outDir": "./dist", "baseUrl": "./", From a97290c555a12cda34c885a3e96fdce3e1fe2c91 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 1 Apr 2024 19:04:15 +0300 Subject: [PATCH 20/20] fix: to properly rotate log file --- src/lib/const/const.ts | 1 + src/logging/logging.service.ts | 67 ++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/lib/const/const.ts b/src/lib/const/const.ts index 09f773f..095e744 100644 --- a/src/lib/const/const.ts +++ b/src/lib/const/const.ts @@ -28,5 +28,6 @@ export const FAVS_TABLE_ID = 'favs'; export const CRYPT_SALT = Number(process.env.CRYPT_SALT); export const IS_SKIP_AUTH_KEY = 'isSkipAuth'; export const LOGS_FILE_NAME = 'log.txt'; +export const LOGS_ERROR_FILE_NAME = 'logError.txt'; export const LOGS_MAX_FILE_SIZE = Number(process.env.LOGS_MAX_FILE_SIZE_KB) * 1000; diff --git a/src/logging/logging.service.ts b/src/logging/logging.service.ts index da92775..09e39ba 100644 --- a/src/logging/logging.service.ts +++ b/src/logging/logging.service.ts @@ -3,7 +3,11 @@ import * as path from 'path'; import { ExecutionContext, Injectable } from '@nestjs/common'; -import { LOGS_FILE_NAME, LOGS_MAX_FILE_SIZE } from '../lib/const/const'; +import { + LOGS_ERROR_FILE_NAME, + LOGS_FILE_NAME, + LOGS_MAX_FILE_SIZE, +} from '../lib/const/const'; @Injectable() export class LoggingService { @@ -11,6 +15,10 @@ export class LoggingService { private logsFileVersion = 0; + private logsErrorFileNameTemp = LOGS_ERROR_FILE_NAME; + + private logsErrorFileVersion = 0; + constructor() { process.on('uncaughtException', (err) => { const errorMsg = `Error occurred! (${err.message})`; @@ -41,33 +49,62 @@ export class LoggingService { const logStr = `${date} Url: ${url}, Query: ${query}, Body: ${null}, Status code: ${statusCode} Error message: (${error.message})\n`; process.stdout.write(logStr); - void this.writeLogFile(logStr); + void this.writeErrorLogsFile(logStr); } private async writeLogFile(logStr: string) { - const logsFilePath = path.resolve(this.logsFileNameTemp); + const { fileName, version } = this.incrFileVersion( + path.resolve(this.logsFileNameTemp), + this.logsFileNameTemp, + this.logsFileVersion, + ); + this.logsFileNameTemp = fileName; + this.logsFileVersion = version; + + fs.createWriteStream(path.resolve(this.logsFileNameTemp), { flags: 'a' }) + .on('error', (e) => { + console.log(e.message); + }) + .write(`${logStr}\n`); + } + + private writeErrorLogsFile(logStr: string) { + const logsErrorFilePath = path.resolve(this.logsErrorFileNameTemp); + + const { fileName, version } = this.incrFileVersion( + logsErrorFilePath, + this.logsErrorFileNameTemp, + this.logsErrorFileVersion, + ); + this.logsErrorFileNameTemp = fileName; + this.logsErrorFileVersion = version; + + fs.createWriteStream(logsErrorFilePath, { flags: 'a' }) + .on('error', (e) => { + console.log(e.message); + }) + .write(`${logStr}\n`); + } + private incrFileVersion( + logsFilePath: string, + fileName: string, + version: number, + ) { try { if (fs.statSync(logsFilePath).size >= LOGS_MAX_FILE_SIZE) { - const name = this.logsFileNameTemp.slice( - 0, - this.logsFileNameTemp.lastIndexOf('.'), - ); - this.logsFileVersion += 1; + const name = fileName.slice(0, fileName.lastIndexOf('.')); + const newVersion = version + 1; const fileNameNoDigits = name.replaceAll(/\d/g, ''); - this.logsFileNameTemp = `${fileNameNoDigits}${this.logsFileVersion}.txt`; + const newFileName = `${fileNameNoDigits}${newVersion}.txt`; + return { fileName: newFileName, version: newVersion }; } } catch (e) { // } - console.log(logsFilePath); - fs.createWriteStream(logsFilePath, { flags: 'a' }) - .on('error', (e) => { - console.log(e.message); - }) - .write(`${logStr}\n`); + return { fileName, version }; } private prepareCtx(