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:
197
backend/src/auth/auth.service.ts
Normal file
197
backend/src/auth/auth.service.ts
Normal 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" };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user