Files
esg/src/auth/auth.service.ts
s.hajizadeh 45ef396466 feat: add Persian error translations and comprehensive test suite
Implement normalized error handling with Persian translations across all
exception types, replace legacy NestJS exceptions with AppException,
and add unit, integration, and smoke tests.

- Add error catalog with gateway-owned codes and Persian messages
- Introduce AppException wrapping normalized error envelopes
- Add translateError helper for automatic messageFa population
- Remove claims module and update provider error normalization
- Add unit tests for error contracts and helper functions
- Add integration tests for admin and inquiry endpoints
- Add smoke tests for real provider connectivity
- Add test support utilities (auth mocks, assertions, app factory)
2026-06-29 17:25:19 +03:30

156 lines
4.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { AuditEvent } from '../common/enums/audit-event.enum';
import { AuditService } from '../audit/audit.service';
import { UsersService, RequestContext } from '../users/users.service';
import { PasswordService } from './services/password.service';
import { TokenService } from './services/token.service';
import { LoginDto } from './dto/login.dto';
import { AuthTokensDto } from './dto/auth-tokens.dto';
import { ProfileDto } from './dto/profile.dto';
import { UserMapper } from '../users/mappers/user.mapper';
import { JwtPayload } from './interfaces/jwt-payload.interface';
import { AuthenticatedUser } from './interfaces/authenticated-user.interface';
import { AppException } from '../common/exceptions/app-exception';
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly passwordService: PasswordService,
private readonly tokenService: TokenService,
private readonly auditService: AuditService,
) {}
async login(dto: LoginDto, context: RequestContext = {}): Promise<AuthTokensDto> {
const user = await this.usersService.findByUsername(dto.username, true);
if (!user) {
await this.auditService.log(AuditEvent.LOGIN_FAILED, {
username: dto.username,
ip: context.ip,
userAgent: context.userAgent,
metadata: { reason: 'invalid_credentials' },
});
throw new AppException('INVALID_CREDENTIALS');
}
if (!user.isActive) {
await this.auditFailedLogin(user._id.toString(), user.username, 'inactive', context);
throw new AppException('ACCOUNT_INACTIVE');
}
if (user.isBlocked) {
await this.auditFailedLogin(user._id.toString(), user.username, 'blocked', context);
throw new AppException('ACCOUNT_BLOCKED');
}
const valid = await this.passwordService.compare(dto.password, user.password);
if (!valid) {
await this.auditFailedLogin(user._id.toString(), user.username, 'invalid_credentials', context);
throw new AppException('INVALID_CREDENTIALS');
}
const tokens = await this.issueTokens(user);
await this.usersService.saveRefreshToken(user._id.toString(), tokens.refreshToken);
await this.usersService.recordLogin(user._id.toString());
await this.auditService.log(AuditEvent.LOGIN, {
userId: user._id.toString(),
username: user.username,
ip: context.ip,
userAgent: context.userAgent,
});
return tokens;
}
async refresh(refreshToken: string, context: RequestContext = {}): Promise<AuthTokensDto> {
let payload: { sub: string };
try {
payload = this.tokenService.verifyRefreshToken(refreshToken);
} catch {
throw new AppException('INVALID_REFRESH_TOKEN');
}
const valid = await this.usersService.validateRefreshToken(payload.sub, refreshToken);
if (!valid) {
throw new AppException('INVALID_REFRESH_TOKEN');
}
const user = await this.usersService.findById(payload.sub);
if (!user.isActive) {
throw new AppException('ACCOUNT_INACTIVE');
}
if (user.isBlocked) {
throw new AppException('ACCOUNT_BLOCKED');
}
const tokens = await this.issueTokens(user);
await this.usersService.saveRefreshToken(user._id.toString(), tokens.refreshToken);
await this.auditService.log(AuditEvent.LOGIN, {
userId: user._id.toString(),
username: user.username,
ip: context.ip,
userAgent: context.userAgent,
metadata: { type: 'refresh' },
});
return tokens;
}
async logout(userId: string, context: RequestContext = {}): Promise<void> {
await this.usersService.clearRefreshToken(userId);
const user = await this.usersService.findById(userId);
await this.auditService.log(AuditEvent.LOGOUT, {
userId,
username: user.username,
ip: context.ip,
userAgent: context.userAgent,
});
}
async getProfile(userId: string): Promise<ProfileDto> {
const user = await this.usersService.findById(userId);
return UserMapper.toProfileDto(user);
}
private async issueTokens(user: {
_id: { toString(): string };
role: JwtPayload['role'];
clientType: JwtPayload['clientType'];
appName: string;
}): Promise<AuthTokensDto> {
const payload: JwtPayload = {
sub: user._id.toString(),
role: user.role,
clientType: user.clientType,
appName: user.appName,
};
const pair = this.tokenService.generateTokenPair(payload);
return {
accessToken: pair.accessToken,
refreshToken: pair.refreshToken,
tokenType: 'Bearer',
expiresIn: 900,
};
}
private async auditFailedLogin(
userId: string,
username: string,
reason: string,
context: RequestContext,
): Promise<void> {
await this.auditService.log(AuditEvent.LOGIN_FAILED, {
userId,
username,
ip: context.ip,
userAgent: context.userAgent,
metadata: { reason },
});
}
}