user module

This commit is contained in:
2026-01-13 19:59:06 +03:30
parent 6522acff5f
commit d630cb844a
62 changed files with 1137 additions and 3040 deletions
+58 -58
View File
@@ -18,11 +18,11 @@ export class TokensService {
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
) {}
) { }
async generateAccessAndRefreshToken(
ownerId: string,
restaurantId: string,
: string,
isAdmin: boolean,
slug: string,
em?: EntityManager,
@@ -31,15 +31,15 @@ export class TokensService {
const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
? { adminId: ownerId, restId: restaurantId }
: { userId: ownerId, restId: restaurantId, slug };
? { adminId: ownerId, : }
: { userId: ownerId, : , slug };
const accessToken = await this.generateAccessToken(payload, accessExpire);
const refreshToken = this.generateRefreshToken();
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER;
// Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type, em);
await this.storeRefreshToken(ownerId, , refreshToken, type, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() },
@@ -56,7 +56,7 @@ export class TokensService {
async storeRefreshToken(
ownerId: string,
restId: string,
: string,
refreshToken: string,
type: RefreshTokenType,
em?: EntityManager,
@@ -70,76 +70,76 @@ export class TokensService {
const token = entityManager.create(RefreshToken, {
hashedToken,
ownerId,
restId,
,
type,
expiresAt,
});
if (em) {
// Within transaction, just persist (flush will be called by transaction)
entityManager.persist(token);
} else {
// Outside transaction, persist and flush immediately
await entityManager.persistAndFlush(token);
}
if(em) {
// Within transaction, just persist (flush will be called by transaction)
entityManager.persist(token);
} else {
// Outside transaction, persist and flush immediately
await entityManager.persistAndFlush(token);
}
}
async refreshToken(oldRefreshToken: string) {
const hashedToken = this.hashToken(oldRefreshToken);
const hashedToken = this.hashToken(oldRefreshToken);
// Use transaction to ensure atomicity and prevent race conditions
return this.em.transactional(async em => {
// Lock the token row to prevent concurrent refresh attempts
// Using pessimistic write lock (FOR UPDATE) to prevent race conditions
const token = await em.findOne(RefreshToken, { hashedToken }, { lockMode: LockMode.PESSIMISTIC_WRITE });
// Use transaction to ensure atomicity and prevent race conditions
return this.em.transactional(async em => {
// Lock the token row to prevent concurrent refresh attempts
// Using pessimistic write lock (FOR UPDATE) to prevent race conditions
const token = await em.findOne(RefreshToken, { hashedToken }, { lockMode: LockMode.PESSIMISTIC_WRITE });
if (!token) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
if (!token) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
// Check expiration
if (dayjs(token.expiresAt).isBefore(dayjs())) {
em.remove(token);
await em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Check expiration
if (dayjs(token.expiresAt).isBefore(dayjs())) {
em.remove(token);
await em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Store token data before removal
const ownerId = token.ownerId;
const restId = token.restId;
const isAdmin = token.type === RefreshTokenType.ADMIN;
// Store token data before removal
const ownerId = token.ownerId;
const = token.;
const isAdmin = token.type === RefreshTokenType.ADMIN;
// Verify restaurant still exists
const restaurant = await em.findOne(Restaurant, { id: restId });
if (!restaurant) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
// Verify restaurant still exists
const restaurant = await em.findOne(Restaurant, { id: });
if (!restaurant) {
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
try {
// Generate new tokens first (before removing old token)
// Pass the transaction EntityManager to ensure operations are within the transaction
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
try {
// Generate new tokens first (before removing old token)
// Pass the transaction EntityManager to ensure operations are within the transaction
const tokens = await this.generateAccessAndRefreshToken(ownerId, restaurant.id, isAdmin, restaurant.slug, em);
// Remove old token only after new token is created
em.remove(token);
// Remove old token only after new token is created
em.remove(token);
// Persist changes atomically
await em.flush();
// Persist changes atomically
await em.flush();
return tokens;
} catch (error) {
this.logger.error('Failed to generate new tokens after refresh', error);
// Transaction will rollback automatically, preserving the old token
throw new UnauthorizedException('Failed to refresh token');
}
});
}
return tokens;
} catch (error) {
this.logger.error('Failed to generate new tokens after refresh', error);
// Transaction will rollback automatically, preserving the old token
throw new UnauthorizedException('Failed to refresh token');
}
});
}
private generateRefreshToken() {
return randomBytes(32).toString('hex');
}
return randomBytes(32).toString('hex');
}
private hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex');
}
return createHash('sha256').update(token).digest('hex');
}
}