Files
memegoat/backend/src/auth/auth.service.ts

220 lines
5.8 KiB
TypeScript

import {
BadRequestException,
forwardRef,
Inject,
Injectable,
Logger,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { authenticator } from "otplib";
import { toDataURL } from "qrcode";
import { HashingService } from "../crypto/services/hashing.service";
import { JwtService } from "../crypto/services/jwt.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 {
private readonly logger = new Logger(AuthService.name);
constructor(
@Inject(forwardRef(() => UsersService))
private readonly usersService: UsersService,
private readonly hashingService: HashingService,
private readonly jwtService: JwtService,
private readonly sessionsService: SessionsService,
private readonly configService: ConfigService,
) {}
async generateTwoFactorSecret(userId: string) {
this.logger.log(`Generating 2FA secret for user ${userId}`);
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) {
this.logger.log(`Enabling 2FA for user ${userId}`);
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) {
this.logger.log(`Disabling 2FA for user ${userId}`);
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) {
this.logger.log(`Registering new user: ${dto.username}`);
const { username, email, password } = dto;
const passwordHash = await this.hashingService.hashPassword(password);
const emailHash = await this.hashingService.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) {
this.logger.log(`Login attempt for email: ${dto.email}`);
const { email, password } = dto;
const emailHash = await this.hashingService.hashEmail(email);
const user = await this.usersService.findByEmailHash(emailHash);
if (!user) {
this.logger.warn(`Login failed: user not found for email hash`);
throw new UnauthorizedException("Invalid credentials");
}
const isPasswordValid = await this.hashingService.verifyPassword(
password,
user.passwordHash,
);
if (!isPasswordValid) {
this.logger.warn(`Login failed: invalid password for user ${user.uuid}`);
throw new UnauthorizedException("Invalid credentials");
}
if (user.isTwoFactorEnabled) {
this.logger.log(`2FA required for user ${user.uuid}`);
return {
message: "2FA required",
requires2FA: true,
userId: user.uuid,
};
}
const accessToken = await this.jwtService.generateJwt({
sub: user.uuid,
username: user.username,
});
const session = await this.sessionsService.createSession(
user.uuid,
userAgent,
ip,
);
this.logger.log(`User ${user.uuid} logged in successfully`);
return {
message: "User logged in successfully",
access_token: accessToken,
refresh_token: session.refreshToken,
};
}
async verifyTwoFactorLogin(
userId: string,
token: string,
userAgent?: string,
ip?: string,
) {
this.logger.log(`2FA verification attempt for user ${userId}`);
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) {
this.logger.warn(
`2FA verification failed for user ${userId}: invalid token`,
);
throw new UnauthorizedException("Invalid 2FA token");
}
const accessToken = await this.jwtService.generateJwt({
sub: user.uuid,
username: user.username,
});
const session = await this.sessionsService.createSession(
user.uuid,
userAgent,
ip,
);
this.logger.log(`User ${userId} logged in successfully via 2FA`);
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.jwtService.generateJwt({
sub: user.uuid,
username: user.username,
});
return {
access_token: accessToken,
refresh_token: session.refreshToken,
};
}
async logout() {
return { message: "User logged out" };
}
}