import { BadRequestException, ForbiddenException, Injectable, PreconditionFailedException, } from '@nestjs/common'; import { LoginRequest } from '../interfaces/auth.interface'; import { JWTService } from './jwt.service'; import { ILike, Repository } from 'typeorm'; import { UserEntity } from '../database/entities/user.entity'; import { compare } from 'bcrypt'; import { instanceToPlain } from 'class-transformer'; import { InjectRepository } from '@nestjs/typeorm'; import { OTPService } from './otp.service'; import { BanService } from './ban.service'; import { UserInfo } from '@freeblox/shared'; import { RoleService } from './role.service'; import { ConfigService } from '@nestjs/config'; @Injectable() export class AuthService { constructor( private readonly jwtService: JWTService, private readonly otpService: OTPService, private readonly banService: BanService, private readonly roleService: RoleService, @InjectRepository(UserEntity) private readonly userRepository: Repository, private readonly config: ConfigService, ) {} /** * Login by username/email and password * @param body Username/email and password * @returns JWT token */ async login(body: LoginRequest) { if (!body.email || !body.password) { throw new BadRequestException('Invalid username or password'); } // Prevent wildcards const userInput = body.email?.replace(/%/g, ''); const userEntity = await this.userRepository.findOne({ where: [ { username: ILike(userInput), activated: true, }, { email: ILike(userInput), activated: true, }, ], }); // User not found if (!userEntity) { throw new BadRequestException('Invalid username or password'); } // Compare passwords const passwordMatch = await compare(body.password, userEntity.password); if (!passwordMatch) { throw new BadRequestException('Invalid username or password'); } // Check TOTP const userOTPToken = await this.otpService.getUserTOTP(userEntity); if (userOTPToken) { if (!body.totpToken) { throw new PreconditionFailedException('TOTP Token required'); } const validate = this.otpService.validateTOTP( userOTPToken.token, body.totpToken, ); if (!validate) { throw new ForbiddenException('Invalid TOTP Token'); } } // Check for active ban const bans = await this.banService.getActiveBansForUser(userEntity); const banned = !!bans.length; // Get all privileges applicable to user const privileges = await this.roleService.getUserPrivileges(userEntity); // Issue token const exp = this.config.get('security.tokenExpiry'); const issuedToken = await this.jwtService.sign({ sub: userEntity.id, username: userEntity.username, display_name: userEntity.displayName, language: userEntity.language, banned: banned, privileges: banned ? [] : privileges.map((privilege) => privilege.privilege), }); // Set login time to now await this.userRepository.update( { id: userEntity.id }, { loginAt: new Date() }, ); return { token: issuedToken, expires_in: exp, }; } /** * Get user from token * @param token JWT Token * @returns User entity */ async getUserFromToken(token: string) { const tokenInfo = await this.verifyToken(token); const user = await this.userRepository.findOneByOrFail({ id: tokenInfo.sub, activated: true, }); return instanceToPlain(user); } /** * Verify user token * @param token JWT token * @returns User token info */ async verifyToken(token: string) { try { return await this.jwtService.verify(token); } catch (e) { console.error(token, e); throw new ForbiddenException('Invalid token'); } } /** * Get user entity by ID * @param id User ID * @returns User entity */ async getUserById(id: string) { if (!id) throw new BadRequestException('ID is required'); return instanceToPlain(await this.userRepository.findOneByOrFail({ id })); } /** * Get user bans * @param tokeninfo */ async getUserBans(userInfo: UserInfo) { const user = await this.userRepository.findOneByOrFail({ id: userInfo.sub, activated: true, }); const bans = await this.banService.getAllBansForUser(user); return instanceToPlain(bans); } }