import { Injectable, UnauthorizedException, BadRequestException, } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { JwtService } from "@nestjs/jwt"; import { UsersService } from "../users/users.service"; import { LoginDto, ChangePasswordDto, SetupPasswordDto } from "./dto"; import { TokenPair } from "./interfaces"; @Injectable() export class AuthService { constructor( private jwtService: JwtService, private configService: ConfigService, private usersService: UsersService, ) {} async validateUser( loginDto: LoginDto, ): Promise<{ id: string; username: string }> { const { username, password } = loginDto; const user = await this.usersService.findByUsername(username); if (!user) { throw new UnauthorizedException("Invalid credentials"); } if (!user.isPasswordSet) { throw new BadRequestException( "Password not set. Please use /auth/setup endpoint first.", ); } const isPasswordValid = await this.usersService.validatePassword( user, password, ); if (!isPasswordValid) { throw new UnauthorizedException("Invalid credentials"); } if (!user.isAdmin) { throw new UnauthorizedException("Access denied"); } return { id: user.id, username: user.username }; } async setupPassword( setupPasswordDto: SetupPasswordDto, ): Promise<{ id: string; username: string }> { const { username, password } = setupPasswordDto; const user = await this.usersService.findByUsername(username); if (!user) { throw new UnauthorizedException("User not found"); } if (!user.isAdmin) { throw new UnauthorizedException("Access denied"); } if (user.isPasswordSet) { throw new BadRequestException( "Password already set. Use /auth/change-password to change it.", ); } await this.usersService.setupPassword(username, password); return { id: user.id, username: user.username }; } async needsSetup(): Promise { return this.usersService.needsPasswordSetup(); } async generateTokens(userId: string, username: string): Promise { const accessPayload = { sub: userId, username, type: "access", }; const refreshPayload = { sub: userId, username, type: "refresh", }; const accessSecret = this.configService.get("JWT_ACCESS_SECRET") || "default-secret"; const refreshSecret = this.configService.get("JWT_REFRESH_SECRET") || "default-refresh-secret"; const [accessToken, refreshToken] = await Promise.all([ this.jwtService.signAsync(accessPayload, { secret: accessSecret, expiresIn: "15m", }), this.jwtService.signAsync(refreshPayload, { secret: refreshSecret, expiresIn: "7d", }), ]); return { accessToken, refreshToken }; } async refreshTokens(userId: string, username: string): Promise { const user = await this.usersService.findById(userId); if (!user || !user.isAdmin) { throw new UnauthorizedException("User not found or no longer authorized"); } return this.generateTokens(userId, username); } getCookieOptions(isRefreshToken = false) { // const isProduction = // this.configService.get("NODE_ENV") === "production"; // const cookieSecure = // this.configService.get("COOKIE_SECURE") === "true"; // const domain = this.configService.get("COOKIE_DOMAIN"); return { httpOnly: true, // secure: isProduction || cookieSecure, sameSite: "none" as const, path: isRefreshToken ? "/auth/refresh" : "/", // domain: domain, maxAge: isRefreshToken ? 7 * 24 * 60 * 60 * 1000 : 15 * 60 * 1000, }; } getClearCookieOptions(isRefreshToken = false) { return { httpOnly: true, path: isRefreshToken ? "/auth/refresh" : "/", }; } async changePassword( userId: string, changePasswordDto: ChangePasswordDto, ): Promise { const { currentPassword, newPassword } = changePasswordDto; const user = await this.usersService.findById(userId); if (!user) { throw new UnauthorizedException("User not found"); } const isCurrentPasswordValid = await this.usersService.validatePassword( user, currentPassword, ); if (!isCurrentPasswordValid) { throw new BadRequestException("Current password is incorrect"); } if (currentPassword === newPassword) { throw new BadRequestException( "New password must be different from current password", ); } await this.usersService.updatePassword(userId, newPassword); } }