import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateAuthDto } from './dto/create-auth.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { encry, User } from '@app/models';
import { Repository } from 'typeorm';
import { RedisService } from '../../libs/redis/redis.service';
import { I18nTranslations } from '../.generate/i18n.generated';
import { I18nContext, I18nService } from 'nestjs-i18n';
import { TokenService } from './token.service';
import { AccessTokenPayload, RefreshTokenPayload } from './entity/token';
import { pick } from '../../libs/utils/pick';
import { JwtService } from '@app/jwt';
import { ConfigService } from '@nestjs/config';
import { Configure } from 'src/config-schema';
import { WithLock } from '@app/locker/with-lock.decorator';
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private user: Repository<User>,
private jwtService: JwtService,
private readonly redisService: RedisService,
private readonly i18n: I18nService<I18nTranslations>,
private tokenService: TokenService,
private cfg: ConfigService<Configure, true>,
) {}
async getToken(userId: string): Promise<string | null> {
return this.redisService.getUserToken(`user:${userId}:token`);
}
async kickOut(id: number) {
await this.tokenService.revokeByUid(id);
}
async logout(token: string): Promise<void> {
const decoded = await this.jwtService.verify<AccessTokenPayload>(token);
const {id,jti} = decoded.payload;
const accessToken = await this.tokenService.getTokenByJti(id,jti,'at');
if (accessToken) {
await this.tokenService.revokeToken(accessToken)
}
return;
}
@WithLock({
key(args) {
let uid = 'unknown';
try {
const token = this.jwtService.decode(args[0]) as any;
uid = token?.id;
} catch {}
return `user-token:${uid}`;
}
})
async refreshToken(
maybeToken: string
){
const token = this.jwtService.decode<AccessTokenPayload | RefreshTokenPayload>(maybeToken);
if ('refreshTokenJti' in token) {
throw new HttpException(
this.i18n.translate('exception.common.tokenError'),
HttpStatus.BAD_REQUEST
)
}
const refresTokenObject = token as RefreshTokenPayload;
const {id, jti, accessTokenJti, email} = refresTokenObject;
const refreshToken = await this.tokenService.getTokenByJti(id, jti,'rt');
if (!refreshToken) {
throw new HttpException(
this.i18n.translate('exception.common.tokenExpire'),
HttpStatus.UNAUTHORIZED
)
}
const accessToken = await this.tokenService.getTokenByJti(id, accessTokenJti, 'at');
if (accessToken){
await this.tokenService.revokeToken(accessToken);
}
await this.tokenService.revokeRefreshToken(refreshToken);
const tokenPair = await this.tokenService.createToken(id, email);
await this.tokenService.issueToken(id, tokenPair);
return pick(tokenPair, ['accessToken', 'accessTokenTTL', 'refreshToken', 'refreshTokenTTL'])
}
async login(dto: CreateAuthDto) {
const { email, password } = dto;
const userInfo = await this.user.findOne({ where: { email } });
if (userInfo === null) {
throw new HttpException(
this.i18n.translate('exception.auth.userNotExists', {
lang: I18nContext.current().lang,
}),
HttpStatus.NOT_FOUND
);
}
if (encry(password, userInfo.salt) !== userInfo.password) {
throw new HttpException(
this.i18n.translate('exception.auth.passwordOrEmailError', {
lang: I18nContext.current().lang,
}),
HttpStatus.BAD_REQUEST
);
}
const payload = {
email,
id: userInfo.id
};
const token = await this.tokenService.createToken(payload.id, payload.email);
await this.tokenService.issueToken(payload.id, token);
return pick(token, ['accessToken', 'accessTokenTTL', 'refreshToken', 'refreshTokenTTL'])
}
async generateApiToken(dto: CreateAuthDto, tokenName?: string) {
const { email, password } = dto;
const userInfo = await this.user.findOne({ where: { email } });
if (userInfo === null) {
throw new HttpException(
this.i18n.translate('exception.auth.userNotExists', {
lang: I18nContext.current().lang,
}),
HttpStatus.NOT_FOUND
);
}
if (encry(password, userInfo.salt) !== userInfo.password) {
throw new HttpException(
this.i18n.translate('exception.auth.passwordOrEmailError', {
lang: I18nContext.current().lang,
}),
HttpStatus.BAD_REQUEST
);
}
const payload = {
email,
type: 'api',
};
const token = await this.jwtService.sign(payload, this.cfg.get('REDIS_SECONDS') * 1000);
const tokenId =
tokenName ||
`api_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const ttl = parseInt(process.env.API_TOKEN_SECONDS) || 86400 * 7;
await this.redisService.setApiToken(email, tokenId, token, ttl);
return {
token,
tokenId,
expiresIn: ttl,
};
}
async validateApiToken(email: string, token: string): Promise<boolean> {
const apiTokens = await this.redisService.getAllApiTokens(email);
return apiTokens.includes(token);
}
async revokeApiToken(email: string, tokenId: string): Promise<void> {
await this.redisService.delApiToken(email, tokenId);
}
}