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 { 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' })
+3
View File
@@ -21,6 +21,9 @@ export class AdminService {
async findById(id: string): Promise<Admin | null> {
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 }) {
const { phone, firstName, lastName, roleId, restId } = data;
+6 -1
View File
@@ -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;
}
+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 { 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;
@@ -9,6 +9,10 @@ export class AdminRepository extends EntityRepository<Admin> {
}
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) {
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}`);
+52 -7
View File
@@ -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<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,
});
@@ -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();
@@ -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;
+1 -5
View File
@@ -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<RefreshToken>(this);
@Property()
firstName!: string;
+2 -1
View File
@@ -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 {}