web-service/apps/auth/src/services/auth.service.ts
2023-06-30 21:29:34 +03:00

168 lines
4.5 KiB
TypeScript

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<UserEntity>,
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);
}
}