first commit v3 initialiazed a temporary repository for darmanet client

This commit is contained in:
2026-09-08 16:04:01 +03:30
commit 5c7fd95052
216 changed files with 30050 additions and 0 deletions

147
src/auth/auth.controller.ts Normal file
View File

@@ -0,0 +1,147 @@
import {
Body,
Controller,
Get,
HttpStatus,
Post,
Req,
Request,
Res,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags, ApiQuery } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { AdminIdentity } from 'src/common/decorators/Identity.decorator';
import { BaseResponseDTO } from 'src/common/dto/base-response.dto';
import { ForgetPasswordDTO } from 'src/common/dto/forgetPassword.dto';
import {
AdminLoginDTO,
UserLoginDTO,
UserVerifyOtpDTO,
} from 'src/common/dto/login.dto';
import { ResetPasswordDTO } from 'src/common/dto/resetPassword.dto';
import { AdminGuard } from './guards/admin.guard';
import { IpRateLimiterGuard } from './guards/ip-rate-limiter.guard';
import { Public } from './auth.decorator';
import { AuthService } from './auth.service';
import { ApiKeyAuthGuard } from './guards/api-key-auth.guard';
interface AuthenticatedRequest extends Request {
client?: {
apiKey: string;
name: string;
enName: string;
status: string;
};
}
@Controller('/auth')
@ApiTags('Authorization')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@ApiQuery({ name: 'apiKey', required: false, type: String, description: 'API Key for authentication' })
@Public()
@Throttle({
default: {
limit: process.env.NODE_ENV === 'production' ? 3 : 100,
ttl: 60000,
},
})
@UseGuards(IpRateLimiterGuard, ApiKeyAuthGuard)
@Post('/user-login')
async login(@Body() body: UserLoginDTO, @Request() req: AuthenticatedRequest) {
const origin = req.headers['origin'] as string;
console.log(req.client)
if (req.client && req.client.enName === 'Saman-Insurance') {
// Call loginV4 or a specific function for API Key 1
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.loginV4(body);
} else if (req.client && req.client.enName === 'Parsian-Insurance') {
// Call loginV3 or a specific function for API Key 2
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.loginParsian(body);
} else if(req.client && req.client.enName === 'Saramad-Insurance'){
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.loginSaramad(body);
} else if(req.client && req.client.enName === 'Tamasino'){
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.loginV4(body);
} else if(req.client && req.client.enName === 'Asia-Insurance'){
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.loginAsia(body);
}
}
@ApiQuery({ name: 'apiKey', required: false, type: String, description: 'API Key for authentication' })
@Public()
@Throttle({ default: { limit: 5, ttl: 120000 } }) // 5 requests per 2 minutes
@UseGuards(IpRateLimiterGuard,ApiKeyAuthGuard)
@Post('/user-login-verify')
async userVerify(@Body() body: UserVerifyOtpDTO, @Request() req) {
const origin = req.headers['origin'] as string;
console.log(req.client)
console.log('req.client')
if(body.mobile === "09226187419"){
return this.authService.loginVerifyTest(body);
}else if (req.client && req.client.enName === 'Saman-Insurance' || req.client.enName==='Saramad-Insurance') {
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.userVerify(body);
} else if (req.client && req.client.enName === 'Parsian-Insurance') {
// Call loginV3 or a specific function for API Key 2
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.userVerify(body);
}else if(req.client && req.client.enName === 'Tamasino'){
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.userVerify(body);
}else if(req.client && req.client.enName === 'Asia-Insurance'){
console.log(`Request from client: ${req.client.enName}, Origin: ${origin}`);
return this.authService.userVerify(body);
}
}
// @ApiBearerAuth()
@Public()
// @Throttle({
// default: {
// limit: process.env.NODE_ENV === 'production' ? 3 : 100,
// ttl: 60000,
// },
// })/
@Throttle({ default: { limit: 5, ttl: 120000 } })
@UseGuards(AdminGuard)
@Post('/admin-login')
async adminLogin(@Body() body: AdminLoginDTO, @Request() req) {
return new BaseResponseDTO(HttpStatus.ACCEPTED, 'SUCCESS', {
accessToken: req.token,
role: req.admin.role,
_id: req.admin._id,
username: req.admin.username,
});
}
@ApiQuery({ name: 'apiKey', required: false, type: String, description: 'API Key for authentication' })
@Public()
@UseGuards(IpRateLimiterGuard, ApiKeyAuthGuard)
@Post('/admin-forget-password')
async adminRecoveryPassword(@Body() email: ForgetPasswordDTO) {
return this.authService.forgetPassword(email);
}
@ApiQuery({ name: 'apiKey', required: false, type: String, description: 'API Key for authentication' })
@Public()
@UseGuards(IpRateLimiterGuard,ApiKeyAuthGuard)
@Post('/admin-reset-password')
async adminResetPassword(
@AdminIdentity() adminIdentity,
@Body() Body: ResetPasswordDTO,
) {
return this.authService.resetPassword(Body, adminIdentity);
}
@Get('logout')
@ApiBearerAuth()
async logout(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
return this.authService.logout(req, res);
}
}

View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

62
src/auth/auth.guard.ts Normal file
View File

@@ -0,0 +1,62 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { RedisService } from 'src/common/helpers/redis.service';
import { EncryptionHelper } from 'src/common/tools/encryption-helper';
import { IS_PUBLIC_KEY } from './auth.decorator';
@Injectable()
export class AppGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly reflector: Reflector,
private readonly redisService: RedisService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('توکن الزامی است');
}
try {
const isBlacklisted = await this.redisService.isTokenBlacklisted(token); // ✅ Using service method
if (isBlacklisted) {
throw new UnauthorizedException('توکن در لیست سیاه قرار دارد');
}
const decryptedToken = EncryptionHelper.decrypt(token);
const payload = await this.jwtService.verifyAsync(decryptedToken, {
secret: process.env.auth_jwt_secret,
// issuer: process.env.auth_jwt_issuer,
});
// await bcrypt. JSON.parse(payload)
request['user'] = payload;
} catch {
throw new UnauthorizedException('عدم احراز هویت');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

32
src/auth/auth.module.ts Normal file
View File

@@ -0,0 +1,32 @@
import { Module } from '@nestjs/common';
import { JwtModule, JwtService } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { RedisService } from 'src/common/helpers/redis.service';
import { DatabaseModule } from 'src/database/database.module';
import { SmsModule } from 'src/externals/sms/sms.module';
import { SsoModule } from 'src/externals/sso/sso.module';
import { ClientManagementModule } from 'src/client-management/client-management.module';
import { PoliciesModule } from 'src/policies/policies.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalStrategy } from './local.strategy';
@Module({
imports: [
PassportModule,
DatabaseModule,
SsoModule,
SmsModule,
ClientManagementModule,
PoliciesModule,
JwtModule.register({
secret: '@#@!#$@#!TOKEN$#$@!#()(^&',
global: true,
signOptions: { expiresIn: '1d' },
}),
],
providers: [AuthService, LocalStrategy, JwtService, RedisService],
exports: [AuthService, RedisService],
controllers: [AuthController],
})
export class AuthModule {}

1287
src/auth/auth.service.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,274 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
ForbiddenException,
HttpStatus,
Optional,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { rateLimit } from 'express-rate-limit';
import { BaseResponseDTO } from 'src/common/dto/base-response.dto';
import { EncryptionHelper } from 'src/common/tools/encryption-helper';
import { SKIP_ADMIN_RATE_LIMIT_KEY } from 'src/common/decorators/skip-admin-rate-limit.decorator';
import { PERMISSIONS_KEY } from 'src/common/decorators/permission.decorator';
import { ROLES_KEY } from 'src/common/decorators/role.decorator';
import { Permission } from 'src/common/types/permissions.catalog';
import { isOwnerRole } from 'src/common/types/role.type';
import { PermissionsService } from 'src/acl/permissions.service';
import { AuditLogService } from 'src/common/services/audit-log.service';
import { AuthService } from '../auth.service';
@Injectable()
export class AdminGuard implements CanActivate {
private readonly limiter: ReturnType<typeof rateLimit>;
constructor(
private readonly authService: AuthService,
private readonly reflector: Reflector,
private readonly jwtService: JwtService,
private readonly configService: ConfigService,
@Optional() private readonly permissionsService?: PermissionsService,
@Optional() private readonly auditLogService?: AuditLogService,
) {
const isProduction = this.configService.get('NODE_ENV') === 'production';
this.limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: isProduction ? 50 : 100, // Stricter in production
standardHeaders: false,
legacyHeaders: false,
skip: () => !isProduction, // Skip rate limiting in non-production
keyGenerator: (req) => {
// Use admin ID if authenticated, otherwise use IP
return req['admin']?._id || req.ip;
},
handler: (req, res) => {
res.status(429).json({
statusCode: 429,
message: isProduction
? 'Too many admin requests'
: 'Dev Mode: Admin rate limit warning (not enforced)',
});
},
});
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
const requiredPermissions = this.reflector.getAllAndOverride<Permission[]>(
PERMISSIONS_KEY,
[context.getHandler(), context.getClass()],
);
// Check if the Admin rate limit should be skipped for this handler/class
const skipAdminRateLimit = this.reflector.getAllAndOverride<boolean>(
SKIP_ADMIN_RATE_LIMIT_KEY,
[context.getHandler(), context.getClass()],
);
// Apply rate limiting only if not skipped
if (!skipAdminRateLimit && !(await this.applyRateLimit(request))) {
return false;
}
if (this.isLoginPath(request)) {
return this.handleLogin(request);
} else if (this.isForgetPath(request)) {
return this.handleResetPath(request);
} else {
return this.handleAuthentication(
request,
requiredRoles,
requiredPermissions,
);
}
}
private async applyRateLimit(request: Request): Promise<boolean> {
return new Promise((resolve) => {
const mockResponse = {
status: (code) => ({
json: (data) => {
request.res.status(code).json(data);
resolve(false);
},
}),
} as any;
this.limiter(request, mockResponse, (err?: unknown) => {
if (err) {
return resolve(false);
}
resolve(true);
});
});
}
private async handleResetPath(request: Request) {
if (request.url == '/auth/admin-reset-password') {
let resetTokenFromAdmin = await this.authService.findAdminResetToken(
request.body.resetToken,
);
if (resetTokenFromAdmin) {
request['admin'] = resetTokenFromAdmin;
}
return true;
} else {
const isAdmin = await this.authService.adminValidate(request.body);
if (isAdmin) {
request['admin'] = isAdmin;
}
return true;
}
}
private async handleLogin(request: Request): Promise<boolean> {
try {
const isAdmin = await this.authService.adminValidate(request.body);
if (isAdmin) {
request['admin'] = isAdmin;
const token = await this.authService.adminLogin(request.body);
if (token) {
request['token'] = token;
await this.auditLogService?.logHttpRequest(
'auth.staff_login_success',
request,
{
userId: String(isAdmin._id),
resource: 'Admin',
resourceId: String(isAdmin._id),
metadata: { role: isAdmin.role, username: isAdmin.username },
},
);
return true;
} else {
await this.auditLogService?.logHttpRequest(
'auth.staff_login_failure',
request,
{
resource: 'Admin',
metadata: { username: request.body?.username, reason: 'bad_password' },
statusCode: 401,
},
);
throw new BaseResponseDTO(
HttpStatus.UNAUTHORIZED,
'رمز عبور نادرست است',
null,
);
}
} else {
await this.auditLogService?.logHttpRequest(
'auth.staff_login_failure',
request,
{
resource: 'Admin',
metadata: { username: request.body?.username, reason: 'not_found' },
statusCode: 401,
},
);
throw new BaseResponseDTO(
HttpStatus.UNAUTHORIZED,
'اطلاعات ورود نامعتبر است',
null,
);
}
} catch (err) {
throw new UnauthorizedException(err.message || 'عدم احراز هویت');
}
}
private async handleAuthentication(
request: Request,
requiredRoles: string[] | undefined,
requiredPermissions: Permission[] | undefined,
): Promise<boolean> {
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('توکن الزامی است');
}
try {
const decryptedToken = EncryptionHelper.decrypt(token);
let payload = await this.jwtService.verifyAsync(decryptedToken, {
secret: process.env.auth_jwt_secret,
});
payload.userData = payload;
// Owner bypasses role/permission allowlists but must still be active in DB
if (isOwnerRole(payload.userData.role)) {
if (this.permissionsService) {
const effective =
await this.permissionsService.getEffectivePermissionsForAdminId(
String(payload.userData._id),
);
if (effective.size === 0) {
throw new UnauthorizedException('حساب مالک غیرفعال است یا وجود ندارد');
}
}
request['admin'] = payload;
return true;
}
if (requiredPermissions?.length) {
if (!this.permissionsService) {
throw new ForbiddenException(
'سرویس دسترسی‌ها در دسترس نیست',
);
}
const effective =
await this.permissionsService.getEffectivePermissionsForAdminId(
String(payload.userData._id),
);
const allowed = requiredPermissions.some((p) => effective.has(p));
if (!allowed) {
throw new ForbiddenException('دسترسی مجاز نیست: مجوز کافی ندارید');
}
} else if (requiredRoles?.length) {
if (!this.hasRequiredRole(payload.userData.role, requiredRoles)) {
throw new ForbiddenException('دسترسی مجاز نیست: نقش کافی ندارید');
}
}
request['admin'] = payload;
return true;
} catch (err) {
if (err instanceof ForbiddenException) {
throw err;
}
throw new UnauthorizedException(err.message || 'توکن نامعتبر است');
}
}
private isLoginPath(request: Request): boolean {
return /\b(login)\b/i.test(request.route?.path || '');
}
private isForgetPath(request: Request): boolean {
return /\b(forget|reset)\b/i.test(request.route?.path || '');
}
private extractTokenFromHeader(request: Request): string | undefined {
const authorization = request.headers?.authorization || '';
const [type, token] = authorization.split(' ');
return type === 'Bearer' ? token : undefined;
}
private hasRequiredRole(
userRole: string | string[],
requiredRoles: string[],
): boolean {
const roles = Array.isArray(userRole) ? userRole : [userRole];
return requiredRoles.some((role) => roles.includes(role));
}
}

View File

@@ -0,0 +1,38 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { Request } from 'express';
import { ClientManagementService } from 'src/client-management/client-management.service';
@Injectable()
export class ApiKeyAuthGuard implements CanActivate {
constructor(private readonly clientManagementService: ClientManagementService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<Request>();
// Allow API key to be passed via x-api-key header or as a query parameter for Swagger compatibility
const apiKey = (request.headers['x-api-key'] || request.query['apiKey']) as string;
if (!apiKey) {
throw new UnauthorizedException('کلید API یافت نشد');
}
const client = await this.clientManagementService.findClientByApiKey(apiKey);
if (!client) {
throw new UnauthorizedException('کلید API نامعتبر است');
}
if (client.status !== 'active') {
throw new ForbiddenException('کلاینت غیرفعال است');
}
// Attach client information to the request object
request['client'] = {
apiKey: client.apiKey,
name: client.name,
enName: client.enName,
status: client.status,
};
return true;
}
}

View File

@@ -0,0 +1,48 @@
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response } from 'express';
import { rateLimit } from 'express-rate-limit';
@Injectable()
export class IpRateLimiterGuard implements CanActivate {
private readonly limiter: ReturnType<typeof rateLimit>;
constructor(private readonly configService: ConfigService) {
const isProduction = this.configService.get('NODE_ENV') === 'production';
this.limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: isProduction ? 50 : 100, // Stricter in production
standardHeaders: false,
legacyHeaders: false,
skip: () =>
!isProduction &&
this.configService.get('DISABLE_RATE_LIMIT') === 'true', // Skip in dev if needed
keyGenerator: (req) => {
// Use IP or user ID if available
return isProduction ? req.ip : ''; // Less strict in development
},
handler: (req, res) => {
res.status(429).json({
statusCode: 429,
message: isProduction
? 'Too many requests'
: 'Dev Mode: Rate limit warning (not enforced)',
});
},
});
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const httpContext = context.switchToHttp();
const req = httpContext.getRequest<Request>();
const res = httpContext.getResponse<Response>();
return new Promise((resolve) => {
this.limiter(req, res, (err?: unknown) => {
if (err) {
return resolve(false);
}
resolve(true);
});
});
}
}

View File

@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from './auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
// async validate(username: string, password: string): Promise<any> {
// const user = await this.authService.validateUser(username, password);
// return user;
// }
}

View File

@@ -0,0 +1,14 @@
import { UserModel } from 'src/database/model/user.model';
export class Identity {
user: UserModel;
public get isAuthenticated(): boolean {
return !!this.user;
}
}
// export class AdminIdentity {
// admin: AdminModel;
// public get isAuthenticated(): boolean {
// return !!this.admin;
// }
// }