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 { 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 { 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 { 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 { 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 { 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 { await this.auditService.log(AuditEvent.LOGIN_FAILED, { userId, username, ip: context.ip, userAgent: context.userAgent, metadata: { reason }, }); } }