feat: added auth guard and some decorators

This commit is contained in:
2026-07-26 16:13:54 +03:30
parent 45464c070f
commit efd468873d
15 changed files with 199 additions and 35 deletions
+2
View File
@@ -25,6 +25,7 @@
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.1.28",
"@nestjs/jwt": "^11.0.2",
"@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.28",
"@nestjs/swagger": "^11.4.6",
"@nestjs/typeorm": "^11.0.3",
@@ -34,6 +35,7 @@
"class-validator": "^0.15.1",
"joi": "^18.2.3",
"ms": "^2.1.3",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"pg": "^8.22.0",
"reflect-metadata": "^0.2.2",
+38
View File
@@ -23,6 +23,9 @@ importers:
'@nestjs/jwt':
specifier: ^11.0.2
version: 11.0.2(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))
'@nestjs/passport':
specifier: ^11.0.5
version: 11.0.5(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
'@nestjs/platform-express':
specifier: ^11.1.28
version: 11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)
@@ -50,6 +53,9 @@ importers:
ms:
specifier: ^2.1.3
version: 2.1.3
passport:
specifier: ^0.7.0
version: 0.7.0
passport-jwt:
specifier: ^4.0.1
version: 4.0.1
@@ -788,6 +794,12 @@ packages:
class-validator:
optional: true
'@nestjs/passport@11.0.5':
resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==}
peerDependencies:
'@nestjs/common': ^10.0.0 || ^11.0.0
passport: ^0.5.0 || ^0.6.0 || ^0.7.0
'@nestjs/platform-express@11.1.28':
resolution: {integrity: sha512-hU+9Sz4m+onHrR5AmelI59QKmY/Re546bPnygnpqqeQdHDiJpBgjWbL4t6Jr73CBpS60cpyng7WzjgphNB9iwA==}
peerDependencies:
@@ -2650,6 +2662,10 @@ packages:
resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==}
engines: {node: '>= 0.4.0'}
passport@0.7.0:
resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==}
engines: {node: '>= 0.4.0'}
path-exists@4.0.0:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
@@ -2677,6 +2693,9 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
pause@0.0.1:
resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==}
pg-cloudflare@1.4.0:
resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==}
@@ -3303,6 +3322,10 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
v8-compile-cache-lib@3.0.1:
resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
@@ -4237,6 +4260,11 @@ snapshots:
class-transformer: 0.5.1
class-validator: 0.15.1
'@nestjs/passport@11.0.5(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)':
dependencies:
'@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
passport: 0.7.0
'@nestjs/platform-express@11.1.28(@nestjs/common@11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.28)':
dependencies:
'@nestjs/common': 11.1.28(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -6306,6 +6334,12 @@ snapshots:
passport-strategy@1.0.0: {}
passport@0.7.0:
dependencies:
passport-strategy: 1.0.0
pause: 0.0.1
utils-merge: 1.0.1
path-exists@4.0.0: {}
path-is-absolute@1.0.1: {}
@@ -6326,6 +6360,8 @@ snapshots:
path-type@4.0.0: {}
pause@0.0.1: {}
pg-cloudflare@1.4.0:
optional: true
@@ -6893,6 +6929,8 @@ snapshots:
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}
v8-compile-cache-lib@3.0.1: {}
v8-to-istanbul@9.3.0:
+31 -32
View File
@@ -1,58 +1,57 @@
import { Body, Controller, Post } from "@nestjs/common";
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
import { AuthService } from "./auth.service";
import { SendOtpDto } from "./DTO/send-otp.dto";
import { LoginCredentialDto } from "./DTO/login-credential.dto";
import { VerifyOtpDto } from "./DTO/verify-otp.dto";
import { Body, Controller, Get, Post, Req, UseGuards } from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { SendOtpDto } from './DTO/send-otp.dto';
import { LoginCredentialDto } from './DTO/login-credential.dto';
import { VerifyOtpDto } from './DTO/verify-otp.dto';
import { AuthGuard } from 'src/common/decorators/auth-guard.decorator';
import type { IRequest } from './interfaces/IRequest';
import { UserDec } from 'src/common/decorators/user.decorator';
@ApiTags('Authentication')
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
) {}
constructor(private readonly authService: AuthService) {}
@Post('send-otp')
@ApiOperation({
summary: 'Send OTP to a phone number',
})
@ApiResponse({status: 200, description: 'Sent Otp Successfully!'})
@ApiResponse({status: 400, description: 'Bad Request from User!'})
async sendOtp(
@Body() dto: SendOtpDto
) {
@ApiResponse({ status: 200, description: 'Sent Otp Successfully!' })
@ApiResponse({ status: 400, description: 'Bad Request from User!' })
async sendOtp(@Body() dto: SendOtpDto) {
return this.authService.sendOTP(dto.phoneNumber);
}
@Post('verify-otp')
@ApiOperation({
summary: 'Verify OTP.'
summary: 'Verify OTP.',
})
@ApiResponse({status: 200, description: 'Verified Otp Successfully!'})
@ApiResponse({status: 400, description: 'Bad Request from the user'})
async verifyOtp(
@Body() dto: VerifyOtpDto
) {
@ApiResponse({ status: 200, description: 'Verified Otp Successfully!' })
@ApiResponse({ status: 400, description: 'Bad Request from the user' })
async verifyOtp(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOTP(dto);
}
@Post('check-credential')
@ApiOperation({summary: 'Check Login Credentials. If Valid Sends OTP'})
@ApiResponse({status: 200, description: 'Valid Credentials!'})
@ApiResponse({status: 400, description: 'Credentials Invalid!'})
async checkLoginCredential(
@Body() dto: LoginCredentialDto
) {
@ApiOperation({ summary: 'Check Login Credentials. If Valid Sends OTP' })
@ApiResponse({ status: 200, description: 'Valid Credentials!' })
@ApiResponse({ status: 400, description: 'Credentials Invalid!' })
async checkLoginCredential(@Body() dto: LoginCredentialDto) {
return this.authService.checkLoginCredentials(dto);
}
@Post('verify-login')
@ApiOperation({summary: 'verifies OTP and then login'})
@ApiResponse({status: 200, description: 'User Logged in successfully!'})
@ApiResponse({status: 400, description: 'Bad Request From The User'})
async verifyOtpAndLogin(
@Body() verifyOtpDto: VerifyOtpDto
) {
@ApiOperation({ summary: 'verifies OTP and then login' })
@ApiResponse({ status: 200, description: 'User Logged in successfully!' })
@ApiResponse({ status: 400, description: 'Bad Request From The User' })
async verifyOtpAndLogin(@Body() verifyOtpDto: VerifyOtpDto) {
return this.authService.verifyOtpAndLogin(verifyOtpDto);
}
@AuthGuard()
@Get('profile')
profile(@Req() req: IRequest, @UserDec('id') userId: string) {
return req.user;
}
}
+5 -1
View File
@@ -10,15 +10,19 @@ import { OtpRepository } from './repositories/otp.repository';
import { UsersModule } from 'src/users/users.module';
import { ConfigService } from '@nestjs/config';
import { UserRole } from 'src/users/entities/user-role.entity';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
TypeOrmModule.forFeature([OTP]),
PassportModule,
JwtModule.register({}),
UtilsModule,
UsersModule,
],
controllers: [AuthController],
providers: [AuthService, OtpRepository],
providers: [AuthService, OtpRepository, JwtStrategy],
exports: [JwtModule, PassportModule]
})
export class AuthModule {}
+1 -1
View File
@@ -133,7 +133,7 @@ export class AuthService {
const roles = user.roles.map((r) => r.role);
const payload = {
sub: user.id,
id: user.id,
phone: user.phone,
roles,
};
+33
View File
@@ -0,0 +1,33 @@
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { SKIP_AUTH_KEY } from 'src/common/decorators/skip-auth.decorator';
import { AuthMessages } from 'src/common/enums/messages.enum';
import { JWT_STRATEGY_NAME } from 'src/constants';
@Injectable()
export class JwtAuthGuard extends AuthGuard(JWT_STRATEGY_NAME) {
constructor(private reflector: Reflector) {
super();
}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [
context.getHandler(),
context.getClass(),
]);
if (skipAuth) return true;
return super.canActivate(context);
}
handleRequest(err: unknown, user: any, _info: unknown) {
if (err || !user)
throw new UnauthorizedException(AuthMessages.TOKEN_EXPIRED_OR_INVALID);
return user;
}
}
+7
View File
@@ -0,0 +1,7 @@
import { ITokenPayload } from "./IToken-payload";
import { IUserIpAndHeaders } from "./IUserIpAndHeader";
export interface IRequest {
user?: ITokenPayload,
ip_header?: IUserIpAndHeaders
}
+7
View File
@@ -0,0 +1,7 @@
import { RoleType } from "src/users/enums/user-role.enum";
export interface ITokenPayload {
id: string;
phone: string;
roles: RoleType[];
}
+6
View File
@@ -0,0 +1,6 @@
export interface IUserIpAndHeaders {
ip: string;
headers: Record<string, string>;
userId: string;
}
+26
View File
@@ -0,0 +1,26 @@
import { Injectable } from '@nestjs/common';
import { RoleType } from 'src/users/enums/user-role.enum';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, ExtractJwt } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { JWT_STRATEGY_NAME } from 'src/constants';
import { ITokenPayload } from '../interfaces/IToken-payload';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY_NAME) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>('JWT_ACCESS_SECRET'),
});
}
async validate(payload: ITokenPayload) {
return {
id: payload.id,
phone: payload.phone,
roles: payload.roles
};
}
}
@@ -0,0 +1,10 @@
import { applyDecorators, UseGuards } from "@nestjs/common";
import { ApiBearerAuth } from "@nestjs/swagger";
import { JwtAuthGuard } from "src/auth/guards/jwt-auth.guard";
export function AuthGuard() {
return applyDecorators(
UseGuards(JwtAuthGuard),
ApiBearerAuth('authorization')
);
}
@@ -0,0 +1,5 @@
import { SetMetadata } from "@nestjs/common";
export const SKIP_AUTH_KEY = "skipAuth";
export const skipAuth = () => SetMetadata(SKIP_AUTH_KEY, true);
+22
View File
@@ -0,0 +1,22 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { IRequest } from 'src/auth/interfaces/IRequest';
import { ITokenPayload } from 'src/auth/interfaces/IToken-payload';
export const UserDec = createParamDecorator(
(data: keyof ITokenPayload | undefined, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<IRequest>();
const user = req.user;
return data ? user?.[data] : user;
},
);
export const UserIpAndHeaders = createParamDecorator(
(_data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest<IRequest>();
const user = req.user;
const ip_header = req.ip_header;
return { ip: ip_header?.ip, headers: ip_header?.headers, userId: user?.id };
},
);
+4
View File
@@ -15,3 +15,7 @@ export enum OTPMessages {
export enum UserRoleMessages {
USER_ROLE_ALREADY_EXISTS = 'نقش مورد نظر برای کاربر مد نظر وجود دارد!'
}
export enum AuthMessages {
TOKEN_EXPIRED_OR_INVALID = 'توکن منسوخ شده یا نامعتبر است!',
}
+1
View File
@@ -0,0 +1 @@
export const JWT_STRATEGY_NAME = "jwt_Strategy";