From b22a787efff00c1ded2e2f70ba4f96c9e25ce5cd Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 4 Feb 2026 06:19:06 +0300 Subject: [PATCH] feat: add passport --- package.json | 1 + src/app.module.ts | 4 +- src/config/env/index.ts | 1 + src/config/env/passport.env.ts | 16 ++++++ src/config/index.ts | 1 + .../interfaces/all-configs.interface.ts | 2 + src/config/interfaces/passport.interface.ts | 5 ++ src/config/loaders/index.ts | 1 + src/config/loaders/passport.config.loader.ts | 12 ++++ src/config/validators/index.ts | 1 + src/config/validators/passport.validator.ts | 14 +++++ src/modules/auth/auth.controller.ts | 15 ++++- src/modules/auth/auth.module.ts | 9 +++ src/modules/auth/auth.service.ts | 55 ++++++++++++++++++- yarn.lock | 20 +++++-- 15 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 src/config/env/passport.env.ts create mode 100644 src/config/interfaces/passport.interface.ts create mode 100644 src/config/loaders/index.ts create mode 100644 src/config/loaders/passport.config.loader.ts create mode 100644 src/config/validators/passport.validator.ts diff --git a/package.json b/package.json index f2e5294..9b765c3 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@prisma/client": "^7.3.0", "@teacinema/common": "^1.0.0", "@teacinema/contracts": "^1.0.0", + "@teacinema/passport": "^1.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.3", "dotenv-expand": "^12.0.3", diff --git a/src/app.module.ts b/src/app.module.ts index e94566b..505d7e0 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' -import { dabataseEnv, grpcEnv, redisEnv } from './config' +import { dabataseEnv, grpcEnv, passportEnv, redisEnv } from './config' import { PrismaModule } from './infra/prisma/prisma.module' import { RedisModule } from './infra/redis/redis.module' import { AuthModule } from './modules/auth/auth.module' @@ -11,7 +11,7 @@ import { OtpModule } from './modules/otp/otp.module' imports: [ ConfigModule.forRoot({ isGlobal: true, - load: [dabataseEnv, grpcEnv, redisEnv] + load: [dabataseEnv, grpcEnv, redisEnv, passportEnv] }), PrismaModule, RedisModule, diff --git a/src/config/env/index.ts b/src/config/env/index.ts index 72c669e..30ff333 100644 --- a/src/config/env/index.ts +++ b/src/config/env/index.ts @@ -1,3 +1,4 @@ export * from './grpc.env' export * from './database.env' export * from './redis.env' +export * from './passport.env' diff --git a/src/config/env/passport.env.ts b/src/config/env/passport.env.ts new file mode 100644 index 0000000..7bbed4c --- /dev/null +++ b/src/config/env/passport.env.ts @@ -0,0 +1,16 @@ +import { registerAs } from '@nestjs/config' + +import { validateEnv } from '@/shared/utils' + +import { PassportConfig } from '../interfaces/passport.interface' +import { PassportValidator } from '../validators' + +export const passportEnv = registerAs('passport', () => { + validateEnv(process.env, PassportValidator) + + return { + secretKey: process.env.PASSPORT_SECRET_KEY, + accessTtl: parseInt(process.env.PASSPORT_ACCESS_TTL), + refreshTtl: parseInt(process.env.PASSPORT_REFRESH_TTL) + } +}) diff --git a/src/config/index.ts b/src/config/index.ts index 0f0484c..b400221 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,2 +1,3 @@ export * from './env' export * from './interfaces' +export * from './loaders' diff --git a/src/config/interfaces/all-configs.interface.ts b/src/config/interfaces/all-configs.interface.ts index 84e9347..9a39d8f 100644 --- a/src/config/interfaces/all-configs.interface.ts +++ b/src/config/interfaces/all-configs.interface.ts @@ -1,9 +1,11 @@ import { DatabaseConfig } from './database.interface' import { GrpcConfig } from './grpc.interface' +import { PassportConfig } from './passport.interface' import { RedisConfig } from './redis.interface' export interface AllConfigs { database: DatabaseConfig grpc: GrpcConfig redis: RedisConfig + passport: PassportConfig } diff --git a/src/config/interfaces/passport.interface.ts b/src/config/interfaces/passport.interface.ts new file mode 100644 index 0000000..4313798 --- /dev/null +++ b/src/config/interfaces/passport.interface.ts @@ -0,0 +1,5 @@ +export interface PassportConfig { + secretKey: string + accessTtl: number + refreshTtl: number +} diff --git a/src/config/loaders/index.ts b/src/config/loaders/index.ts new file mode 100644 index 0000000..02ff424 --- /dev/null +++ b/src/config/loaders/index.ts @@ -0,0 +1 @@ +export * from './passport.config.loader' diff --git a/src/config/loaders/passport.config.loader.ts b/src/config/loaders/passport.config.loader.ts new file mode 100644 index 0000000..c5f9d27 --- /dev/null +++ b/src/config/loaders/passport.config.loader.ts @@ -0,0 +1,12 @@ +import { ConfigService } from '@nestjs/config' +import { PassportOptions } from '@teacinema/passport' + +import { AllConfigs } from '@/config' + +export function getPassportConfig( + configService: ConfigService +): PassportOptions { + return { + secretKey: configService.get('passport.secretKey', { infer: true }) + } +} diff --git a/src/config/validators/index.ts b/src/config/validators/index.ts index 6f3e4eb..283f136 100644 --- a/src/config/validators/index.ts +++ b/src/config/validators/index.ts @@ -1,3 +1,4 @@ export * from './grpc.validator' export * from './database.validator' export * from './redis.validator' +export * from './passport.validator' diff --git a/src/config/validators/passport.validator.ts b/src/config/validators/passport.validator.ts new file mode 100644 index 0000000..e447e17 --- /dev/null +++ b/src/config/validators/passport.validator.ts @@ -0,0 +1,14 @@ +import { IsInt, IsNumber, IsString } from 'class-validator' + +export class PassportValidator { + @IsString() + public PASSPORT_SECRET_KEY: string + + @IsInt() + @IsNumber() + public PASSPORT_ACCESS_TTL: number + + @IsInt() + @IsNumber() + public PASSPORT_REFRESH_TTL: number +} diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 7717fa4..9272f9f 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -1,6 +1,8 @@ -import { Controller } from '@nestjs/common' -import { GrpcMethod } from '@nestjs/microservices' +import { Controller } from '@nestjs/common'; +import { GrpcMethod } from '@nestjs/microservices'; import type { + RefreshRequest, + RefreshResponse, SendOtpRequest, SendOtpResponse, VerifyOtpRequest, @@ -9,6 +11,10 @@ import type { import { AuthService } from './auth.service' + + + + @Controller() export class AuthController { constructor(private readonly authService: AuthService) {} @@ -22,4 +28,9 @@ export class AuthController { public VerifyOtp(data: VerifyOtpRequest): Promise { return this.authService.verifyOtp(data) } + + @GrpcMethod('AuthService', 'Refresh') + public refresh(data: RefreshRequest): RefreshResponse { + return this.authService.refresh(data) + } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 4910359..c4848d4 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -1,5 +1,8 @@ import { Module } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' +import { PassportModule } from '@teacinema/passport' +import { getPassportConfig } from '@/config' import { OtpService } from '@/modules/otp/otp.service' import { AuthController } from './auth.controller' @@ -7,6 +10,12 @@ import { AuthRepository } from './auth.repository' import { AuthService } from './auth.service' @Module({ + imports: [ + PassportModule.registerAsync({ + useFactory: getPassportConfig, + inject: [ConfigService] + }) + ], controllers: [AuthController], providers: [AuthService, AuthRepository, OtpService] }) diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index ed32690..3606738 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -1,24 +1,41 @@ import { Injectable } from '@nestjs/common' +import { ConfigService } from '@nestjs/config' import { RpcException } from '@nestjs/microservices' import { Account } from '@prisma/generated/client' import { RpcStatus } from '@teacinema/common' import { + type RefreshRequest, + type RefreshResponse, SendOtpRequest, SendOtpResponse, VerifyOtpRequest, VerifyOtpResponse } from '@teacinema/contracts/gen/auth' +import { PassportService } from '@teacinema/passport' +import { AllConfigs } from '@/config' import { OtpService } from '@/modules/otp/otp.service' import { AuthRepository } from './auth.repository' @Injectable() export class AuthService { + private readonly ACCESS_TOKEN_TTL: number + private readonly REFRESH_TOKEN_TTL: number + public constructor( + private readonly configService: ConfigService, private readonly authRepository: AuthRepository, - private readonly otpService: OtpService - ) {} + private readonly otpService: OtpService, + private readonly passportService: PassportService + ) { + this.ACCESS_TOKEN_TTL = configService.get('passport.accessTtl', { + infer: true + }) + this.REFRESH_TOKEN_TTL = configService.get('passport.refreshTtl', { + infer: true + }) + } public async sendOtp(data: SendOtpRequest): Promise { const { identifier, type } = data @@ -80,6 +97,38 @@ export class AuthService { }) } - return { accessToken: '123456', refreshToken: '123456' } + return this.generateTokens(account.id) + } + + private generateTokens(userId: Account['id']) { + const payload = { + sub: userId, + iat: Date.now(), + exp: Date.now() + this.ACCESS_TOKEN_TTL + } + const accessToken = this.passportService.generate( + payload.sub, + this.ACCESS_TOKEN_TTL + ) + const refreshToken = this.passportService.generate( + payload.sub, + this.REFRESH_TOKEN_TTL + ) + + return { accessToken, refreshToken } + } + + public refresh(data: RefreshRequest): RefreshResponse { + const { refreshToken } = data + const result = this.passportService.verify(refreshToken) + + if (!result.valid) { + throw new RpcException({ + code: RpcStatus.UNAUTHENTICATED, + details: result.reason + }) + } + + return this.generateTokens(result.userId); } } diff --git a/yarn.lock b/yarn.lock index a78b8ed..973c245 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1055,7 +1055,7 @@ webpack "5.104.1" webpack-node-externals "3.0.0" -"@nestjs/common@^11.0.1": +"@nestjs/common@^11.0.1", "@nestjs/common@^11.1.12": version "11.1.12" resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-11.1.12.tgz#bd28d34fa6bf676671c70be8148b5405ccd3d422" integrity sha512-v6U3O01YohHO+IE3EIFXuRuu3VJILWzyMmSYZXpyBbnp0hk0mFyHxK2w3dF4I5WnbwiRbWlEXdeXFvPQ7qaZzw== @@ -1075,7 +1075,7 @@ dotenv-expand "12.0.1" lodash "4.17.21" -"@nestjs/core@^11.0.1": +"@nestjs/core@^11.0.1", "@nestjs/core@^11.1.12": version "11.1.12" resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-11.1.12.tgz#cdbff023cc87ed071aebafeacb209bc5997da1e4" integrity sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA== @@ -1360,9 +1360,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" @@ -1376,6 +1376,16 @@ dependencies: "@trivago/prettier-plugin-sort-imports" "^5.2.2" +"@teacinema/passport@^1.0.0": + version "1.0.0" + resolved "https://git.ksv741.keenetic.pro/api/packages/teacinema/npm/%40teacinema%2Fpassport/-/1.0.0/passport-1.0.0.tgz#c848d9fb089a975111bf19280cd4ca7b0118b73c" + integrity sha512-aPDPmaAkXiHKTkBxGR08DB3ih3cAZSF91CZaRBLXpQ7/NvfVLcOGmvk9jaAL5vbExtwT/OOQVKEMb0nIfVAzzg== + dependencies: + "@nestjs/common" "^11.1.12" + "@nestjs/core" "^11.1.12" + reflect-metadata "^0.2.2" + rxjs "^7.8.2" + "@tokenizer/inflate@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@tokenizer/inflate/-/inflate-0.4.1.tgz#fa6cdb8366151b3cc8426bf9755c1ea03a2fba08"