This commit is contained in:
2025-11-16 12:07:39 +03:30
parent d605303e52
commit b3c44d734c
10 changed files with 88 additions and 25 deletions
+9 -1
View File
@@ -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 { AdminService } from './admin.service';
import { CreateAdminDto } from './dto/create-admin.dto'; import { CreateAdminDto } from './dto/create-admin.dto';
import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
@@ -8,6 +8,14 @@ import { ApiTags, ApiOperation, ApiBody, ApiResponse } from '@nestjs/swagger';
export class AdminController { export class AdminController {
constructor(private readonly adminService: AdminService) {} 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() @Post()
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Create a new admin' }) @ApiOperation({ summary: 'Create a new admin' })
+3
View File
@@ -21,6 +21,9 @@ export class AdminService {
async findById(id: string): Promise<Admin | null> { async findById(id: string): Promise<Admin | null> {
return this.adminRepository.findOne({ id }); return this.adminRepository.findOne({ id });
} }
async findAll(): Promise<Admin[]> {
return this.adminRepository.findAll();
}
async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) { async create(data: { phone: string; firstName?: string; lastName?: string; roleId: string; restId: string }) {
const { phone, firstName, lastName, roleId, restId } = data; const { phone, firstName, lastName, roleId, restId } = data;
+6 -1
View File
@@ -1,5 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; 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 { export class FindRolesDto {
@ApiProperty({ description: 'Search by role name', required: false }) @ApiProperty({ description: 'Search by role name', required: false })
@@ -14,11 +15,15 @@ export class FindRolesDto {
@ApiProperty({ description: 'Page number', required: false, default: 1 }) @ApiProperty({ description: 'Page number', required: false, default: 1 })
@IsOptional() @IsOptional()
@Type(() => Number)
@IsNumber() @IsNumber()
@Min(1)
page?: number; page?: number;
@ApiProperty({ description: 'Items per page', required: false, default: 20 }) @ApiProperty({ description: 'Items per page', required: false, default: 20 })
@IsOptional() @IsOptional()
@Type(() => Number)
@IsNumber() @IsNumber()
@Min(1)
limit?: number; limit?: number;
} }
+2 -2
View File
@@ -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 { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
import { Role } from './role.entity'; import { Role } from './role.entity';
@Entity({ tableName: 'admins' }) @Entity({ tableName: 'admins' })
// Add the Unique constraint for the combination of 'phone' and 'restaurant' // Add the Unique constraint for the combination of 'phone' and 'restaurant'
@Unique({ properties: ['phone', 'restaurant'] }) // @Unique({ properties: ['phone', 'restaurant'] })
export class Admin extends BaseEntity { export class Admin extends BaseEntity {
@Property({ nullable: true }) @Property({ nullable: true })
firstName?: string; firstName?: string;
@@ -9,6 +9,10 @@ export class AdminRepository extends EntityRepository<Admin> {
} }
async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> { async findByPhoneAndrestaurantSlug(phone: string, slug: string): Promise<Admin | null> {
return this.findOne({ phone, restaurant: { slug } }, { populate: ['restaurant', 'role', 'role.permissions'] }); return this.em.findOne(
Admin,
{ phone, restaurant: { slug } },
{ populate: ['restaurant', 'role', 'role.permissions'] },
);
} }
} }
+6 -4
View File
@@ -36,7 +36,7 @@ export class AuthService {
async requestOtpAdmin(dto: RequestOtpDto) { async requestOtpAdmin(dto: RequestOtpDto) {
const { phone, slug } = dto; const { phone, slug } = dto;
const admin = await this.adminRepository.find({ phone }); const admin = await this.adminRepository.findByPhoneAndrestaurantSlug(phone, slug);
if (!admin) { if (!admin) {
throw new NotFoundException(); throw new NotFoundException();
} }
@@ -45,9 +45,9 @@ export class AuthService {
await this.cacheService.set(`otp-admin:${slug}:${phone}`, code, 160); 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) { async verifyOtp(phone: string, slug: string, code: string) {
@@ -65,14 +65,16 @@ export class AuthService {
throw new BadRequestException(RestMessage.NOT_FOUND); 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 }; return { tokens, user };
} }
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 cachedCode = await this.cacheService.get(`otp-admin:${slug}:${phone}`);
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-admin:${slug}:${phone}`); await this.cacheService.del(`otp-admin:${slug}:${phone}`);
+52 -7
View File
@@ -26,10 +26,10 @@ export class TokensService {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
) {} ) {}
async generateTokens(user: User, rest: Restaurant, em?: EntityManager) { async generateTokens(ownerId: string, rest: Restaurant, em?: EntityManager) {
return this.generateAccessAndRefreshToken( return this.generateAccessAndRefreshToken(
{ {
userId: user.id, userId: ownerId,
restId: rest.id, restId: rest.id,
}, },
em, em,
@@ -76,7 +76,7 @@ export class TokensService {
const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` }); const accessToken = await this.jwtService.signAsync(tokenPayload, { expiresIn: `${accessExpire}m` });
const refreshToken = this.generateRefreshToken(); const refreshToken = this.generateRefreshToken();
await this.storeRefreshToken(payload.adminId, payload.restId, refreshToken, em); await this.storeRefreshTokenAdmin(payload.adminId, payload.restId, refreshToken, em);
return { return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() }, accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'minute').valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() }, refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, 'day').valueOf() },
@@ -105,7 +105,52 @@ export class TokensService {
const token = entityManager.create(RefreshToken, { const token = entityManager.create(RefreshToken, {
token: refreshToken, token: refreshToken,
restaurant, 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<number>('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, expiresAt,
}); });
@@ -138,7 +183,7 @@ export class TokensService {
RefreshToken, RefreshToken,
{ token: oldRefreshToken }, { token: oldRefreshToken },
{ {
populate: ['user', 'restaurant'], populate: ['restaurant'],
}, },
); );
@@ -158,7 +203,7 @@ export class TokensService {
entityManager.remove(token); entityManager.remove(token);
// Generate new tokens within the same transaction // 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 // Commit the transaction
await entityManager.flush(); await entityManager.flush();
@@ -180,7 +225,7 @@ export class TokensService {
try { try {
await entityManager.begin(); 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}`); this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
await entityManager.commit(); await entityManager.commit();
@@ -1,6 +1,5 @@
import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core'; import { Entity, ManyToOne, Opt, Property } from '@mikro-orm/core';
import { User } from './user.entity';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity'; import { Restaurant } from '../../../modules/restaurants/entities/restaurant.entity';
@@ -9,8 +8,8 @@ export class RefreshToken extends BaseEntity {
@Property({ type: 'varchar', length: 255 }) @Property({ type: 'varchar', length: 255 })
token!: string; token!: string;
@ManyToOne(() => User, { deleteRule: 'cascade' }) @Property()
user!: User; ownerId!: string;
@ManyToOne(() => Restaurant, { deleteRule: 'cascade' }) @ManyToOne(() => Restaurant, { deleteRule: 'cascade' })
restaurant!: Restaurant; restaurant!: Restaurant;
+1 -5
View File
@@ -1,5 +1,4 @@
import { Entity, Property, OneToMany, Collection, OneToOne } from '@mikro-orm/core'; import { Entity, Property, OneToOne } from '@mikro-orm/core';
import { RefreshToken } from './refresh-token.entity';
import { BaseEntity } from '../../../common/entities/base.entity'; import { BaseEntity } from '../../../common/entities/base.entity';
import { Restaurant } from '../../restaurants/entities/restaurant.entity'; import { Restaurant } from '../../restaurants/entities/restaurant.entity';
@@ -8,9 +7,6 @@ export class User extends BaseEntity {
@OneToOne(() => Restaurant, { owner: true }) @OneToOne(() => Restaurant, { owner: true })
restaurant!: Restaurant; restaurant!: Restaurant;
@OneToMany(() => RefreshToken, token => token.user)
refreshTokens = new Collection<RefreshToken>(this);
@Property() @Property()
firstName!: string; firstName!: string;
+2 -1
View File
@@ -5,11 +5,12 @@ import { MikroOrmModule } from '@mikro-orm/nestjs';
import { User } from './entities/user.entity'; import { User } from './entities/user.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: [AdminUserController], controllers: [AdminUserController],
imports: [MikroOrmModule.forFeature([User]), JwtModule], imports: [MikroOrmModule.forFeature([User, RefreshToken]), JwtModule],
exports: [UserService, UserRepository], exports: [UserService, UserRepository],
}) })
export class UserModule {} export class UserModule {}