2023-06-30 16:47:29 +00:00
|
|
|
import {
|
|
|
|
BadRequestException,
|
|
|
|
ForbiddenException,
|
|
|
|
Injectable,
|
|
|
|
PreconditionFailedException,
|
|
|
|
} from '@nestjs/common';
|
2023-06-29 17:41:36 +00:00
|
|
|
import { LoginRequest } from '../interfaces/auth.interface';
|
2023-06-30 16:47:29 +00:00
|
|
|
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';
|
2023-06-30 18:29:34 +00:00
|
|
|
import { RoleService } from './role.service';
|
|
|
|
import { ConfigService } from '@nestjs/config';
|
2023-06-29 14:13:57 +00:00
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
export class AuthService {
|
2023-06-30 16:47:29 +00:00
|
|
|
constructor(
|
|
|
|
private readonly jwtService: JWTService,
|
|
|
|
private readonly otpService: OTPService,
|
|
|
|
private readonly banService: BanService,
|
2023-06-30 18:29:34 +00:00
|
|
|
private readonly roleService: RoleService,
|
2023-06-30 16:47:29 +00:00
|
|
|
@InjectRepository(UserEntity)
|
|
|
|
private readonly userRepository: Repository<UserEntity>,
|
2023-06-30 18:29:34 +00:00
|
|
|
private readonly config: ConfigService,
|
2023-06-30 16:47:29 +00:00
|
|
|
) {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Login by username/email and password
|
|
|
|
* @param body Username/email and password
|
|
|
|
* @returns JWT token
|
|
|
|
*/
|
2023-06-29 14:13:57 +00:00
|
|
|
async login(body: LoginRequest) {
|
2023-06-30 16:47:29 +00:00
|
|
|
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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-30 18:29:34 +00:00
|
|
|
// Check for active ban
|
2023-06-30 16:47:29 +00:00
|
|
|
const bans = await this.banService.getActiveBansForUser(userEntity);
|
|
|
|
const banned = !!bans.length;
|
|
|
|
|
2023-06-30 18:29:34 +00:00
|
|
|
// Get all privileges applicable to user
|
|
|
|
const privileges = await this.roleService.getUserPrivileges(userEntity);
|
|
|
|
|
2023-06-30 16:47:29 +00:00
|
|
|
// Issue token
|
2023-06-30 18:29:34 +00:00
|
|
|
const exp = this.config.get('security.tokenExpiry');
|
2023-06-30 16:47:29 +00:00
|
|
|
const issuedToken = await this.jwtService.sign({
|
|
|
|
sub: userEntity.id,
|
|
|
|
username: userEntity.username,
|
|
|
|
display_name: userEntity.displayName,
|
|
|
|
language: userEntity.language,
|
|
|
|
banned: banned,
|
2023-06-30 18:29:34 +00:00
|
|
|
privileges: banned
|
|
|
|
? []
|
|
|
|
: privileges.map((privilege) => privilege.privilege),
|
2023-06-30 16:47:29 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Set login time to now
|
|
|
|
await this.userRepository.update(
|
|
|
|
{ id: userEntity.id },
|
|
|
|
{ loginAt: new Date() },
|
|
|
|
);
|
|
|
|
|
2023-06-30 18:29:34 +00:00
|
|
|
return {
|
|
|
|
token: issuedToken,
|
|
|
|
expires_in: exp,
|
|
|
|
};
|
2023-06-30 16:47:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-06-30 18:29:34 +00:00
|
|
|
* Get user from token
|
2023-06-30 16:47:29 +00:00
|
|
|
* @param token JWT Token
|
|
|
|
* @returns User entity
|
|
|
|
*/
|
|
|
|
async getUserFromToken(token: string) {
|
2023-06-30 18:29:34 +00:00
|
|
|
const tokenInfo = await this.verifyToken(token);
|
2023-06-30 16:47:29 +00:00
|
|
|
const user = await this.userRepository.findOneByOrFail({
|
|
|
|
id: tokenInfo.sub,
|
|
|
|
activated: true,
|
|
|
|
});
|
|
|
|
return instanceToPlain(user);
|
|
|
|
}
|
|
|
|
|
2023-06-30 18:29:34 +00:00
|
|
|
/**
|
|
|
|
* 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 }));
|
|
|
|
}
|
|
|
|
|
2023-06-30 16:47:29 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
2023-06-29 14:13:57 +00:00
|
|
|
}
|
|
|
|
}
|