diff --git a/package.json b/package.json index 6937d14..e151eab 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@teacinema/contracts": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", + "cookie-parser": "^1.4.7", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, @@ -42,6 +43,7 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.1", "@teacinema/core": "1.0.7", + "@types/cookie-parser": "^1.4.10", "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/node": "^22.10.7", diff --git a/src/main.ts b/src/main.ts index 41fb7ab..51029c7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,6 +2,7 @@ import { Logger } from '@nestjs/common' import { ConfigService } from '@nestjs/config' import { NestFactory } from '@nestjs/core' import { SwaggerModule } from '@nestjs/swagger' +import cookieParser from 'cookie-parser' import { AppModule } from './core/app.module' import { getCorsConfig, swaggerConfig, validationPipe } from './core/config' @@ -13,6 +14,8 @@ async function bootstrap() { const logger = new Logger() const config = app.get(ConfigService) + app.use(cookieParser(config.getOrThrow('COOKIES_SECRET'))) + app.useGlobalPipes(validationPipe) app.useGlobalFilters(new GrpcExceptionFilter()) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index a14f01d..2dbbf92 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,12 +1,26 @@ -import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common' +import { + Body, + Controller, + HttpCode, + HttpStatus, + Post, + Req, + Res +} from '@nestjs/common' +import { ConfigService } from '@nestjs/config' import { ApiOperation } from '@nestjs/swagger' +import type { Request, Response } from 'express' +import { lastValueFrom } from 'rxjs' import { AuthClientGrpc } from './auth.grpc' import { SendOtpRequestDto, VerifyOtpRequestDto } from './dto' @Controller('auth') export class AuthController { - constructor(private readonly client: AuthClientGrpc) {} + constructor( + private readonly client: AuthClientGrpc, + private readonly config: ConfigService + ) {} @ApiOperation({ summary: 'Send OTP', @@ -24,7 +38,60 @@ export class AuthController { }) @Post('otp/verify') @HttpCode(HttpStatus.OK) - public verifyOtp(@Body() dto: VerifyOtpRequestDto) { - return this.client.verifyOtp(dto) + public async verifyOtp( + @Body() dto: VerifyOtpRequestDto, + @Res({ passthrough: true }) res: Response + ) { + const { accessToken, refreshToken } = await lastValueFrom( + this.client.verifyOtp(dto) + ) + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + domain: this.config.getOrThrow('COOKIES_DOMAIN'), + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000 + }) + + return { accessToken } + } + + @ApiOperation({ + summary: 'Refresh', + description: 'Refresh access token' + }) + @Post('refresh') + @HttpCode(HttpStatus.OK) + public async refresh( + @Req() req: Request, + @Res({ passthrough: true }) res: Response + ) { + const refreshToken = req.cookies?.refreshToken as string + + const { accessToken, refreshToken: newRefreshToken } = await lastValueFrom( + this.client.refresh({ refreshToken }) + ) + + res.cookie('refreshToken', newRefreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + domain: this.config.getOrThrow('COOKIES_DOMAIN'), + sameSite: 'lax', + maxAge: 30 * 24 * 60 * 60 * 1000 + }) + + return { accessToken } + } + + @ApiOperation({ + summary: 'Logout', + description: 'Logout user' + }) + @Post('logout') + @HttpCode(HttpStatus.OK) + public logout(@Res({ passthrough: true }) res: Response) { + res.clearCookie('refreshToken') + return { ok: true } } } diff --git a/src/modules/auth/auth.grpc.ts b/src/modules/auth/auth.grpc.ts index 750ad05..142ceac 100644 --- a/src/modules/auth/auth.grpc.ts +++ b/src/modules/auth/auth.grpc.ts @@ -2,6 +2,7 @@ import { Inject, Injectable, OnModuleInit } from '@nestjs/common' import type { ClientGrpc } from '@nestjs/microservices' import { AuthServiceClient, + RefreshRequest, SendOtpRequest, VerifyOtpRequest } from '@teacinema/contracts/gen/auth' @@ -23,4 +24,8 @@ export class AuthClientGrpc implements OnModuleInit { public verifyOtp(request: VerifyOtpRequest) { return this.authService.verifyOtp(request) } + + public refresh(request: RefreshRequest) { + return this.authService.refresh(request) + } } diff --git a/src/modules/auth/dto/rq/send-otp.request.dto.ts b/src/modules/auth/dto/rq/send-otp.request.dto.ts index a231c72..07ffe0b 100644 --- a/src/modules/auth/dto/rq/send-otp.request.dto.ts +++ b/src/modules/auth/dto/rq/send-otp.request.dto.ts @@ -6,7 +6,7 @@ import { IdentifierValidator } from '../../../../shared/validators' export class SendOtpRequestDto { @ApiProperty({ description: 'User phone or email', - example: '+791234567890' + example: '+79123456789' }) @Validate(IdentifierValidator) @IsString() diff --git a/src/modules/auth/dto/rq/verify-otp.request.dto.ts b/src/modules/auth/dto/rq/verify-otp.request.dto.ts index ce942d1..6afea9c 100644 --- a/src/modules/auth/dto/rq/verify-otp.request.dto.ts +++ b/src/modules/auth/dto/rq/verify-otp.request.dto.ts @@ -13,7 +13,7 @@ import { IdentifierValidator } from '../../../../shared/validators' export class VerifyOtpRequestDto { @ApiProperty({ description: 'User phone or email', - example: '+791234567890' + example: '+79123456789' }) @Validate(IdentifierValidator) @IsString() diff --git a/yarn.lock b/yarn.lock index 6910dd7..52a22c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1198,9 +1198,9 @@ integrity sha512-L006mySvhRpUlRHf3ONbXA0RG42gbQ9QAnmQuZx4zejHEsWB9702xe18MSYcBilv+lmYkQpiX/g3p8P7suLfqQ== "@teacinema/contracts@^1.0.0": - version "1.0.4" - resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.4/contracts-1.0.4.tgz#178dc6392aae52c4dc2b60bb1470a0a2885a8715" - integrity sha512-3jHP4NAoH9munaN2Iegvg0X6KN/SR1rwZU7KpbuBtlcTJqsLljTaHwG+XKjEL84Y9b+sF73jGGbg/w2Y8IBQmw== + version "1.0.5" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fcontracts/-/1.0.5/contracts-1.0.5.tgz#f6db65f58ba3621e81e886f54d6ec9e674c4785b" + integrity sha512-paFa/zVWKzDNuH04EyGPlG981YEX7Xt42gRwpfZwACGa5CCM5lehT7xKU4h9p/l9IebrOtTPDwJdQn/D41o3Gw== dependencies: "@nestjs/microservices" "^11.1.12" protoc "33.4.0" @@ -1314,6 +1314,11 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.10": + version "1.4.10" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.10.tgz#a045272a383a30597a01955d4f9c790018f214e4" + integrity sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg== + "@types/cookiejar@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.5.tgz#14a3e83fa641beb169a2dd8422d91c3c345a9a78" @@ -2375,12 +2380,25 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-parser@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.7.tgz#e2125635dfd766888ffe90d60c286404fa0e7b26" + integrity sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw== + dependencies: + cookie "0.7.2" + cookie-signature "1.0.6" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + cookie-signature@^1.2.1, cookie-signature@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793" integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg== -cookie@^0.7.1: +cookie@0.7.2, cookie@^0.7.1: version "0.7.2" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7" integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==