phone
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { Cascade, Entity, ManyToOne, OptionalProps, Property } from '@mikro-orm/core';
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { Role } from 'src/modules/roles/entities/role.entity';
|
||||
|
||||
@Entity({ tableName: 'admins' })
|
||||
@@ -13,16 +12,8 @@ export class Admin extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
phone: string
|
||||
|
||||
@ManyToOne(() => Role)
|
||||
role: Role
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Admin } from '../entities/admin.entity';
|
||||
import { Role } from '../../roles/entities/role.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
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 'src/modules/roles/respository/role.repository';
|
||||
@@ -20,10 +19,9 @@ export class AdminService {
|
||||
async create(dto: CreateAdminDto) {
|
||||
const { phone, firstName, lastName, roleId } = dto;
|
||||
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
const currentAdmin = await this.adminRepository.findOne({
|
||||
phone: normalizedPhone,
|
||||
phone,
|
||||
}, { filters: { notDeleted: false } });
|
||||
|
||||
if (currentAdmin && !currentAdmin.deletedAt) {
|
||||
@@ -42,7 +40,7 @@ export class AdminService {
|
||||
}
|
||||
|
||||
const admin = this.em.create(Admin, {
|
||||
phone: normalizedPhone,
|
||||
phone,
|
||||
firstName,
|
||||
lastName,
|
||||
role,
|
||||
@@ -52,8 +50,7 @@ export class AdminService {
|
||||
}
|
||||
|
||||
async findByPhone(phone: string): Promise<Admin | null> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
return this.adminRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(adminId: string): Promise<Admin | null> {
|
||||
@@ -89,12 +86,11 @@ export class AdminService {
|
||||
}
|
||||
|
||||
if (rest.phone !== undefined && rest.phone !== admin.phone) {
|
||||
const normalizedPhone = normalizePhone(rest.phone);
|
||||
const exists = await this.adminRepository.findOne({ phone: normalizedPhone });
|
||||
const exists = await this.adminRepository.findOne({ phone: rest.phone });
|
||||
if (exists) {
|
||||
throw new ConflictException('This Phone Number is already taken');
|
||||
}
|
||||
admin.phone = normalizedPhone;
|
||||
admin.phone = rest.phone;
|
||||
}
|
||||
|
||||
// Update role if roleId is provided
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IsNotEmpty, IsString, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { normalizePhoneToLocal } from 'src/modules/util/phone.util';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class RequestOtpDto {
|
||||
@Transform(({ value }) => normalizePhoneToLocal(value))
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IsNotEmpty, IsString, Length, IsMobilePhone } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { normalizePhoneToLocal } from 'src/modules/util/phone.util';
|
||||
|
||||
export class VerifyOtpDto {
|
||||
@Transform(({ value }) => normalizePhoneToLocal(value))
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ 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';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { OtpService } from './otp.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -27,15 +26,14 @@ export class AuthService {
|
||||
|
||||
async requestOtp(dto: RequestOtpDto, isAdmin: boolean) {
|
||||
const { phone } = dto;
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
if (isAdmin) {
|
||||
await this.valdateAdmin(normalizedPhone)
|
||||
await this.valdateAdmin(phone)
|
||||
}
|
||||
|
||||
const code = this.generateOtpCode();
|
||||
|
||||
await this.otpService.set(normalizedPhone, code)
|
||||
await this.otpService.set(phone, code)
|
||||
|
||||
// await this.smsService.sendotp(normalizedPhone, code);
|
||||
|
||||
@@ -43,16 +41,15 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verifyOtp(phone: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
const cachedCode = await this.otpService.get(normalizedPhone);
|
||||
const cachedCode = await this.otpService.get(phone);
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
|
||||
await this.otpService.delete(normalizedPhone)
|
||||
await this.otpService.delete(phone)
|
||||
|
||||
const user = await this.userService.getOrCreate({
|
||||
phone: normalizedPhone,
|
||||
phone: phone,
|
||||
gender: true,
|
||||
// maxCredit: 0
|
||||
});
|
||||
@@ -66,16 +63,15 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verifyOtpAdmin(phone: string, code: string) {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
|
||||
const cachedCode = await this.otpService.get(normalizedPhone);
|
||||
const cachedCode = await this.otpService.get(phone);
|
||||
|
||||
if (!cachedCode) throw new BadRequestException('OTP expired or not found');
|
||||
|
||||
if (cachedCode !== code) throw new BadRequestException('Invalid OTP');
|
||||
await this.otpService.delete(normalizedPhone);
|
||||
await this.otpService.delete(phone);
|
||||
|
||||
const admin = await this.adminRepository.findOne({ phone: normalizedPhone }, { populate: ['role', 'role.permissions'] });
|
||||
const admin = await this.adminRepository.findOne({ phone }, { populate: ['role', 'role.permissions'] });
|
||||
|
||||
if (!admin) {
|
||||
throw new BadRequestException(AuthMessage.ADMIN_NOT_FOUND);
|
||||
|
||||
@@ -50,7 +50,7 @@ export class UsersController {
|
||||
return this.userService.createUserAsAdmin(dto);
|
||||
}
|
||||
|
||||
@Post('admin/users/:id')
|
||||
@Get('admin/users/:id')
|
||||
@UseGuards(AdminAuthGuard)
|
||||
@Permissions(PermissionEnum.VIEW_USERS)
|
||||
@ApiOperation({ summary: 'find user ' })
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { IsString, IsOptional, IsNumber, IsBoolean, Min, Max } from 'class-validator';
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* DTO for creating a new user address.
|
||||
*/
|
||||
export class CreateUserAddressDto {
|
||||
@ApiProperty({ description: 'Address title/label (e.g., "Home", "Work")', example: 'Home' })
|
||||
@IsString()
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ description: 'Street address', example: '123 Main Street' })
|
||||
@IsString()
|
||||
address!: string;
|
||||
|
||||
@ApiProperty({ description: 'City name', example: 'Tehran' })
|
||||
@IsString()
|
||||
city!: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Province', example: 'Tehran Province' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
province?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Postal/ZIP code', example: '12345-6789' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
postalCode?: string;
|
||||
|
||||
@ApiProperty({ description: 'Latitude coordinate', example: 35.6892, minimum: -90, maximum: 90 })
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
latitude!: number;
|
||||
|
||||
@ApiProperty({ description: 'Longitude coordinate', example: 51.389, minimum: -180, maximum: 180 })
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
longitude!: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Contact phone for this address', example: '+989123456789' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
phone?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Set as default address', example: false, default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDefault?: boolean;
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt, IsNumber, IsOptional } from 'class-validator';
|
||||
import { IsString, IsBoolean, IsNotEmpty, IsMobilePhone, IsInt, IsNumber, IsOptional, Min } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { normalizePhoneToLocal } from '../../util/phone.util';
|
||||
|
||||
export class CreateUserDto {
|
||||
@Transform(({ value }) => normalizePhoneToLocal(value))
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number' })
|
||||
@ApiProperty({ example: '09362532122', description: 'Mobile number (accepts +98, 0098, 98, 09 formats)' })
|
||||
@IsMobilePhone('fa-IR')
|
||||
phone: string;
|
||||
|
||||
@@ -32,6 +35,8 @@ export class CreateUserDto {
|
||||
|
||||
export class CreateUserAsAdminDto extends CreateUserDto {
|
||||
@ApiProperty({ description: "User's max credit in Toman", example: 100000 })
|
||||
@Transform(({ value }) => Number(value))
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
maxCredit: number;
|
||||
}
|
||||
@@ -2,9 +2,7 @@ import { Entity, Index, Property, OneToMany, Collection, PrimaryKey, OptionalPro
|
||||
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||
import { LearningProgress } from '../../learnings/entities/learning-progress.entity';
|
||||
import { Criticism } from '../../criticisms/entities/criticism.entity';
|
||||
// import { Order } from 'src/modules/order/entities/order.entity';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
import { ulid } from 'ulid';
|
||||
|
||||
|
||||
@Entity({ tableName: 'users' })
|
||||
export class User extends BaseEntity {
|
||||
@@ -26,16 +24,9 @@ export class User extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
lastName?: string;
|
||||
|
||||
private _phone!: string;
|
||||
|
||||
@Property({ unique: true })
|
||||
get phone(): string {
|
||||
return this._phone;
|
||||
}
|
||||
|
||||
set phone(value: string) {
|
||||
this._phone = normalizePhone(value);
|
||||
}
|
||||
phone: string
|
||||
|
||||
|
||||
@Property({ default: true })
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CreateUserAsAdminDto, CreateUserDto } from '../dto/create-user.dto';
|
||||
import { FindUsersDto } from '../dto/find-user.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { normalizePhone } from '../../util/phone.util';
|
||||
// import { normalizePhone } from '../../util/phone.util';
|
||||
import { UpdateUserDto } from '../dto/update-user.dto';
|
||||
import { CreateUser } from '../interface/user';
|
||||
|
||||
@@ -25,13 +25,15 @@ export class UserService {
|
||||
throw new BadRequestException("User with this phone already exist")
|
||||
}
|
||||
|
||||
const user = await this.createUser({ ...dto, phone: normalizePhone(dto.phone) })
|
||||
const user = await this.createUser(dto)
|
||||
|
||||
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async getOrCreate(dto: CreateUserDto): Promise<User> {
|
||||
const normalizedPhone = normalizePhone(dto.phone)
|
||||
const normalizedPhone = dto.phone
|
||||
|
||||
const currentUser = await this.userRepository.findOne({ phone: normalizedPhone });
|
||||
|
||||
@@ -45,8 +47,8 @@ export class UserService {
|
||||
}
|
||||
|
||||
async findByPhone(phone: string): Promise<User | null> {
|
||||
const normalizedPhone = normalizePhone(phone);
|
||||
return this.userRepository.findOne({ phone: normalizedPhone });
|
||||
|
||||
return this.userRepository.findOne({ phone });
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
@@ -61,7 +63,11 @@ export class UserService {
|
||||
throw new NotFoundException(`User with ID ${userId} not found.`);
|
||||
}
|
||||
|
||||
this.em.assign(user, dto);
|
||||
const { address, ...rest } = dto;
|
||||
this.em.assign(user, rest);
|
||||
if (address !== undefined) {
|
||||
user.addresse = address;
|
||||
}
|
||||
|
||||
await this.em.flush();
|
||||
|
||||
@@ -89,12 +95,13 @@ export class UserService {
|
||||
firstName: param.firstName,
|
||||
lastName: param.lastName,
|
||||
gender: param.gender,
|
||||
maxCredit: param.maxCredit
|
||||
maxCredit: param.maxCredit,
|
||||
addresse:param.address
|
||||
};
|
||||
|
||||
const user = this.userRepository.create(createData);
|
||||
|
||||
await this.em.persistAndFlush([user]);
|
||||
await this.em.flush();
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { EntityManager, EntityRepository, FilterQuery } from '@mikro-orm/postgre
|
||||
import { User } from '../entities/user.entity';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { FindUsersDto } from '../dto/find-user.dto';
|
||||
import { normalizePhone } from 'src/modules/util/phone.util';
|
||||
|
||||
@Injectable()
|
||||
export class UserRepository extends EntityRepository<User> {
|
||||
@@ -23,7 +22,7 @@ export class UserRepository extends EntityRepository<User> {
|
||||
// 4. Add 'search' logic (case-insensitive)
|
||||
if (search) {
|
||||
const searchPattern = `%${search}%`;
|
||||
const normalizedSearch = normalizePhone(search);
|
||||
const normalizedSearch = search
|
||||
const normalizedSearchPattern = `%${normalizedSearch}%`;
|
||||
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
|
||||
// Search with both original and normalized phone patterns to handle various input formats
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @param phone - The phone number to normalize
|
||||
* @returns Normalized phone number (e.g., 9120000001)
|
||||
*/
|
||||
export function normalizePhone(phone: string): string {
|
||||
export function normalizePhone1(phone: string): string {
|
||||
if (!phone) {
|
||||
return phone;
|
||||
}
|
||||
@@ -28,3 +28,17 @@ export function normalizePhone(phone: string): string {
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes Iranian mobile numbers to local format: 09xxxxxxxxx
|
||||
* Accepts +98..., 0098..., 98..., 9..., 09... and strips spaces/dashes
|
||||
* @param phone - The phone number to normalize
|
||||
* @returns Normalized phone number (e.g., 09362532122)
|
||||
*/
|
||||
export function normalizePhoneToLocal(phone: string): string {
|
||||
if (!phone || typeof phone !== 'string') {
|
||||
return phone;
|
||||
}
|
||||
const normalized = normalizePhone1(phone);
|
||||
return normalized ? `0${normalized}` : phone;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import type { EntityManager } from '@mikro-orm/core';
|
||||
import { Admin } from '../modules/admin/entities/admin.entity';
|
||||
import type { Role } from '../modules/roles/entities/role.entity';
|
||||
import { adminsData } from './data/admins.data';
|
||||
import { normalizePhone } from '../modules/util/phone.util';
|
||||
|
||||
|
||||
export class AdminsSeeder {
|
||||
async run(em: EntityManager, rolesMap: Map<string, Role>): Promise<void> {
|
||||
for (const adminData of adminsData) {
|
||||
@@ -15,7 +14,7 @@ export class AdminsSeeder {
|
||||
|
||||
|
||||
const admin = em.create(Admin, {
|
||||
phone: normalizePhone(adminData.phone),
|
||||
phone: (adminData.phone),
|
||||
firstName: adminData.firstName,
|
||||
lastName: adminData.lastName,
|
||||
role
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { EntityManager } from '@mikro-orm/core';
|
||||
import { User } from '../modules/user/entities/user.entity';
|
||||
import { usersData } from './data/users.data';
|
||||
import { normalizePhone } from '../modules/util/phone.util';
|
||||
|
||||
export class UsersSeeder {
|
||||
async run(em: EntityManager): Promise<Map<string, User>> {
|
||||
@@ -9,11 +8,10 @@ export class UsersSeeder {
|
||||
|
||||
// Create users
|
||||
for (const userData of usersData) {
|
||||
const normalizedPhone = normalizePhone(userData.phone);
|
||||
let user = await em.findOne(User, { phone: normalizedPhone });
|
||||
let user = await em.findOne(User, { phone: userData.phone });
|
||||
if (!user) {
|
||||
user = em.create(User, {
|
||||
phone: normalizedPhone, // Use normalized phone
|
||||
phone: userData.phone, // Use normalized phone
|
||||
firstName: userData.firstName,
|
||||
lastName: userData.lastName,
|
||||
isActive: userData.isActive,
|
||||
@@ -23,7 +21,7 @@ export class UsersSeeder {
|
||||
});
|
||||
em.persist(user);
|
||||
}
|
||||
usersMap.set(normalizedPhone, user); // Use normalized phone as key
|
||||
usersMap.set(userData.phone, user); // Use normalized phone as key
|
||||
}
|
||||
|
||||
await em.flush();
|
||||
|
||||
Reference in New Issue
Block a user