feat: implement AuthModule with authentication and RBAC features

Added AuthModule with services, controllers, and guards for authentication. Implements session management, role-based access control, 2FA, and DTOs for user login, registration, and token refresh.
This commit is contained in:
Mathis HERRIOT
2026-01-08 15:24:40 +01:00
parent 9406ed9350
commit 42805e371e
12 changed files with 509 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
import {
BadRequestException,
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { authenticator } from "otplib";
import { toDataURL } from "qrcode";
import { CryptoService } from "../crypto/crypto.service";
import { SessionsService } from "../sessions/sessions.service";
import { UsersService } from "../users/users.service";
import { LoginDto } from "./dto/login.dto";
import { RegisterDto } from "./dto/register.dto";
@Injectable()
export class AuthService {
constructor(
private readonly usersService: UsersService,
private readonly cryptoService: CryptoService,
private readonly sessionsService: SessionsService,
private readonly configService: ConfigService,
) {}
async generateTwoFactorSecret(userId: string) {
const user = await this.usersService.findOne(userId);
if (!user) throw new UnauthorizedException();
const secret = authenticator.generateSecret();
const otpauthUrl = authenticator.keyuri(
user.username,
this.configService.get("DOMAIN_NAME") || "Memegoat",
secret,
);
await this.usersService.setTwoFactorSecret(userId, secret);
const qrCodeDataUrl = await toDataURL(otpauthUrl);
return {
secret,
qrCodeDataUrl,
};
}
async enableTwoFactor(userId: string, token: string) {
const secret = await this.usersService.getTwoFactorSecret(userId);
if (!secret) {
throw new BadRequestException("2FA not initiated");
}
const isValid = authenticator.verify({ token, secret });
if (!isValid) {
throw new BadRequestException("Invalid 2FA token");
}
await this.usersService.toggleTwoFactor(userId, true);
return { message: "2FA enabled successfully" };
}
async disableTwoFactor(userId: string, token: string) {
const secret = await this.usersService.getTwoFactorSecret(userId);
if (!secret) {
throw new BadRequestException("2FA not enabled");
}
const isValid = authenticator.verify({ token, secret });
if (!isValid) {
throw new BadRequestException("Invalid 2FA token");
}
await this.usersService.toggleTwoFactor(userId, false);
return { message: "2FA disabled successfully" };
}
async register(dto: RegisterDto) {
const { username, email, password } = dto;
const passwordHash = await this.cryptoService.hashPassword(password);
const emailHash = await this.cryptoService.hashEmail(email);
const user = await this.usersService.create({
username,
email,
passwordHash,
emailHash,
});
return {
message: "User registered successfully",
userId: user.uuid,
};
}
async login(dto: LoginDto, userAgent?: string, ip?: string) {
const { email, password } = dto;
const emailHash = await this.cryptoService.hashEmail(email);
const user = await this.usersService.findByEmailHash(emailHash);
if (!user) {
throw new UnauthorizedException("Invalid credentials");
}
const isPasswordValid = await this.cryptoService.verifyPassword(
password,
user.passwordHash,
);
if (!isPasswordValid) {
throw new UnauthorizedException("Invalid credentials");
}
if (user.isTwoFactorEnabled) {
return {
message: "2FA required",
requires2FA: true,
userId: user.uuid,
};
}
const accessToken = await this.cryptoService.generateJwt({
sub: user.uuid,
username: user.username,
});
const session = await this.sessionsService.createSession(
user.uuid,
userAgent,
ip,
);
return {
message: "User logged in successfully",
access_token: accessToken,
refresh_token: session.refreshToken,
};
}
async verifyTwoFactorLogin(
userId: string,
token: string,
userAgent?: string,
ip?: string,
) {
const user = await this.usersService.findOneWithPrivateData(userId);
if (!user || !user.isTwoFactorEnabled) {
throw new UnauthorizedException();
}
const secret = await this.usersService.getTwoFactorSecret(userId);
if (!secret) throw new UnauthorizedException();
const isValid = authenticator.verify({ token, secret });
if (!isValid) {
throw new UnauthorizedException("Invalid 2FA token");
}
const accessToken = await this.cryptoService.generateJwt({
sub: user.uuid,
username: user.username,
});
const session = await this.sessionsService.createSession(
user.uuid,
userAgent,
ip,
);
return {
message: "User logged in successfully (2FA)",
access_token: accessToken,
refresh_token: session.refreshToken,
};
}
async refresh(refreshToken: string) {
const session = await this.sessionsService.refreshSession(refreshToken);
const user = await this.usersService.findOne(session.userId);
if (!user) {
throw new UnauthorizedException("User not found");
}
const accessToken = await this.cryptoService.generateJwt({
sub: user.uuid,
username: user.username,
});
return {
access_token: accessToken,
refresh_token: session.refreshToken,
};
}
async logout() {
return { message: "User logged out" };
}
}