forked from Shared/esg
initial commit
This commit is contained in:
158
src/auth/auth.service.ts
Normal file
158
src/auth/auth.service.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} 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';
|
||||
|
||||
@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 UnauthorizedException('Invalid credentials');
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
await this.auditFailedLogin(user._id.toString(), user.username, 'inactive', context);
|
||||
throw new ForbiddenException('Account is inactive');
|
||||
}
|
||||
|
||||
if (user.isBlocked) {
|
||||
await this.auditFailedLogin(user._id.toString(), user.username, 'blocked', context);
|
||||
throw new ForbiddenException('Account is 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 UnauthorizedException('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 UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const valid = await this.usersService.validateRefreshToken(payload.sub, refreshToken);
|
||||
if (!valid) {
|
||||
throw new UnauthorizedException('Invalid refresh token');
|
||||
}
|
||||
|
||||
const user = await this.usersService.findById(payload.sub);
|
||||
if (!user.isActive) {
|
||||
throw new ForbiddenException('Account is inactive');
|
||||
}
|
||||
if (user.isBlocked) {
|
||||
throw new ForbiddenException('Account is 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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user