From a3b7e9055c2376c580d2dd5ae6d5bfe281b4dc59 Mon Sep 17 00:00:00 2001 From: keyhan Date: Wed, 17 Jun 2026 12:34:43 +0330 Subject: [PATCH] feat(admin): create users by mobile number instead of email Admin user management now creates accounts with a required mobile number and an optional contact email, matching mobile-only auth. Admin-created accounts are pre-verified (phoneVerified) so the user can sign in by password immediately. The user list and search now include phone. Co-Authored-By: Claude Opus 4.8 --- backend/src/users/users.controller.ts | 9 ++++-- backend/src/users/users.service.ts | 30 +++++++++++++++---- .../app/[lang]/dashboard/admin/users/page.tsx | 27 +++++++++++++---- frontend/src/i18n/dictionaries/en.ts | 6 +++- frontend/src/i18n/dictionaries/fa.ts | 6 +++- 5 files changed, 63 insertions(+), 15 deletions(-) diff --git a/backend/src/users/users.controller.ts b/backend/src/users/users.controller.ts index e49cbe2..da11549 100644 --- a/backend/src/users/users.controller.ts +++ b/backend/src/users/users.controller.ts @@ -19,8 +19,8 @@ import { Roles } from '../common/decorators/roles.decorator'; import { UserRole } from '../common/enums'; class AdminCreateUserDto { - @IsEmail() - email: string; + @IsString() + phone: string; @IsString() @MinLength(8) @@ -35,6 +35,11 @@ class AdminCreateUserDto { @MinLength(1) lastName: string; + // Optional contact email (not a login identifier). + @IsOptional() + @IsEmail() + email?: string; + @IsOptional() @IsEnum(UserRole) role?: UserRole; diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index 0ddc451..0b08429 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -11,6 +11,7 @@ import { Repository, ILike } from 'typeorm'; import * as bcrypt from 'bcrypt'; import { User } from './entities/user.entity'; import { UserRole } from '../common/enums'; +import { normalizeIranMobile } from '../common/phone.util'; @Injectable() export class UsersService { @@ -31,21 +32,37 @@ export class UsersService { * Admin create user — hashes password, checks duplicate email */ async adminCreate(data: { - email: string; + phone: string; password: string; firstName: string; lastName: string; + email?: string; role?: UserRole; }): Promise> { - const existing = await this.findByEmail(data.email); - if (existing) { - throw new ConflictException('Email already registered'); + const phone = normalizeIranMobile(data.phone); + if (!phone) { + throw new BadRequestException('Invalid mobile number'); } + if (await this.findByPhone(phone)) { + throw new ConflictException('Mobile number already registered'); + } + + const email = data.email ? data.email.trim().toLowerCase() : null; + if (email && (await this.findByEmail(email))) { + throw new ConflictException('Email already in use'); + } + const hashedPassword = await bcrypt.hash(data.password, 12); const user = await this.create({ - ...data, + phone, + email, + firstName: data.firstName, + lastName: data.lastName, password: hashedPassword, role: data.role || UserRole.USER, + // Admin-created accounts are trusted — the phone is pre-verified so the + // user can sign in by password immediately, no OTP step. + phoneVerified: true, }); const { password, ...result } = user; return result as Omit; @@ -81,6 +98,7 @@ export class UsersService { async findAll(search?: string): Promise { const where = search ? [ + { phone: ILike(`%${search}%`) }, { email: ILike(`%${search}%`) }, { firstName: ILike(`%${search}%`) }, { lastName: ILike(`%${search}%`) }, @@ -91,6 +109,7 @@ export class UsersService { where, select: { id: true, + phone: true, email: true, firstName: true, lastName: true, @@ -105,6 +124,7 @@ export class UsersService { return users.map((u) => ({ id: u.id, + phone: u.phone, email: u.email, firstName: u.firstName, lastName: u.lastName, diff --git a/frontend/src/app/[lang]/dashboard/admin/users/page.tsx b/frontend/src/app/[lang]/dashboard/admin/users/page.tsx index b330570..c643893 100644 --- a/frontend/src/app/[lang]/dashboard/admin/users/page.tsx +++ b/frontend/src/app/[lang]/dashboard/admin/users/page.tsx @@ -31,6 +31,7 @@ export default function AdminUsersPage() { const [pwdModalUser, setPwdModalUser] = useState(null); const [pwdModalPassword, setPwdModalPassword] = useState(''); const [form, setForm] = useState({ + phone: '', email: '', password: '', firstName: '', @@ -50,7 +51,7 @@ export default function AdminUsersPage() { queryClient.invalidateQueries({ queryKey: ['admin-users'] }); notify.success(u.createdSuccess); setShowForm(false); - setForm({ email: '', password: '', firstName: '', lastName: '', role: 'user' }); + setForm({ phone: '', email: '', password: '', firstName: '', lastName: '', role: 'user' }); }, onError: (err: any) => { notify.error(err, u.createFailed); @@ -141,10 +142,22 @@ export default function AdminUsersPage() { />
- + + setForm({ ...form, phone: e.target.value })} + /> +
+
+ setForm({ ...form, email: e.target.value })} @@ -179,7 +192,7 @@ export default function AdminUsersPage() {