update auth

This commit is contained in:
2025-11-24 16:08:16 +03:30
parent e79040d81e
commit 9996ea3b09
8 changed files with 91 additions and 366 deletions
+41 -265
View File
@@ -1,19 +1,13 @@
import { randomBytes } from 'node:crypto';
import { EntityManager } from '@mikro-orm/postgresql';
import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import dayjs from 'dayjs';
import { AuthMessage, UserMessage } from '../../../common/enums/message.enum';
import { RefreshToken } from '../../users/entities/refresh-token.entity';
import { User } from '../../users/entities/user.entity';
import { AuthMessage } from '../../../common/enums/message.enum';
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
import { Restaurant } from '../../restaurants/entities/restaurant.entity';
import { RestRepository } from '../../restaurants/repositories/rest.repository';
import { Admin } from '../../admin/entities/admin.entity';
import { CacheService } from '../../utils/cache.service';
@Injectable()
export class TokensService {
@@ -22,286 +16,68 @@ export class TokensService {
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
private readonly restRepository: RestRepository,
private readonly cacheService: CacheService,
) {}
async generateTokens(ownerId: string, rest: Restaurant, em?: EntityManager) {
return this.generateAccessAndRefreshToken(
{
userId: ownerId,
restId: rest.id,
},
em,
);
}
async generateTokensAdmin(adminId: string, rest: Restaurant, em?: EntityManager) {
return this.generateAccessAndRefreshTokenAdmin(
{
adminId,
restId: rest.id,
},
em,
);
}
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
async generateAccessAndRefreshToken(ownerId: string, restaurantId: string, isAdmin: boolean) {
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
const tokenPayload: ITokenPayload = {
userId: payload.userId,
restId: payload.restId,
};
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
? { adminId: ownerId, restId: restaurantId }
: { userId: ownerId, restId: restaurantId };
const accessToken = await this.generateAccessToken(payload, accessExpire);
const refreshToken = this.generateRefreshToken();
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER;
// Skip validation when called from refresh token flow (when em is provided and in transaction)
const skipValidation = !!em && em.isInTransaction();
await this.storeRefreshToken(payload.userId, payload.restId, refreshToken, em, skipValidation);
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minutes').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
};
}
private async generateAccessAndRefreshTokenAdmin(payload: IAdminTokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expire: number) {
return this.jwtService.signAsync(payload, { expiresIn: `${expire}m` });
}
async storeRefreshToken(ownerId: string, restId: string, refreshToken: string, type: RefreshTokenType) {
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
const tokenPayload: IAdminTokenPayload = {
adminId: payload.adminId,
restId: payload.restId,
};
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
const refreshToken = this.generateRefreshToken();
const token = this.em.create(RefreshToken, {
token: refreshToken,
ownerId,
restId,
type,
expiresAt,
});
// Skip validation when called from refresh token flow (when em is provided and in transaction)
const skipValidation = !!em && em.isInTransaction();
await this.storeRefreshTokenAdmin(payload.adminId, payload.restId, refreshToken, em, skipValidation);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
};
return this.em.persistAndFlush(token);
}
async storeRefreshToken(
userId: string,
restId: string,
refreshToken: string,
em?: EntityManager,
skipValidation = false,
) {
const entityManager = em || this.em.fork();
const isExternalTransaction = !!em && em.isInTransaction();
async refreshToken(oldRefreshToken: string, isAdmin: boolean) {
const token = await this.em.findOne(RefreshToken, { token: oldRefreshToken });
try {
// Only start transaction if no external EntityManager with transaction is provided
if (!isExternalTransaction && !entityManager.isInTransaction()) {
await entityManager.begin();
}
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
// Skip User validation when refreshing (user is already validated via refresh token)
if (!skipValidation) {
const user = await entityManager.findOne(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
}
const restaurant = await entityManager.findOne(Restaurant, { id: restId });
if (!restaurant) throw new BadRequestException(UserMessage.Rest_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
restaurant,
ownerId: userId,
expiresAt,
});
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
await entityManager.commit();
}
}
} catch (error) {
this.logger.error(error);
// Only rollback if we started the transaction
if (!isExternalTransaction && entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
if (dayjs(token.expiresAt).isBefore(dayjs())) {
this.em.remove(token);
await this.em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
}
async storeRefreshTokenAdmin(
adminId: string,
restId: string,
refreshToken: string,
em?: EntityManager,
skipValidation = false,
) {
const entityManager = em || this.em.fork();
const isExternalTransaction = !!em && em.isInTransaction();
// Remove the old token
this.em.remove(token);
try {
// Only start transaction if no external EntityManager with transaction is provided
if (!isExternalTransaction && !entityManager.isInTransaction()) {
await entityManager.begin();
}
// Generate new tokens within the same transaction
const tokens = await this.generateAccessAndRefreshToken(token.ownerId, token.restId, isAdmin);
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
// Commit the transaction
await this.em.flush();
// Skip Admin validation when refreshing (admin is already validated via refresh token)
if (!skipValidation) {
const admin = await entityManager.findOne(Admin, { id: adminId });
if (!admin) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
}
const restaurant = await entityManager.findOne(Restaurant, { id: restId });
if (!restaurant) throw new BadRequestException(UserMessage.Rest_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
restaurant,
ownerId: adminId,
expiresAt,
});
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
await entityManager.commit();
}
}
} catch (error) {
this.logger.error(error);
// Only rollback if we started the transaction
if (!isExternalTransaction && entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async refreshToken(oldRefreshToken: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken, isRevoked: { $ne: true } },
{
populate: ['restaurant'],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
// const rest = await this.restRepository.findOne({ id: token.user.id });
// if (!rest) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
entityManager.remove(token);
await entityManager.flush();
await entityManager.commit();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Remove the old token
entityManager.remove(token);
// Generate new tokens within the same transaction
const tokens = await this.generateTokens(token.ownerId, token.restaurant, entityManager);
// Commit the transaction
await entityManager.flush();
await entityManager.commit();
return tokens;
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async refreshTokenAdmin(oldRefreshToken: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken, isRevoked: { $ne: true } },
{
populate: ['restaurant'],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
// const rest = await this.restRepository.findOne({ id: token.user.id });
// if (!rest) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
entityManager.remove(token);
await entityManager.flush();
await entityManager.commit();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Remove the old token
entityManager.remove(token);
// Generate new tokens within the same transaction
const tokens = await this.generateTokensAdmin(token.ownerId, token.restaurant, entityManager);
// Commit the transaction
await entityManager.flush();
await entityManager.commit();
return tokens;
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
}
async invalidateRefreshToken(userId: string) {
const entityManager = this.em.fork();
try {
await entityManager.begin();
await entityManager.nativeDelete(RefreshToken, { ownerId: userId });
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
await entityManager.commit();
} catch (error) {
this.logger.error(error);
if (entityManager.isInTransaction()) {
await entityManager.rollback();
}
throw error;
}
return tokens;
}
private generateRefreshToken() {