update auth
This commit is contained in:
@@ -28,7 +28,7 @@ export class AdminService {
|
|||||||
async findById(adminId: string, restId: string): Promise<Admin | null> {
|
async findById(adminId: string, restId: string): Promise<Admin | null> {
|
||||||
const admin = await this.adminRepository.findOne(
|
const admin = await this.adminRepository.findOne(
|
||||||
{ id: adminId, roles: { restaurant: { id: restId } } },
|
{ id: adminId, roles: { restaurant: { id: restId } } },
|
||||||
{ populate: ['roles', 'roles.role'], distinct: true },
|
{ populate: ['roles', 'roles.role'] },
|
||||||
);
|
);
|
||||||
return admin;
|
return admin;
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ export class AdminService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update role if roleId is provided
|
// Update role if roleId is provided
|
||||||
if (roleId) {
|
if (roleId ) {
|
||||||
const role = await this.em.findOne(Role, { id: roleId });
|
const role = await this.em.findOne(Role, { id: roleId });
|
||||||
if (!role) {
|
if (!role) {
|
||||||
throw new NotFoundException('Role not found');
|
throw new NotFoundException('Role not found');
|
||||||
@@ -147,6 +147,8 @@ export class AdminService {
|
|||||||
// Update existing role
|
// Update existing role
|
||||||
existingAdminRole.role = role;
|
existingAdminRole.role = role;
|
||||||
} else {
|
} else {
|
||||||
|
const restaurant = await this.em.findOne(Restaurant, { id: restId });
|
||||||
|
if (!restaurant) throw new NotFoundException('Restaurant not found');
|
||||||
// Create new AdminRole
|
// Create new AdminRole
|
||||||
const newAdminRole = this.em.create(AdminRole, {
|
const newAdminRole = this.em.create(AdminRole, {
|
||||||
admin,
|
admin,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
|
|||||||
import { User } from '../users/entities/user.entity';
|
import { User } from '../users/entities/user.entity';
|
||||||
import { AdminAuthController } from './controllers/admin-auth.controller';
|
import { AdminAuthController } from './controllers/admin-auth.controller';
|
||||||
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
import { AdminAuthGuard } from './guards/adminAuth.guard';
|
||||||
|
import { RefreshToken } from './entities/refresh-token.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -29,7 +30,7 @@ import { AdminAuthGuard } from './guards/adminAuth.guard';
|
|||||||
}),
|
}),
|
||||||
forwardRef(() => AdminModule),
|
forwardRef(() => AdminModule),
|
||||||
forwardRef(() => RestaurantsModule),
|
forwardRef(() => RestaurantsModule),
|
||||||
MikroOrmModule.forFeature([User]),
|
MikroOrmModule.forFeature([User, RefreshToken]),
|
||||||
],
|
],
|
||||||
controllers: [AuthController, AdminAuthController],
|
controllers: [AuthController, AdminAuthController],
|
||||||
providers: [AuthService, TokensService, AdminAuthGuard],
|
providers: [AuthService, TokensService, AdminAuthGuard],
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class AdminAuthController {
|
|||||||
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
||||||
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
||||||
otpRequest(@Body() dto: RequestOtpDto) {
|
otpRequest(@Body() dto: RequestOtpDto) {
|
||||||
return this.authService.requestOtpAdmin(dto);
|
return this.authService.requestOtp(dto, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('otp/verify')
|
@Post('otp/verify')
|
||||||
@@ -31,28 +31,11 @@ export class AdminAuthController {
|
|||||||
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Post('admin/otp/request')
|
|
||||||
// @ApiOperation({ summary: 'Request OTP for login or signup' })
|
|
||||||
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
|
||||||
// @ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
|
||||||
// @ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
|
||||||
// adminOtpRequest(@Body() dto: RequestOtpDto) {
|
|
||||||
// return this.authService.requestOtpAdmin(dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Post('admin/otp/verify')
|
|
||||||
// @ApiOperation({ summary: 'Verify OTP code' })
|
|
||||||
// @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
|
||||||
// @ApiResponse({ status: 200, description: 'OTP verified successfully' })
|
|
||||||
// @ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
|
|
||||||
// adminOtpVerify(@Body() dto: VerifyOtpDto) {
|
|
||||||
// return this.authService.verifyOtpAdmin(dto.phone, dto.otp); // assuming dto has `otp` property
|
|
||||||
// }
|
|
||||||
|
|
||||||
@RefreshTokenRateLimit()
|
@RefreshTokenRateLimit()
|
||||||
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
@ApiOperation({ summary: 'refresh the admin access token / refresh token' })
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||||
return this.authService.refreshTokenAdmin(refreshTokenDto.refreshToken);
|
return this.authService.refreshToken(refreshTokenDto.refreshToken, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class AuthController {
|
|||||||
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
@ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
||||||
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
@ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
||||||
otpRequest(@Body() dto: RequestOtpDto) {
|
otpRequest(@Body() dto: RequestOtpDto) {
|
||||||
return this.authService.requestOtp(dto);
|
return this.authService.requestOtp(dto, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('otp/verify')
|
@Post('otp/verify')
|
||||||
@@ -31,28 +31,12 @@ export class AuthController {
|
|||||||
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Post('admin/otp/request')
|
|
||||||
// @ApiOperation({ summary: 'Request OTP for login or signup' })
|
|
||||||
// @ApiBody({ type: RequestOtpDto, description: 'Mobile number to receive OTP' })
|
|
||||||
// @ApiResponse({ status: 201, description: 'OTP requested successfully' })
|
|
||||||
// @ApiResponse({ status: 400, description: 'Invalid mobile number' })
|
|
||||||
// adminOtpRequest(@Body() dto: RequestOtpDto) {
|
|
||||||
// return this.authService.requestOtpAdmin(dto);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @Post('admin/otp/verify')
|
|
||||||
// @ApiOperation({ summary: 'Verify OTP code' })
|
|
||||||
// @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
|
|
||||||
// @ApiResponse({ status: 200, description: 'OTP verified successfully' })
|
|
||||||
// @ApiResponse({ status: 400, description: 'Invalid OTP or expired' })
|
|
||||||
// adminOtpVerify(@Body() dto: VerifyOtpDto) {
|
|
||||||
// return this.authService.verifyOtpAdmin(dto.phone, dto.otp); // assuming dto has `otp` property
|
|
||||||
// }
|
|
||||||
|
|
||||||
@RefreshTokenRateLimit()
|
@RefreshTokenRateLimit()
|
||||||
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
@ApiOperation({ summary: 'refresh the user access token / refresh token' })
|
||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
|
||||||
return this.authService.refreshToken(refreshTokenDto.refreshToken);
|
return this.authService.refreshToken(refreshTokenDto.refreshToken, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-7
@@ -1,7 +1,11 @@
|
|||||||
import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
|
import { Entity, Enum, Property } from '@mikro-orm/core';
|
||||||
|
|
||||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
|
|
||||||
|
export enum RefreshTokenType {
|
||||||
|
USER = 'user',
|
||||||
|
ADMIN = 'admin',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity({ tableName: 'refreshtokens' })
|
@Entity({ tableName: 'refreshtokens' })
|
||||||
export class RefreshToken extends BaseEntity {
|
export class RefreshToken extends BaseEntity {
|
||||||
@@ -11,12 +15,12 @@ export class RefreshToken extends BaseEntity {
|
|||||||
@Property()
|
@Property()
|
||||||
ownerId!: string;
|
ownerId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => Restaurant, { deleteRule: 'cascade' })
|
@Property()
|
||||||
restaurant!: Restaurant;
|
restId!: string;
|
||||||
|
|
||||||
|
@Enum(() => RefreshTokenType)
|
||||||
|
type!: RefreshTokenType;
|
||||||
|
|
||||||
@Property({ type: 'timestamptz' })
|
@Property({ type: 'timestamptz' })
|
||||||
expiresAt!: Date;
|
expiresAt!: Date;
|
||||||
|
|
||||||
@Property({ default: false })
|
|
||||||
isRevoked: boolean & Opt;
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable, BadRequestException, NotFoundException } from '@nestjs/common';
|
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||||
import { RequestOtpDto } from '../dto/request-otp.dto';
|
import { RequestOtpDto } from '../dto/request-otp.dto';
|
||||||
import { CacheService } from '../../utils/cache.service';
|
import { CacheService } from '../../utils/cache.service';
|
||||||
import { SmsService } from '../../utils/sms.service';
|
import { SmsService } from '../../utils/sms.service';
|
||||||
@@ -10,11 +10,12 @@ import { AuthMessage, RestMessage } from 'src/common/enums/message.enum';
|
|||||||
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
import { AdminRepository } from 'src/modules/admin/repositories/admin.repository';
|
||||||
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
|
import { AdminLoginTransformer } from '../transformers/admin-login.transformer';
|
||||||
import { UserLoginTransformer } from '../transformers/user-login.transformer';
|
import { UserLoginTransformer } from '../transformers/user-login.transformer';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
|
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
|
||||||
|
private OTP_EXPIRATION_TIME: number;
|
||||||
constructor(
|
constructor(
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService,
|
||||||
private readonly smsService: SmsService,
|
private readonly smsService: SmsService,
|
||||||
@@ -22,42 +23,35 @@ export class AuthService {
|
|||||||
private readonly tokensService: TokensService,
|
private readonly tokensService: TokensService,
|
||||||
private readonly restRepository: RestRepository,
|
private readonly restRepository: RestRepository,
|
||||||
private readonly adminRepository: AdminRepository,
|
private readonly adminRepository: AdminRepository,
|
||||||
) {}
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.OTP_EXPIRATION_TIME = this.configService.getOrThrow<number>('OTP_EXPIRATION_TIME');
|
||||||
|
}
|
||||||
|
private userOtpKey(restaurantSlug: string, phone: string) {
|
||||||
|
return `otp:${restaurantSlug}:${phone}`;
|
||||||
|
}
|
||||||
|
private adminOtpKey(restaurantSlug: string, phone: string) {
|
||||||
|
return `otp-admin:${restaurantSlug}:${phone}`;
|
||||||
|
}
|
||||||
|
|
||||||
async requestOtp(dto: RequestOtpDto) {
|
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||||
const { phone, slug } = dto;
|
const { phone, slug } = dto;
|
||||||
const code = this.generateOtpCode();
|
const code = this.generateOtpCode();
|
||||||
|
const key = isAdmin ? this.adminOtpKey(slug, phone) : this.userOtpKey(slug, phone);
|
||||||
await this.cacheService.set(`otp:${slug}:${phone}`, code, 160);
|
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
|
||||||
|
|
||||||
// await this.smsService.sendOtp(phone, code);
|
// await this.smsService.sendOtp(phone, code);
|
||||||
|
|
||||||
return { code };
|
return { code };
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestOtpAdmin(dto: RequestOtpDto) {
|
|
||||||
const { phone, slug } = dto;
|
|
||||||
|
|
||||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug);
|
|
||||||
if (!admin) {
|
|
||||||
throw new NotFoundException(AuthMessage.ADMIN_NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
const code = this.generateOtpCode();
|
|
||||||
|
|
||||||
await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160);
|
|
||||||
|
|
||||||
// await this.smsService.sendOtp(phone, code);
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
async verifyOtp(phone: string, slug: string, code: string) {
|
async verifyOtp(phone: string, slug: string, code: string) {
|
||||||
const cachedCode = await this.cacheService.get(`otp:${slug}:${phone}`);
|
const key = this.userOtpKey(slug, phone);
|
||||||
|
const cachedCode = await this.cacheService.get(key);
|
||||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||||
|
|
||||||
await this.cacheService.del(`otp:${slug}:${phone}`);
|
await this.cacheService.del(key);
|
||||||
|
|
||||||
const user = await this.userService.findOrCreateByPhone(phone);
|
const user = await this.userService.findOrCreateByPhone(phone);
|
||||||
|
|
||||||
@@ -67,7 +61,7 @@ export class AuthService {
|
|||||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await this.tokensService.generateTokens(user.id, rest);
|
const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false);
|
||||||
|
|
||||||
const userResponse = UserLoginTransformer.transform(user, rest);
|
const userResponse = UserLoginTransformer.transform(user, rest);
|
||||||
|
|
||||||
@@ -75,18 +69,18 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
async verifyOtpAdmin(phone: string, slug: string, code: string) {
|
||||||
const cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`);
|
const key = this.adminOtpKey(slug, phone);
|
||||||
|
const cachedCode = await this.cacheService.get(key);
|
||||||
|
|
||||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||||
|
|
||||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||||
|
await this.cacheService.del(key);
|
||||||
await this.cacheService.del(`otp-admin:${slug}:${phone}`);
|
|
||||||
|
|
||||||
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug);
|
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(phone, slug);
|
||||||
|
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
throw new BadRequestException(AuthMessage.USER_NOT_FOUND);
|
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rest = await this.restRepository.findOne({ slug });
|
const rest = await this.restRepository.findOne({ slug });
|
||||||
@@ -95,7 +89,7 @@ export class AuthService {
|
|||||||
throw new BadRequestException(RestMessage.NOT_FOUND);
|
throw new BadRequestException(RestMessage.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tokens = await this.tokensService.generateTokensAdmin(admin.id, rest);
|
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true);
|
||||||
|
|
||||||
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
|
||||||
|
|
||||||
@@ -107,25 +101,7 @@ export class AuthService {
|
|||||||
return code.toString();
|
return code.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshToken(oldRefreshToken: string) {
|
refreshToken(oldRefreshToken: string, isAdmin: boolean) {
|
||||||
return this.tokensService.refreshToken(oldRefreshToken);
|
return this.tokensService.refreshToken(oldRefreshToken, isAdmin);
|
||||||
}
|
|
||||||
|
|
||||||
refreshTokenAdmin(oldRefreshToken: string) {
|
|
||||||
return this.tokensService.refreshTokenAdmin(oldRefreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
// async getAdminPerms(adminId: string, restId: string) {
|
|
||||||
// const cachedPerms = await this.cacheService.get(`${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`);
|
|
||||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
||||||
// return cachedPerms ? JSON.parse(cachedPerms) : [];
|
|
||||||
// }
|
|
||||||
|
|
||||||
async setAdminPerms(adminId: string, restId: string, permissions: string) {
|
|
||||||
return this.cacheService.set(
|
|
||||||
`${this.ADMIN_PERMISSIONS_KEY}:${adminId}:${restId}`,
|
|
||||||
JSON.stringify(permissions),
|
|
||||||
3600,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
import { EntityManager } from '@mikro-orm/postgresql';
|
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 { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { AuthMessage, UserMessage } from '../../../common/enums/message.enum';
|
import { AuthMessage } from '../../../common/enums/message.enum';
|
||||||
import { RefreshToken } from '../../users/entities/refresh-token.entity';
|
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity';
|
||||||
import { User } from '../../users/entities/user.entity';
|
|
||||||
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
|
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()
|
@Injectable()
|
||||||
export class TokensService {
|
export class TokensService {
|
||||||
@@ -22,286 +16,68 @@ export class TokensService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly restRepository: RestRepository,
|
|
||||||
private readonly cacheService: CacheService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateTokens(ownerId: string, rest: Restaurant, em?: EntityManager) {
|
async generateAccessAndRefreshToken(ownerId: string, restaurantId: string, isAdmin: boolean) {
|
||||||
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');
|
|
||||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||||
|
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
|
||||||
|
|
||||||
const tokenPayload: ITokenPayload = {
|
const payload: ITokenPayload | IAdminTokenPayload = isAdmin
|
||||||
userId: payload.userId,
|
? { adminId: ownerId, restId: restaurantId }
|
||||||
restId: payload.restId,
|
: { userId: ownerId, restId: restaurantId };
|
||||||
};
|
|
||||||
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
|
const accessToken = await this.generateAccessToken(payload, accessExpire);
|
||||||
const refreshToken = this.generateRefreshToken();
|
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)
|
await this.storeRefreshToken(ownerId, restaurantId, refreshToken, type);
|
||||||
const skipValidation = !!em && em.isInTransaction();
|
|
||||||
await this.storeRefreshToken(payload.userId, payload.restId, refreshToken, em, skipValidation);
|
|
||||||
|
|
||||||
return {
|
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() },
|
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateAccessAndRefreshTokenAdmin(payload: IAdminTokenPayload, em?: EntityManager) {
|
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expire: number) {
|
||||||
const accessExpire = this.configService.getOrThrow<number>('ACCESS_TOKEN_EXPIRE');
|
return this.jwtService.signAsync(payload, { expiresIn: `${expire}m` });
|
||||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
|
||||||
|
|
||||||
const tokenPayload: IAdminTokenPayload = {
|
|
||||||
adminId: payload.adminId,
|
|
||||||
restId: payload.restId,
|
|
||||||
};
|
|
||||||
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
|
|
||||||
const refreshToken = this.generateRefreshToken();
|
|
||||||
|
|
||||||
// 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() },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async storeRefreshToken(
|
|
||||||
userId: string,
|
|
||||||
restId: string,
|
|
||||||
refreshToken: string,
|
|
||||||
em?: EntityManager,
|
|
||||||
skipValidation = false,
|
|
||||||
) {
|
|
||||||
const entityManager = em || this.em.fork();
|
|
||||||
const isExternalTransaction = !!em && em.isInTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Only start transaction if no external EntityManager with transaction is provided
|
|
||||||
if (!isExternalTransaction && !entityManager.isInTransaction()) {
|
|
||||||
await entityManager.begin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async storeRefreshToken(ownerId: string, restId: string, refreshToken: string, type: RefreshTokenType) {
|
||||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
||||||
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
|
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
|
||||||
|
|
||||||
// Skip User validation when refreshing (user is already validated via refresh token)
|
const token = this.em.create(RefreshToken, {
|
||||||
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,
|
token: refreshToken,
|
||||||
restaurant,
|
ownerId,
|
||||||
ownerId: userId,
|
restId,
|
||||||
|
type,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use persist instead of persistAndFlush when in transaction
|
return this.em.persistAndFlush(token);
|
||||||
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 storeRefreshTokenAdmin(
|
async refreshToken(oldRefreshToken: string, isAdmin: boolean) {
|
||||||
adminId: string,
|
const token = await this.em.findOne(RefreshToken, { token: oldRefreshToken });
|
||||||
restId: string,
|
|
||||||
refreshToken: string,
|
|
||||||
em?: EntityManager,
|
|
||||||
skipValidation = false,
|
|
||||||
) {
|
|
||||||
const entityManager = em || this.em.fork();
|
|
||||||
const isExternalTransaction = !!em && em.isInTransaction();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Only start transaction if no external EntityManager with transaction is provided
|
|
||||||
if (!isExternalTransaction && !entityManager.isInTransaction()) {
|
|
||||||
await entityManager.begin();
|
|
||||||
}
|
|
||||||
|
|
||||||
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
|
|
||||||
const expiresAt = dayjs().add(refreshExpire, 'day').toDate();
|
|
||||||
|
|
||||||
// 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);
|
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())) {
|
if (dayjs(token.expiresAt).isBefore(dayjs())) {
|
||||||
entityManager.remove(token);
|
this.em.remove(token);
|
||||||
await entityManager.flush();
|
await this.em.flush();
|
||||||
await entityManager.commit();
|
|
||||||
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the old token
|
// Remove the old token
|
||||||
entityManager.remove(token);
|
this.em.remove(token);
|
||||||
|
|
||||||
// Generate new tokens within the same transaction
|
// Generate new tokens within the same transaction
|
||||||
const tokens = await this.generateTokens(token.ownerId, token.restaurant, entityManager);
|
const tokens = await this.generateAccessAndRefreshToken(token.ownerId, token.restId, isAdmin);
|
||||||
|
|
||||||
// Commit the transaction
|
// Commit the transaction
|
||||||
await entityManager.flush();
|
await this.em.flush();
|
||||||
await entityManager.commit();
|
|
||||||
|
|
||||||
return tokens;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateRefreshToken() {
|
private generateRefreshToken() {
|
||||||
|
|||||||
@@ -7,12 +7,11 @@ import { User } from './entities/user.entity';
|
|||||||
import { UserAddress } from './entities/user-address.entity';
|
import { UserAddress } from './entities/user-address.entity';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { UserRepository } from './repositories/user.repository';
|
import { UserRepository } from './repositories/user.repository';
|
||||||
import { RefreshToken } from './entities/refresh-token.entity';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, UserRepository],
|
providers: [UserService, UserRepository],
|
||||||
controllers: [UsersController, PublicUserController],
|
controllers: [UsersController, PublicUserController],
|
||||||
imports: [MikroOrmModule.forFeature([User, UserAddress, RefreshToken]), JwtModule],
|
imports: [MikroOrmModule.forFeature([User, UserAddress]), JwtModule],
|
||||||
exports: [UserService, UserRepository],
|
exports: [UserService, UserRepository],
|
||||||
})
|
})
|
||||||
export class UserModule {}
|
export class UserModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user