diff --git a/src/modules/admin/admin.module.ts b/src/modules/admin/admin.module.ts index 83eb130..a1215e6 100644 --- a/src/modules/admin/admin.module.ts +++ b/src/modules/admin/admin.module.ts @@ -1,4 +1,4 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { AdminService } from './providers/admin.service'; import { MikroOrmModule } from '@mikro-orm/nestjs'; 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 { AdminController } from './controllers/admin.controller'; import { UtilsModule } from '../util/utils.module'; -import { RestaurantsModule } from '../restaurants/restaurants.module'; -import { AdminRole } from './entities/adminRole.entity'; + @Module({ providers: [AdminService, AdminRepository], controllers: [AdminController], imports: [ - MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission, AdminRole]), + MikroOrmModule.forFeature([Admin, Role, Permission, RolePermission]), JwtModule, UtilsModule, - forwardRef(() => RestaurantsModule), ], exports: [AdminService, AdminRepository], }) diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 75dbf72..0e174ee 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -6,7 +6,7 @@ import { UpdateAdminDto } from '../dto/update-admin.dto'; import { normalizePhone } from '../../util/phone.util'; import { CreateAdminDto } from '../dto/create-admin.dto'; import { AdminRepository } from '../repositories/admin.repository'; -import { RoleRepository } from '../repositories/role.repository'; +import { RoleRepository } from 'src/modules/roles/respository/role.repository'; @Injectable() export class AdminService { @@ -60,7 +60,7 @@ export class AdminService { } async findAll(): Promise { - return this.adminRepository.find({}, { populate: ['role', 'roles.role'] }); + return this.adminRepository.find({}, { populate: ['role'] }); } async update(adminId: string, dto: UpdateAdminDto): Promise { diff --git a/src/modules/admin/repositories/admin.repository.ts b/src/modules/admin/repositories/admin.repository.ts index 9e4cba6..38519e3 100644 --- a/src/modules/admin/repositories/admin.repository.ts +++ b/src/modules/admin/repositories/admin.repository.ts @@ -14,7 +14,7 @@ export class AdminRepository extends EntityRepository { const admins = await this.em.find( Admin, { role: { permissions: { name: permission } } }, - { populate: ['role', 'roles.permissions'] }, + { populate: ['role', 'role.permissions'] }, ); return admins; } diff --git a/src/modules/admin/transformers/admin-detail.transformer.ts b/src/modules/admin/transformers/admin-detail.transformer.ts deleted file mode 100644 index 84704a0..0000000 --- a/src/modules/admin/transformers/admin-detail.transformer.ts +++ /dev/null @@ -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 diff --git a/src/modules/auth/controllers/auth.controller.ts b/src/modules/auth/controllers/auth.controller.ts index 6a4b7b9..e7be66f 100644 --- a/src/modules/auth/controllers/auth.controller.ts +++ b/src/modules/auth/controllers/auth.controller.ts @@ -1,13 +1,12 @@ import { Controller, Post, Body, UseGuards } from '@nestjs/common'; import { AuthService } from '../services/auth.service'; import { RequestOtpDto } from '../dto/request-otp.dto'; -import { DirectLoginDto } from '../dto/direct-login.dto'; import { ApiTags, ApiOperation, ApiBody } from '@nestjs/swagger'; import { VerifyOtpDto } from '../dto/verify-otp.dto'; import { Throttle } from '@nestjs/throttler'; import { RefreshTokenRateLimit } from 'src/common/decorators/rate-limit.decorator'; import { RefreshTokenDto } from '../dto/refresh-token.dto'; -import { SuperAdminAuthGuard } from '../guards/superAdminAuth.guard'; + @ApiTags('auth') @Controller() @@ -26,7 +25,7 @@ export class AuthController { @ApiOperation({ summary: 'Verify OTP code' }) @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' }) 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() @@ -48,7 +47,7 @@ export class AuthController { @ApiOperation({ summary: 'Verify OTP code' }) @ApiBody({ type: VerifyOtpDto, description: 'Mobile number and OTP code' }) otpVerifyAdmin(@Body() dto: VerifyOtpDto) { - return this.authService.verifyOtpAdmin(dto.phone, dto.slug, dto.otp); + return this.authService.verifyOtpAdmin(dto.phone, dto.otp); } @RefreshTokenRateLimit() @@ -58,14 +57,5 @@ export class AuthController { 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); - } } diff --git a/src/modules/auth/entities/refresh-token.entity.ts b/src/modules/auth/entities/refresh-token.entity.ts index c9855dc..c58e773 100644 --- a/src/modules/auth/entities/refresh-token.entity.ts +++ b/src/modules/auth/entities/refresh-token.entity.ts @@ -1,29 +1,19 @@ import { Entity, Enum, Index, Property } from '@mikro-orm/core'; - -import { BaseEntity } from '../../../common/entities/base.entity'; - -export enum RefreshTokenType { - USER = 'user', - ADMIN = 'admin', -} +import { RefreshTokenType } from '../interfaces/IToken-payload'; @Entity({ tableName: 'refreshtokens' }) -@Index({ properties: ['ownerId', '', 'type'] }) @Index({ properties: ['hashedToken'] }) -@Index({ properties: ['expiresAt'] }) -export class RefreshToken extends BaseEntity { +export class RefreshToken { + @Property({ type: 'varchar', length: 255 }) hashedToken!: string; @Property() ownerId!: string; - @Property() - !: string; - -@Enum(() => RefreshTokenType) + @Enum(() => RefreshTokenType) type!: RefreshTokenType; -@Property({ type: 'timestamptz' }) -expiresAt!: Date; + @Property({ type: 'timestamptz' }) + expiresAt!: Date; } diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts index d21d106..40064a5 100755 --- a/src/modules/auth/interfaces/IToken-payload.ts +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -1,10 +1,12 @@ export interface ITokenPayload { userId: string; - : string; - slug: string; } export interface IAdminTokenPayload { adminId: string; - : string; } + +export enum RefreshTokenType { + USER = 'user', + ADMIN = 'admin', +} \ No newline at end of file diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index 76f4bd3..9685471 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -5,8 +5,7 @@ import { SmsService } from '../../notification/services/sms.service'; import { UserService } from '../../user/providers/user.service'; import { randomInt } from 'crypto'; import { TokensService } from './tokens.service'; -import { RestRepository } from 'src/modules/restaurants/repositories/rest.repository'; -import { AuthMessage, RestMessage } from 'src/common/enums/message.enum'; +import { AuthMessage } from 'src/common/enums/message.enum'; import { AdminRepository } from 'src/modules/admin/repositories/admin.repository'; import { AdminLoginTransformer } from '../transformers/admin-login.transformer'; import { UserLoginTransformer } from '../transformers/user-login.transformer'; @@ -17,31 +16,32 @@ import { normalizePhone } from '../../util/phone.util'; export class AuthService { readonly ADMIN_PERMISSIONS_KEY = 'admin-perms'; private OTP_EXPIRATION_TIME: number; + constructor( private readonly cacheService: CacheService, private readonly smsService: SmsService, private readonly userService: UserService, private readonly tokensService: TokensService, - private readonly restRepository: RestRepository, private readonly adminRepository: AdminRepository, private readonly configService: ConfigService, ) { this.OTP_EXPIRATION_TIME = this.configService.get('OTP_EXPIRATION_TIME') ?? 240; } - private userOtpKey(restaurantSlug: string, phone: string) { - return `otp:${restaurantSlug}:${phone}`; + private userOtpKey(phone: string) { + return `otp:${phone}`; } - private adminOtpKey(restaurantSlug: string, phone: string) { - return `otp-admin:${restaurantSlug}:${phone}`; + private adminOtpKey(phone: string) { + return `otp-admin:${phone}`; } async requestOtp(dto: RequestOtpDto, isAdmin: boolean) { - const { phone, slug } = dto; + const { phone } = dto; const normalizedPhone = normalizePhone(phone); 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.smsService.sendotp(normalizedPhone, code); @@ -49,33 +49,34 @@ export class AuthService { return { code: null }; } - async verifyOtp(phone: string, slug: string, code: string) { + async verifyOtp(phone: string, code: string) { const normalizedPhone = normalizePhone(phone); - const key = this.userOtpKey(slug, normalizedPhone); + const key = this.userOtpKey(normalizedPhone); const cachedCode = await this.cacheService.get(key); if (!cachedCode) throw new BadRequestException('OTP expired or not found'); if (cachedCode !== code) throw new BadRequestException('Invalid OTP'); 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) { - throw new BadRequestException(RestMessage.NOT_FOUND); - } + const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, false); - const tokens = await this.tokensService.generateAccessAndRefreshToken(user.id, rest.id, false, slug); - - const userResponse = UserLoginTransformer.transform(user, rest); + const userResponse = UserLoginTransformer.transform(user); return { tokens, user: userResponse }; } - async verifyOtpAdmin(phone: string, slug: string, code: string) { + async verifyOtpAdmin(phone: string, code: string) { const normalizedPhone = normalizePhone(phone); - const key = this.adminOtpKey(slug, normalizedPhone); + const key = this.adminOtpKey(normalizedPhone); const cachedCode = await this.cacheService.get(key); 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'); await this.cacheService.del(key); - const admin = await this.adminRepository.findByPhoneAndRestaurantSlug(normalizedPhone, slug); + const admin = await this.adminRepository.findOne({ phone: normalizedPhone }); if (!admin) { 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) { - 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); + const adminResponse = await AdminLoginTransformer.transform(admin); return { tokens, admin: adminResponse }; } diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index a728126..d36ee6d 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -1,18 +1,18 @@ import { createHash, randomBytes } from 'node:crypto'; import { EntityManager } from '@mikro-orm/postgresql'; -import { LockMode } from '@mikro-orm/core'; import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import dayjs from 'dayjs'; - +import { RefreshTokenType } from '../interfaces/IToken-payload'; 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 { Restaurant } from 'src/modules/restaurants/entities/restaurant.entity'; + @Injectable() export class TokensService { + private readonly logger = new Logger(TokensService.name); constructor( private readonly configService: ConfigService, @@ -22,24 +22,22 @@ export class TokensService { async generateAccessAndRefreshToken( ownerId: string, - : string, isAdmin: boolean, - slug: string, em?: EntityManager, ) { const refreshExpire = this.configService.getOrThrow('REFRESH_TOKEN_EXPIRE'); const accessExpire = this.configService.getOrThrow('JWT_EXPIRATION_TIME'); const payload: ITokenPayload | IAdminTokenPayload = isAdmin - ? { adminId: ownerId, : } - : { userId: ownerId, : , slug }; + ? { adminId: ownerId, } + : { userId: ownerId }; const accessToken = await this.generateAccessToken(payload, accessExpire); const refreshToken = this.generateRefreshToken(); 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 - await this.storeRefreshToken(ownerId, , refreshToken, type, em); + await this.storeRefreshToken(ownerId, refreshToken, type, em); return { accessToken: { token: accessToken, expire: dayjs().add(accessExpire, 'seconds').valueOf() }, @@ -48,15 +46,11 @@ export class TokensService { } 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` }); } async storeRefreshToken( ownerId: string, - : string, refreshToken: string, type: RefreshTokenType, em?: EntityManager, @@ -70,76 +64,69 @@ export class TokensService { const token = entityManager.create(RefreshToken, { hashedToken, ownerId, - , type, expiresAt, }); - if(em) { - // Within transaction, just persist (flush will be called by transaction) - entityManager.persist(token); - } else { - // Outside transaction, persist and flush immediately - await entityManager.persistAndFlush(token); -} + if (em) { + // Within transaction, just persist (flush will be called by transaction) + entityManager.persist(token); + } else { + // Outside transaction, persist and flush immediately + await entityManager.persistAndFlush(token); + } } 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 => { - // 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 }); + return this.em.transactional(async em => { - if (!token) { - throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); - } + const token = await em.findOne(RefreshToken, { hashedToken }); - // Check expiration - if (dayjs(token.expiresAt).isBefore(dayjs())) { - em.remove(token); - await em.flush(); - throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); - } + if (!token) { + throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); + } - // Store token data before removal - const ownerId = token.ownerId; - const = token.; - const isAdmin = token.type === RefreshTokenType.ADMIN; + // Check expiration + if (dayjs(token.expiresAt).isBefore(dayjs())) { + em.remove(token); + await em.flush(); + throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); + } - // Verify restaurant still exists - const restaurant = await em.findOne(Restaurant, { id: }); - if (!restaurant) { - throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); - } + // Store token data before removal + const ownerId = token.ownerId; + const isAdmin = token.type === RefreshTokenType.ADMIN; - 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 - em.remove(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, isAdmin, em); - // Persist changes atomically - await em.flush(); + // Remove old token only after new token is created + em.remove(token); - return tokens; - } 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'); - } - }); -} + // Persist changes atomically + await em.flush(); + + return tokens; + } 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() { - return randomBytes(32).toString('hex'); -} + return randomBytes(32).toString('hex'); + } private hashToken(token: string): string { - return createHash('sha256').update(token).digest('hex'); -} + return createHash('sha256').update(token).digest('hex'); + } + } + diff --git a/src/modules/auth/transformers/admin-login.transformer.ts b/src/modules/auth/transformers/admin-login.transformer.ts index c58ed4b..26ab024 100644 --- a/src/modules/auth/transformers/admin-login.transformer.ts +++ b/src/modules/auth/transformers/admin-login.transformer.ts @@ -1,5 +1,5 @@ import type { Admin } from '../../admin/entities/admin.entity'; -import type { Restaurant } from '../../restaurants/entities/restaurant.entity'; + export interface AdminLoginResponse { id: string; @@ -8,86 +8,20 @@ export interface AdminLoginResponse { phone: string; role: string; permissions: string[]; - restaurant?: { - id: string; - name: string; - slug: string; - }; + } export class AdminLoginTransformer { - static async transform(admin: Admin, restaurant?: Restaurant): Promise { - // 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); - } - } + static async transform(admin: Admin): Promise { return { id: admin.id, firstName: admin.firstName, lastName: admin.lastName, phone: admin.phone, - role: role.name, - permissions, - restaurant: restaurant - ? { - id: restaurant.id, - name: restaurant.name, - slug: restaurant.slug, - } - : undefined, + role: admin.role.name, + permissions: admin.role.permissions.map(p => p.name), + }; } } diff --git a/src/modules/auth/transformers/user-login.transformer.ts b/src/modules/auth/transformers/user-login.transformer.ts index bcb481d..8f222f2 100644 --- a/src/modules/auth/transformers/user-login.transformer.ts +++ b/src/modules/auth/transformers/user-login.transformer.ts @@ -1,33 +1,21 @@ import type { User } from '../../user/entities/user.entity'; -import type { Restaurant } from '../../restaurants/entities/restaurant.entity'; export interface UserLoginResponse { id: string; firstName: string; lastName?: string; phone: string; - isActive?: boolean; - restaurant: { - id: string; - name: string; - slug: string; - }; } export class UserLoginTransformer { - static transform(user: User, restaurant: Restaurant): UserLoginResponse { + static transform(user: User): UserLoginResponse { return { id: user.id, firstName: user.firstName, lastName: user.lastName, phone: user.phone, isActive: user.isActive, - restaurant: { - id: restaurant.id, - name: restaurant.name, - slug: restaurant.slug, - }, }; } } diff --git a/src/modules/user/providers/user.service.ts b/src/modules/user/providers/user.service.ts index 9e849cf..c38d1ef 100644 --- a/src/modules/user/providers/user.service.ts +++ b/src/modules/user/providers/user.service.ts @@ -1,5 +1,5 @@ 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 { UserAddress } from '../entities/user-address.entity'; import { EntityManager } from '@mikro-orm/postgresql'; @@ -17,7 +17,7 @@ export class UserService { private readonly em: EntityManager, ) { } - async create(dto: CreateUserDto): Promise { + async getOrCreate(dto: CreateUserDto): Promise { const normalizedPhone = normalizePhone(dto.phone) const currentUser = await this.userRepository.findOne({ phone: normalizedPhone });