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 <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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<Omit<User, 'password'>> {
|
||||
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<User, 'password'>;
|
||||
@@ -81,6 +98,7 @@ export class UsersService {
|
||||
async findAll(search?: string): Promise<any[]> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user