diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index de7e9a0..3e2b1e0 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common'; +import { Controller, Post, Body, HttpCode, HttpStatus, Get } from '@nestjs/common'; import { AdminService } from './admin.service'; import { CreateAdminDto } from './dto/create-admin.dto'; import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; @@ -8,6 +8,14 @@ import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; export class AdminController { constructor(private readonly adminService: AdminService) {} + @Get() + @ApiOperation({ summary: 'admin' }) + @ApiResponse({ status: 201, description: 'Admins' }) + async getAll() { + const admin = await this.adminService.findAll(); + return admin; + } + @Post() @HttpCode(HttpStatus.CREATED) @ApiOperation({ summary: 'Create a new admin' }) diff --git a/src/modules/admin/admin.service.ts b/src/modules/admin/admin.service.ts index 63820fe..50ced73 100644 --- a/src/modules/admin/admin.service.ts +++ b/src/modules/admin/admin.service.ts @@ -21,6 +21,9 @@ export class AdminService { async findById(id: string): Promise { return this.adminRepository.findOne({ id }); } + async findAll(): Promise { + return this.adminRepository.findAll(); + } async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) { const { phone, firstName, lastName, roleId, restId } = data; diff --git a/src/modules/admin/dto/find-roles.dto.ts b/src/modules/admin/dto/find-roles.dto.ts index 19518cd..2088ca7 100644 --- a/src/modules/admin/dto/find-roles.dto.ts +++ b/src/modules/admin/dto/find-roles.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString, IsNumber } from 'class-validator'; +import { IsOptional, IsString, IsNumber, Min } from 'class-validator'; +import { Type } from 'class-transformer'; export class FindRolesDto { @ApiProperty({ description: 'Search by role name', required: false }) @@ -14,11 +15,15 @@ export class FindRolesDto { @ApiProperty({ description: 'Page number', required: false, default: 1 }) @IsOptional() + @Type(() => Number) @IsNumber() + @Min(1) page?: number; @ApiProperty({ description: 'Items per page', required: false, default: 20 }) @IsOptional() + @Type(() => Number) @IsNumber() + @Min(1) limit?: number; } diff --git a/src/modules/admin/entities/admin.entity.ts b/src/modules/admin/entities/admin.entity.ts index 1ad145a..8173c81 100644 --- a/src/modules/admin/entities/admin.entity.ts +++ b/src/modules/admin/entities/admin.entity.ts @@ -1,11 +1,11 @@ -import { Entity, ManyToOne, OneToOne, Property, Unique } from '@mikro-orm/core'; +import { Entity, ManyToOne, OneToOne, Property } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; import { Role } from './role.entity'; @Entity({ tableName: 'admins' }) // Add the Unique constraint for the combination of 'phone' and 'restaurant' -@Unique({ properties: ['phone', 'restaurant'] }) +// @Unique({ properties: ['phone', 'restaurant'] }) export class Admin extends BaseEntity { @Property({ nullable: true }) firstName?: string; diff --git a/src/modules/admin/repositories/rest.repository.ts b/src/modules/admin/repositories/rest.repository.ts index a95a00b..c9b4acd 100644 --- a/src/modules/admin/repositories/rest.repository.ts +++ b/src/modules/admin/repositories/rest.repository.ts @@ -9,6 +9,10 @@ export class AdminRepository extends EntityRepository { } async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise { - return this.findOne({ phone, restaurant: { slug } }, { populate: ['restaurant', 'role', 'role.permissions'] }); + return this.em.findOne( + Admin, + { phone, restaurant: { slug } }, + { populate: ['restaurant', 'role', 'role.permissions'] }, + ); } } diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index b4ba414..1875ba0 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -36,7 +36,7 @@ export class AuthService { async requestOtpAdmin(dto: RequestOtpDto) { const { phone, slug } = dto; - const admin = await this.adminRepository.find({ phone }); + const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug); if (!admin) { throw new NotFoundException(); } @@ -45,9 +45,9 @@ export class AuthService { await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160); - await this.smsService.sendOtp(phone, code); + // await this.smsService.sendOtp(phone, code); - return true; + return code; } async verifyOtp(phone: string, slug: string, code: string) { @@ -65,14 +65,16 @@ export class AuthService { throw new BadRequestException(RestMessage.NOT_FOUND); } - const tokens = await this.tokensService.generateTokens(user, rest); + const tokens = await this.tokensService.generateTokens(user.id, rest); return { tokens, user }; } async verifyOtpAdmin(phone: string, slug: string, code: string) { const cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`); + if (!cachedCode) throw new BadRequestException('OTP expired or not found'); + if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); await this.cacheService.del(`otp-admin:${slug}:${phone}`); diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index 6f7979d..df79090 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -26,10 +26,10 @@ export class TokensService { private readonly cacheService: CacheService, ) {} - async generateTokens(user: User, rest: Restaurant, em?: EntityManager) { + async generateTokens(ownerId: string, rest: Restaurant, em?: EntityManager) { return this.generateAccessAndRefreshToken( { - userId: user.id, + userId: ownerId, restId: rest.id, }, em, @@ -76,7 +76,7 @@ export class TokensService { const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` }); const refreshToken = this.generateRefreshToken(); - await this.storeRefreshToken(payload.adminId, payload.restId, refreshToken, em); + await this.storeRefreshTokenAdmin(payload.adminId, payload.restId, refreshToken, em); return { accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() }, refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() }, @@ -105,7 +105,52 @@ export class TokensService { const token = entityManager.create(RefreshToken, { token: refreshToken, restaurant, - user, + 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; + } + } + + async storeRefreshTokenAdmin(adminId: string, restId: string, refreshToken: string, em?: EntityManager) { + 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('REFRESH_TOKEN_EXPIRE'); + const expiresAt = dayjs().add(refreshExpire, 'day').toDate(); + + 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, }); @@ -138,7 +183,7 @@ export class TokensService { RefreshToken, { token: oldRefreshToken }, { - populate: ['user', 'restaurant'], + populate: ['restaurant'], }, ); @@ -158,7 +203,7 @@ export class TokensService { entityManager.remove(token); // Generate new tokens within the same transaction - const tokens = await this.generateTokens(token.user, token.restaurant, entityManager); + const tokens = await this.generateTokens(token.ownerId, token.restaurant, entityManager); // Commit the transaction await entityManager.flush(); @@ -180,7 +225,7 @@ export class TokensService { try { await entityManager.begin(); - await entityManager.nativeDelete(RefreshToken, { user: { id: userId } }); + await entityManager.nativeDelete(RefreshToken, { ownerId: userId }); this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`); await entityManager.commit(); diff --git a/src/modules/users/entities/refresh-token.entity.ts b/src/modules/users/entities/refresh-token.entity.ts index b246d9f..fe49646 100644 --- a/src/modules/users/entities/refresh-token.entity.ts +++ b/src/modules/users/entities/refresh-token.entity.ts @@ -1,6 +1,5 @@ import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core'; -import { User } from './user.entity'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; @@ -9,8 +8,8 @@ export class RefreshToken extends BaseEntity { @Property({ type: 'varchar', length: 255 }) token!: string; - @ManyToOne(() => User, { deleteRule: 'cascade' }) - user!: User; + @Property() + ownerId!: string; @ManyToOne(() => Restaurant, { deleteRule: 'cascade' }) restaurant!: Restaurant; diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index 5256907..d949c71 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -1,5 +1,4 @@ -import { Entity, Property, OneToMany, Collection, OneToOne } from '@mikro-orm/core'; -import { RefreshToken } from './refresh-token.entity'; +import { Entity, Property, OneToOne } from '@mikro-orm/core'; import { BaseEntity } from '../../../common/entities/base.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity'; @@ -8,9 +7,6 @@ export class User extends BaseEntity { @OneToOne(() => Restaurant, { owner: true }) restaurant!: Restaurant; - @OneToMany(() => RefreshToken, token => token.user) - refreshTokens = new Collection(this); - @Property() firstName!: string; diff --git a/src/modules/users/user.module.ts b/src/modules/users/user.module.ts index 6831bf0..79573cf 100644 --- a/src/modules/users/user.module.ts +++ b/src/modules/users/user.module.ts @@ -5,11 +5,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs'; import { User } from './entities/user.entity'; import { JwtModule } from '@nestjs/jwt'; import { UserRepository } from './repositories/user.repository'; +import { RefreshToken } from './entities/refresh-token.entity'; @Module({ providers: [UserService, UserRepository], controllers: [AdminUserController], - imports: [MikroOrmModule.forFeature([User]), JwtModule], + imports: [MikroOrmModule.forFeature([User, RefreshToken]), JwtModule], exports: [UserService, UserRepository], }) export class UserModule {}