This commit is contained in:
2026-01-14 00:05:13 +03:30
parent d046dc21ac
commit 1075dbc44f
12 changed files with 106 additions and 296 deletions
+3 -5
View File
@@ -1,4 +1,4 @@
import { Module, forwardRef } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { AdminService } from './providers/admin.service'; import { AdminService } from './providers/admin.service';
import { MikroOrmModule } from '@mikro-orm/nestjs'; import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Admin } from './entities/admin.entity'; import { Admin } from './entities/admin.entity';
@@ -9,17 +9,15 @@ import { Permission } from '../roles/entities/permission.entity';
import { RolePermission } from '../roles/entities/rolePermission.entity'; import { RolePermission } from '../roles/entities/rolePermission.entity';
import { AdminController } from './controllers/admin.controller'; import { AdminController } from './controllers/admin.controller';
import { UtilsModule } from '../util/utils.module'; import { UtilsModule } from '../util/utils.module';
import { RestaurantsModule } from '../restaurants/restaurants.module';
import { AdminRole } from './entities/adminRole.entity';
@Module({ @Module({
providers: [AdminService, AdminRepository], providers: [AdminService, AdminRepository],
controllers: [AdminController], controllers: [AdminController],
imports: [ imports: [
MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission, AdminRole]), MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission]),
JwtModule, JwtModule,
UtilsModule, UtilsModule,
forwardRef(() => RestaurantsModule),
], ],
exports: [AdminService, AdminRepository], exports: [AdminService, AdminRepository],
}) })
+2 -2
View File
@@ -6,7 +6,7 @@ import { UpdateAdminDto } from '../dto/update-admin.dto';
import { normalizePhone } from '../../util/phone.util'; import { normalizePhone } from '../../util/phone.util';
import { CreateAdminDto } from '../dto/create-admin.dto'; import { CreateAdminDto } from '../dto/create-admin.dto';
import { AdminRepository } from '../repositories/admin.repository'; import { AdminRepository } from '../repositories/admin.repository';
import { RoleRepository } from '../repositories/role.repository'; import { RoleRepository } from 'src/modules/roles/respository/role.repository';
@Injectable() @Injectable()
export class AdminService { export class AdminService {
@@ -60,7 +60,7 @@ export class AdminService {
} }
async findAll(): Promise<Admin[]> { async findAll(): Promise<Admin[]> {
return this.adminRepository.find({}, { populate: ['role', 'roles.role'] }); return this.adminRepository.find({}, { populate: ['role'] });
} }
async update(adminId: string, dto: UpdateAdminDto): Promise<Admin> { async update(adminId: string, dto: UpdateAdminDto): Promise<Admin> {
@@ -14,7 +14,7 @@ export class AdminRepository extends EntityRepository<Admin> {
const admins = await this.em.find( const admins = await this.em.find(
Admin, Admin,
{ role: { permissions: { name: permission } } }, { role: { permissions: { name: permission } } },
{ populate: ['role', 'roles.permissions'] }, { populate: ['role', 'role.permissions'] },
); );
return admins; return admins;
} }
@@ -1,50 +0,0 @@
import type { Admin } from '../entities/admin.entity';
export interface AdminDetailResponse {
id: string;
firstName?: string;
lastName?: string;
phone: string;
role: {
id: string;
name: string;
};
permissions: string[];
restaurant?: {
id: string;
name: string;
slug: string;
};
}
export class AdminDetailTransformer {
static transform(admin: Admin): AdminDetailResponse | null {
if (!admin) {
return null;
}
// Extract role information
const role = admin.roles.getItems().find(r => r.role)?.role;
if (!role) {
return null;
}
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: {
id: role.id,
name: role.name,
},
permissions: role.permissions.getItems().map(p => p.name) || [],
restaurant: {
id: role.restaurant?.id || '',
name: role.restaurant?.name || '',
slug: role.restaurant?.slug || '',
},
};
}
}
// Extract permissions - ensure collection is loaded if needed
@@ -1,13 +1,12 @@
import { Controller, Post, Body, UseGuards } from '@nestjs/common'; import { Controller, Post, Body, UseGuards } from '@nestjs/common';
import { AuthService } from '../services/auth.service'; import { AuthService } from '../services/auth.service';
import { RequestOtpDto } from '../dto/request-otp.dto'; import { RequestOtpDto } from '../dto/request-otp.dto';
import { DirectLoginDto } from '../dto/direct-login.dto';
import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger'; import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger';
import { VerifyOtpDto } from '../dto/verify-otp.dto'; import { VerifyOtpDto } from '../dto/verify-otp.dto';
import { Throttle } from '@nestjs/throttler'; import { Throttle } from '@nestjs/throttler';
import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator'; import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator';
import { RefreshTokenDto } from '../dto/refresh-token.dto'; import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard';
@ApiTags('auth') @ApiTags('auth')
@Controller() @Controller()
@@ -26,7 +25,7 @@ export class AuthController {
@ApiOperation({ summary: 'Verify OTP code' }) @ApiOperation({ summary: 'Verify OTP code' })
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' }) @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
otpVerify(@Body() dto: VerifyOtpDto) { otpVerify(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtp(dto.phone, dto.slug, dto.otp); // assuming dto has `otp` property return this.authService.verifyOtp(dto.phone, dto.otp);
} }
@RefreshTokenRateLimit() @RefreshTokenRateLimit()
@@ -48,7 +47,7 @@ export class AuthController {
@ApiOperation({ summary: 'Verify OTP code' }) @ApiOperation({ summary: 'Verify OTP code' })
@ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' }) @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' })
otpVerifyAdmin(@Body() dto: VerifyOtpDto) { otpVerifyAdmin(@Body() dto: VerifyOtpDto) {
return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp); return this.authService.verifyOtpAdmin(dto.phone, dto.otp);
} }
@RefreshTokenRateLimit() @RefreshTokenRateLimit()
@@ -58,14 +57,5 @@ export class AuthController {
return this.authService.refreshToken(refreshTokenDto.refreshToken); return this.authService.refreshToken(refreshTokenDto.refreshToken);
} }
//super admin routes
@UseGuards(SuperAdminAuthGuard)
@Post('super-admin/auth/direct-login')
@ApiOperation({ summary: 'Direct login for DSC - returns login credentials' })
@ApiBody({ type: DirectLoginDto, description: 'Phone number and restaurant slug for direct login' })
directloginForDsc(@Body() dto: DirectLoginDto) {
return this.authService.loginAdminForDsc(dto.phone, dto.slug);
}
} }
@@ -1,29 +1,19 @@
import { Entity, Enum, Index, Property } from '@mikro-orm/core'; import { Entity, Enum, Index, Property } from '@mikro-orm/core';
import { RefreshTokenType } from '../interfaces/IToken-payload';
import { BaseEntity } from '../../../common/entities/base.entity';
export enum RefreshTokenType {
USER = 'user',
ADMIN = 'admin',
}
@Entity({ tableName: 'refreshtokens' }) @Entity({ tableName: 'refreshtokens' })
@Index({ properties: ['ownerId', '', 'type'] })
@Index({ properties: ['hashedToken'] }) @Index({ properties: ['hashedToken'] })
@Index({ properties: ['expiresAt'] }) export class RefreshToken {
export class RefreshToken extends BaseEntity {
@Property({ type: 'varchar', length: 255 }) @Property({ type: 'varchar', length: 255 })
hashedToken!: string; hashedToken!: string;
@Property() @Property()
ownerId!: string; ownerId!: string;
@Property() @Enum(() => RefreshTokenType)
!: string;
@Enum(() => RefreshTokenType)
type!: RefreshTokenType; type!: RefreshTokenType;
@Property({ type: 'timestamptz' }) @Property({ type: 'timestamptz' })
expiresAt!: Date; expiresAt!: Date;
} }
@@ -1,10 +1,12 @@
export interface ITokenPayload { export interface ITokenPayload {
userId: string; userId: string;
: string;
slug: string;
} }
export interface IAdminTokenPayload { export interface IAdminTokenPayload {
adminId: string; adminId: string;
: string; }
export enum RefreshTokenType {
USER = 'user',
ADMIN = 'admin',
} }
+25 -54
View File
@@ -5,8 +5,7 @@ import { SmsService } from '../../notification/services/sms.service';
import { UserService } from '../../user/providers/user.service'; import { UserService } from '../../user/providers/user.service';
import { randomInt } from 'crypto'; import { randomInt } from 'crypto';
import { TokensService } from './tokens.service'; import { TokensService } from './tokens.service';
import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; import { AuthMessage } from 'src/common/enums/message.enum';
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';
@@ -17,31 +16,32 @@ import { normalizePhone } from '../../util/phone.util';
export class AuthService { export class AuthService {
readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; readonly ADMIN_PERMISSIONS_KEY = 'admin-perms';
private OTP_EXPIRATION_TIME: number; private OTP_EXPIRATION_TIME: number;
constructor( constructor(
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly smsService: SmsService, private readonly smsService: SmsService,
private readonly userService: UserService, private readonly userService: UserService,
private readonly tokensService: TokensService, private readonly tokensService: TokensService,
private readonly restRepository: RestRepository,
private readonly adminRepository: AdminRepository, private readonly adminRepository: AdminRepository,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) { ) {
this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240; this.OTP_EXPIRATION_TIME = this.configService.get<number>('OTP_EXPIRATION_TIME') ?? 240;
} }
private userOtpKey(restaurantSlug: string, phone: string) { private userOtpKey(phone: string) {
return `otp:${restaurantSlug}:${phone}`; return `otp:${phone}`;
} }
private adminOtpKey(restaurantSlug: string, phone: string) { private adminOtpKey(phone: string) {
return `otp-admin:${restaurantSlug}:${phone}`; return `otp-admin:${phone}`;
} }
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) { async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
const { phone, slug } = dto; const { phone } = dto;
const normalizedPhone = normalizePhone(phone); const normalizedPhone = normalizePhone(phone);
const code = this.generateOtpCode(); const code = this.generateOtpCode();
const key = isAdmin ? this.adminOtpKey(slug, normalizedPhone) : this.userOtpKey(slug, normalizedPhone); const key = isAdmin ? this.adminOtpKey(normalizedPhone) : this.userOtpKey(normalizedPhone);
await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME); await this.cacheService.set(key, code, this.OTP_EXPIRATION_TIME);
await this.smsService.sendotp(normalizedPhone, code); await this.smsService.sendotp(normalizedPhone, code);
@@ -49,33 +49,34 @@ export class AuthService {
return { code: null }; return { code: null };
} }
async verifyOtp(phone: string, slug: string, code: string) { async verifyOtp(phone: string, code: string) {
const normalizedPhone = normalizePhone(phone); const normalizedPhone = normalizePhone(phone);
const key = this.userOtpKey(slug, normalizedPhone); const key = this.userOtpKey(normalizedPhone);
const cachedCode = await this.cacheService.get(key); 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(key);
const user = await this.userService.findOrCreateByPhone(normalizedPhone); const user = await this.userService.getOrCreate({
firstName: '[کاربر]',
lastName: '',
phone: normalizedPhone,
gender: true,
maxCredit: 0
});
const rest = await this.restRepository.findOne({ slug });
if (!rest) { const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, false);
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug); const userResponse = UserLoginTransformer.transform(user);
const userResponse = UserLoginTransformer.transform(user, rest);
return { tokens, user: userResponse }; return { tokens, user: userResponse };
} }
async verifyOtpAdmin(phone: string, slug: string, code: string) { async verifyOtpAdmin(phone: string, code: string) {
const normalizedPhone = normalizePhone(phone); const normalizedPhone = normalizePhone(phone);
const key = this.adminOtpKey(slug, normalizedPhone); const key = this.adminOtpKey(normalizedPhone);
const cachedCode = await this.cacheService.get(key); 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');
@@ -83,45 +84,15 @@ export class AuthService {
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(key);
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug); const admin = await this.adminRepository.findOne({ phone: normalizedPhone });
if (!admin) { if (!admin) {
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND); throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
} }
const rest = await this.restRepository.findOne({ slug }); const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, true);
if (!rest) { const adminResponse = await AdminLoginTransformer.transform(admin);
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
return { tokens, admin: adminResponse };
}
/**
*
to use for login directly from DSC
*/
async loginAdminForDsc(phone: string, slug: string) {
const normalizedPhone = normalizePhone(phone);
const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug);
if (!admin) {
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
}
const rest = await this.restRepository.findOne({ slug });
if (!rest) {
throw new BadRequestException(RestMessage.NOT_FOUND);
}
const tokens = await this.tokensService.generateAccessAndRefreshToken(admin.id, rest.id, true, slug);
const adminResponse = await AdminLoginTransformer.transform(admin, rest);
return { tokens, admin: adminResponse }; return { tokens, admin: adminResponse };
} }
+52 -65
View File
@@ -1,18 +1,18 @@
import { createHash, randomBytes } from 'node:crypto'; import { createHash, randomBytes } from 'node:crypto';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
import { LockMode } from '@mikro-orm/core';
import { 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 { RefreshTokenType } from '../interfaces/IToken-payload';
import { AuthMessage } from '../../../common/enums/message.enum'; import { AuthMessage } from '../../../common/enums/message.enum';
import { RefreshToken, RefreshTokenType } from '../entities/refresh-token.entity'; import { RefreshToken } from '../entities/refresh-token.entity';
import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload'; import { IAdminTokenPayload, ITokenPayload } from '../interfaces/IToken-payload';
import { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity';
@Injectable() @Injectable()
export class TokensService { export class TokensService {
private readonly logger = new Logger(TokensService.name); private readonly logger = new Logger(TokensService.name);
constructor( constructor(
private readonly configService: ConfigService, private readonly configService: ConfigService,
@@ -22,24 +22,22 @@ export class TokensService {
async generateAccessAndRefreshToken( async generateAccessAndRefreshToken(
ownerId: string, ownerId: string,
: string,
isAdmin: boolean, isAdmin: boolean,
slug: string,
em?: EntityManager, em?: EntityManager,
) { ) {
const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE'); const refreshExpire = this.configService.getOrThrow<number>('REFRESH_TOKEN_EXPIRE');
const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME'); const accessExpire = this.configService.getOrThrow<number>('JWT_EXPIRATION_TIME');
const payload: ITokenPayload | IAdminTokenPayload = isAdmin const payload: ITokenPayload | IAdminTokenPayload = isAdmin
? { adminId: ownerId, : } ? { adminId: ownerId, }
: { userId: ownerId, : , slug }; : { userId: ownerId };
const accessToken = await this.generateAccessToken(payload, accessExpire); const accessToken = await this.generateAccessToken(payload, accessExpire);
const refreshToken = this.generateRefreshToken(); const refreshToken = this.generateRefreshToken();
const type = isAdmin ? RefreshTokenType.ADMIN : RefreshTokenType.USER; 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 // Only pass em if it's a transaction manager (not this.em), otherwise pass undefined to trigger flush
await this.storeRefreshToken(ownerId, , refreshToken, type, em); await this.storeRefreshToken(ownerId, refreshToken, type, em);
return { return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() }, accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() },
@@ -48,15 +46,11 @@ export class TokensService {
} }
private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expiresIn: number) { private generateAccessToken(payload: ITokenPayload | IAdminTokenPayload, expiresIn: number) {
// Ensure expiresIn is passed as a string with time unit for reliability
// JWT library accepts: number (seconds) or string with unit (e.g., "3600s", "1h")
// Using string format is more explicit and prevents unit confusion
return this.jwtService.signAsync(payload, { expiresIn: `${expiresIn}s` }); return this.jwtService.signAsync(payload, { expiresIn: `${expiresIn}s` });
} }
async storeRefreshToken( async storeRefreshToken(
ownerId: string, ownerId: string,
: string,
refreshToken: string, refreshToken: string,
type: RefreshTokenType, type: RefreshTokenType,
em?: EntityManager, em?: EntityManager,
@@ -70,76 +64,69 @@ export class TokensService {
const token = entityManager.create(RefreshToken, { const token = entityManager.create(RefreshToken, {
hashedToken, hashedToken,
ownerId, ownerId,
,
type, type,
expiresAt, expiresAt,
}); });
if(em) { if (em) {
// Within transaction, just persist (flush will be called by transaction) // Within transaction, just persist (flush will be called by transaction)
entityManager.persist(token); entityManager.persist(token);
} else { } else {
// Outside transaction, persist and flush immediately // Outside transaction, persist and flush immediately
await entityManager.persistAndFlush(token); await entityManager.persistAndFlush(token);
} }
} }
async refreshToken(oldRefreshToken: string) { 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 => {
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) { const token = await em.findOne(RefreshToken, { hashedToken });
throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
}
// Check expiration if (!token) {
if (dayjs(token.expiresAt).isBefore(dayjs())) { throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
em.remove(token); }
await em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Store token data before removal // Check expiration
const ownerId = token.ownerId; if (dayjs(token.expiresAt).isBefore(dayjs())) {
const = token.; em.remove(token);
const isAdmin = token.type === RefreshTokenType.ADMIN; await em.flush();
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
// Verify restaurant still exists // Store token data before removal
const restaurant = await em.findOne(Restaurant, { id: }); const ownerId = token.ownerId;
if (!restaurant) { const isAdmin = token.type === RefreshTokenType.ADMIN;
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);
// Remove old token only after new token is created try {
em.remove(token); // 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, isAdmin, em);
// Persist changes atomically // Remove old token only after new token is created
await em.flush(); em.remove(token);
return tokens; // Persist changes atomically
} catch (error) { await em.flush();
this.logger.error('Failed to generate new tokens after refresh', error);
// Transaction will rollback automatically, preserving the old token return tokens;
throw new UnauthorizedException('Failed to refresh token'); } 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() { private generateRefreshToken() {
return randomBytes(32).toString('hex'); return randomBytes(32).toString('hex');
} }
private hashToken(token: string): string { private hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex'); return createHash('sha256').update(token).digest('hex');
} }
} }
@@ -1,5 +1,5 @@
import type { Admin } from '../../admin/entities/admin.entity'; import type { Admin } from '../../admin/entities/admin.entity';
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
export interface AdminLoginResponse { export interface AdminLoginResponse {
id: string; id: string;
@@ -8,86 +8,20 @@ export interface AdminLoginResponse {
phone: string; phone: string;
role: string; role: string;
permissions: string[]; permissions: string[];
restaurant?: {
id: string;
name: string;
slug: string;
};
} }
export class AdminLoginTransformer { export class AdminLoginTransformer {
static async transform(admin: Admin, restaurant?: Restaurant): Promise<AdminLoginResponse> { static async transform(admin: Admin): Promise<AdminLoginResponse> {
// Find the AdminRole that matches the restaurant (or get the first one)
let adminRole = admin.roles.getItems().find(r => (restaurant ? r.restaurant?.id === restaurant.id : true));
// If no match found, get the first one
if (!adminRole && admin.roles.getItems().length > 0) {
adminRole = admin.roles.getItems()[0];
}
if (!adminRole) {
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: '',
permissions: [],
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
};
}
// Get the role from AdminRole
const role = adminRole.role;
if (!role) {
return {
id: admin.id,
firstName: admin.firstName,
lastName: admin.lastName,
phone: admin.phone,
role: '',
permissions: [],
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
};
}
// Extract permissions - ensure collection is loaded if needed
let permissions: string[] = [];
if (role.permissions) {
if (role.permissions.isInitialized()) {
permissions = role.permissions.getItems().map(p => p.name);
} else {
await role.permissions.loadItems();
permissions = role.permissions.getItems().map(p => p.name);
}
}
return { return {
id: admin.id, id: admin.id,
firstName: admin.firstName, firstName: admin.firstName,
lastName: admin.lastName, lastName: admin.lastName,
phone: admin.phone, phone: admin.phone,
role: role.name, role: admin.role.name,
permissions, permissions: admin.role.permissions.map(p => p.name),
restaurant: restaurant
? {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
}
: undefined,
}; };
} }
} }
@@ -1,33 +1,21 @@
import type { User } from '../../user/entities/user.entity'; import type { User } from '../../user/entities/user.entity';
import type { Restaurant } from '../../restaurants/entities/restaurant.entity';
export interface UserLoginResponse { export interface UserLoginResponse {
id: string; id: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
phone: string; phone: string;
isActive?: boolean; isActive?: boolean;
restaurant: {
id: string;
name: string;
slug: string;
};
} }
export class UserLoginTransformer { export class UserLoginTransformer {
static transform(user: User, restaurant: Restaurant): UserLoginResponse { static transform(user: User): UserLoginResponse {
return { return {
id: user.id, id: user.id,
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
phone: user.phone, phone: user.phone,
isActive: user.isActive, isActive: user.isActive,
restaurant: {
id: restaurant.id,
name: restaurant.name,
slug: restaurant.slug,
},
}; };
} }
} }
+2 -2
View File
@@ -1,5 +1,5 @@
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
import { RequiredEntityData } from '@mikro-orm/core'; import { RequiredEntityData } from '@mikro-orm/core';
import { User } from '../entities/user.entity'; import { User } from '../entities/user.entity';
import { UserAddress } from '../entities/user-address.entity'; import { UserAddress } from '../entities/user-address.entity';
import { EntityManager } from '@mikro-orm/postgresql'; import { EntityManager } from '@mikro-orm/postgresql';
@@ -17,7 +17,7 @@ export class UserService {
private readonly em: EntityManager, private readonly em: EntityManager,
) { } ) { }
async create(dto: CreateUserDto): Promise<User> { async getOrCreate(dto: CreateUserDto): Promise<User> {
const normalizedPhone = normalizePhone(dto.phone) const normalizedPhone = normalizePhone(dto.phone)
const currentUser = await this.userRepository.findOne({ phone: normalizedPhone }); const currentUser = await this.userRepository.findOne({ phone: normalizedPhone });