From e34b93aa834d066d9eff9591d6f333d5f01135ad Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Tue, 11 Nov 2025 09:50:18 +0330 Subject: [PATCH] refresh token --- src/common/decorators/rate-limit.decorator.ts | 18 ++++++++++++++++++ src/modules/admin/entities/admin.entity.ts | 2 +- src/modules/auth/auth.controller.ts | 9 +++++++++ src/modules/auth/auth.module.ts | 3 ++- src/modules/auth/dto/refresh-token.dto.ts | 9 +++++++++ src/modules/auth/services/auth.service.ts | 11 +++++++++-- .../users/entities/refresh-token.entity.ts | 4 ++-- src/modules/users/entities/user.entity.ts | 2 +- 8 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 src/common/decorators/rate-limit.decorator.ts create mode 100755 src/modules/auth/dto/refresh-token.dto.ts diff --git a/src/common/decorators/rate-limit.decorator.ts b/src/common/decorators/rate-limit.decorator.ts new file mode 100644 index 0000000..f21993a --- /dev/null +++ b/src/common/decorators/rate-limit.decorator.ts @@ -0,0 +1,18 @@ +import { UseGuards, applyDecorators } from '@nestjs/common'; +import { Throttle, ThrottlerGuard } from '@nestjs/throttler'; + +import { + AUTH_THROTTLE_LIMIT, + AUTH_THROTTLE_TTL, + AUTH__REFRESH_THROTTLE_LIMIT, + AUTH__REFRESH_THROTTLE_TTL, +} from '../constants'; + +export const RateLimit = (limit: number, ttl: number) => + applyDecorators(Throttle({ default: { limit, ttl } }), UseGuards(ThrottlerGuard)); + +// Predefined rate limits for common scenarios +export const StrictRateLimit = () => RateLimit(AUTH_THROTTLE_LIMIT, AUTH_THROTTLE_TTL); // 5 requests per minute +export const StandardRateLimit = () => RateLimit(30, 60000); // 30 requests per minute +export const MailSendRateLimit = () => RateLimit(10, 60000); // 10 emails per minute +export const RefreshTokenRateLimit = () => RateLimit(AUTH__REFRESH_THROTTLE_LIMIT, AUTH__REFRESH_THROTTLE_TTL); // 10 emails per minute diff --git a/src/modules/admin/entities/admin.entity.ts b/src/modules/admin/entities/admin.entity.ts index 734b07d..975ba25 100644 --- a/src/modules/admin/entities/admin.entity.ts +++ b/src/modules/admin/entities/admin.entity.ts @@ -1,5 +1,5 @@ import { Entity, Property } from '@mikro-orm/core'; -import { BaseEntity } from 'src/common/entities/base.entity'; +import { BaseEntity } from '../../../common/entities/base.entity'; @Entity({ tableName: 'admins' }) export class Admin extends BaseEntity { diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 2880e5f..5a831da 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -4,6 +4,8 @@ import { RequestOtpDto } from './dto/request-otp.dto'; import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; import { VerifyOtpDto } from './dto/verify-otp.dto copy'; import { Throttle } from '@nestjs/throttler'; +import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator'; +import { RefreshTokenDto } from './dto/refresh-token.dto'; @ApiTags('auth') @Controller('auth') @@ -46,4 +48,11 @@ export class AuthController { adminOtpVerify(@Body() dto: VerifyOtpDto) { return this.authService.verifyOtpAdmin(dto.phone, dto.otp); // assuming dto has `otp` property } + + @RefreshTokenRateLimit() + @ApiOperation({ summary: 'refresh the user access token / refresh token' }) + @Post('refresh') + refreshToken(@Body() refreshTokenDto: RefreshTokenDto) { + return this.authService.refreshToken(refreshTokenDto.refreshToken); + } } diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index f864691..92453f7 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -6,6 +6,7 @@ import { UserModule } from '../users/user.module'; import { JwtModule } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import { AdminModule } from '../admin/admin.module'; +import { TokensService } from './services/tokens.service'; @Module({ imports: [ @@ -24,6 +25,6 @@ import { AdminModule } from '../admin/admin.module'; AdminModule, ], controllers: [AuthController], - providers: [AuthService], + providers: [AuthService, TokensService], }) export class AuthModule {} diff --git a/src/modules/auth/dto/refresh-token.dto.ts b/src/modules/auth/dto/refresh-token.dto.ts new file mode 100755 index 0000000..180bbaa --- /dev/null +++ b/src/modules/auth/dto/refresh-token.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export class RefreshTokenDto { + @ApiProperty({ description: 'Refresh token' }) + @IsString({ message: 'Refresh token must be a string' }) + @IsNotEmpty({ message: 'Refresh token is required' }) + refreshToken: string; +} diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 2567bbb..1087057 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -7,6 +7,7 @@ import { randomInt } from 'crypto'; import { JwtService } from '@nestjs/jwt'; import { AdminService } from '../../admin/admin.service'; import { AuthEntityType } from 'src/modules/auth/guards/auth.guard'; +import { TokensService } from './tokens.service'; @Injectable() export class AuthService { @@ -16,6 +17,7 @@ export class AuthService { private readonly userService: UserService, private readonly jwtService: JwtService, private readonly adminService: AdminService, + private readonly tokensService: TokensService, ) {} async requestOtp(dto: RequestOtpDto) { @@ -55,9 +57,10 @@ export class AuthService { const user = await this.userService.findOrCreateByPhone(phone); - const accessToken = await this.issueToken(user.id, AuthEntityType.USER); + // const accessToken = await this.issueToken(user.id, AuthEntityType.USER); + const tokens = await this.tokensService.generateTokens(user); - return { success: true, message: 'User registered successfully!', accessToken }; + return { success: true, message: 'User registered successfully!', tokens }; } async verifyOtpAdmin(phone: string, code: string) { @@ -88,4 +91,8 @@ export class AuthService { }; return this.jwtService.signAsync(payload); } + + refreshToken(oldRefreshToken: string) { + return this.tokensService.refreshToken(oldRefreshToken); + } } diff --git a/src/modules/users/entities/refresh-token.entity.ts b/src/modules/users/entities/refresh-token.entity.ts index 68272ad..3b9b489 100644 --- a/src/modules/users/entities/refresh-token.entity.ts +++ b/src/modules/users/entities/refresh-token.entity.ts @@ -1,8 +1,8 @@ import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core'; import { User } from './user.entity'; -import { BaseEntity } from 'src/common/entities/base.entity'; -@Entity() +import { BaseEntity } from '../../../common/entities/base.entity'; +@Entity({ tableName: 'refreshtokens' }) export class RefreshToken extends BaseEntity { @Property({ type: 'varchar', length: 255 }) token!: string; diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index cacb7ed..84fb9dc 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,6 +1,6 @@ import { Entity, Property, OneToMany, Collection } from '@mikro-orm/core'; import { RefreshToken } from './refresh-token.entity'; -import { BaseEntity } from 'src/common/entities/base.entity'; +import { BaseEntity } from '../../../common/entities/base.entity'; @Entity({ tableName: 'users' }) export class User extends BaseEntity {