220 lines
5.8 KiB
TypeScript
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" };
|
|
}
|
|
}
|