user module
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class DirectLoginDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'Restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
@@ -8,8 +8,4 @@ export class RequestOtpDto {
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -14,8 +14,4 @@ export class VerifyOtpDto {
|
||||
@Length(5)
|
||||
otp: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'zhivan', description: 'restaurant slug' })
|
||||
slug: string;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export enum RefreshTokenType {
|
||||
}
|
||||
|
||||
@Entity({ tableName: 'refreshtokens' })
|
||||
@Index({ properties: ['ownerId', 'restId', 'type'] })
|
||||
@Index({ properties: ['ownerId', '', 'type'] })
|
||||
@Index({ properties: ['hashedToken'] })
|
||||
@Index({ properties: ['expiresAt'] })
|
||||
export class RefreshToken extends BaseEntity {
|
||||
@@ -19,11 +19,11 @@ export class RefreshToken extends BaseEntity {
|
||||
ownerId!: string;
|
||||
|
||||
@Property()
|
||||
restId!: string;
|
||||
!: string;
|
||||
|
||||
@Enum(() => RefreshTokenType)
|
||||
@Enum(() => RefreshTokenType)
|
||||
type!: RefreshTokenType;
|
||||
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
@Property({ type: 'timestamptz' })
|
||||
expiresAt!: Date;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export class AdminAuthGuard implements CanActivate {
|
||||
secret,
|
||||
});
|
||||
|
||||
if (!payload.adminId || !payload.restId) {
|
||||
if (!payload.adminId || !payload.) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
@@ -70,9 +70,9 @@ export class AdminAuthGuard implements CanActivate {
|
||||
return true;
|
||||
}
|
||||
|
||||
const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.restId);
|
||||
const adminPermission = await this.permissionsService.getAdminPermissions(payload.adminId, payload.);
|
||||
if (!adminPermission || !Array.isArray(adminPermission)) {
|
||||
this.logger.error('No permissions found', { adminId: payload.adminId, restId: payload.restId });
|
||||
this.logger.error('No permissions found', { adminId: payload.adminId, : payload. });
|
||||
throw new ForbiddenException('No permissions found');
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export class AdminAuthGuard implements CanActivate {
|
||||
if (!hasPermission) {
|
||||
this.logger.warn('Insufficient permissions', {
|
||||
adminId: payload.adminId,
|
||||
restId: payload.restId,
|
||||
: payload.,
|
||||
required: requiredPermissions,
|
||||
has: adminPermission,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export class AuthGuard implements CanActivate {
|
||||
private readonly jwtService: JwtService,
|
||||
@Inject(ConfigService)
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<UserAuthRequest>();
|
||||
@@ -25,7 +25,7 @@ export class AuthGuard implements CanActivate {
|
||||
try {
|
||||
const secret = this.configService.getOrThrow<string>('JWT_SECRET');
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
@@ -34,13 +34,13 @@ export class AuthGuard implements CanActivate {
|
||||
const payload = await this.jwtService.verifyAsync<ITokenPayload>(token, {
|
||||
secret,
|
||||
});
|
||||
if (!payload.userId || !payload.restId) {
|
||||
if (!payload.userId || !payload.) {
|
||||
this.logger.error('Invalid token payload structure', payload);
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
|
||||
|
||||
request['userId'] = payload.userId;
|
||||
|
||||
|
||||
} catch {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
@@ -58,5 +58,5 @@ export class AuthGuard implements CanActivate {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export interface ITokenPayload {
|
||||
userId: string;
|
||||
restId: string;
|
||||
: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface IAdminTokenPayload {
|
||||
adminId: string;
|
||||
restId: string;
|
||||
: string;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user