Allow editing org team member name, mobile, and password from settings.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-04 12:31:07 +03:30
co-authored by Cursor
parent 39e2eb24a6
commit 75ff2c36c4
5 changed files with 219 additions and 1 deletions
+20
View File
@@ -44,6 +44,26 @@ export class InviteUserDto {
orgRoleId?: string;
}
export class UpdateOrgUserDto {
@IsString()
@MinLength(2)
@MaxLength(120)
fullName!: string;
@IsString()
@Matches(/^09\d{9}$|^\+98\d{10}$|^98\d{10}$|^9\d{9}$/, {
message: 'شماره موبایل معتبر نیست',
})
mobile!: string;
@IsOptional()
@ValidateIf((_, value) => typeof value === 'string' && value.length > 0)
@IsString()
@MinLength(8)
@MaxLength(72)
password?: string;
}
export class UpdateUserRoleDto {
@IsOptional()
@IsIn(ALL_ROLES)
+16
View File
@@ -15,6 +15,7 @@ import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
import {
InviteUserDto,
UpdateOrgUserDto,
UpdateUserCommissionDto,
UpdateUserRoleDto,
UpdateUserStatusDto,
@@ -50,6 +51,21 @@ export class UsersController {
return this.usersService.invite(user.organizationId, user.role, dto);
}
@Patch(':id')
@Roles('owner', 'admin')
updateProfile(
@CurrentUser() user: { organizationId: string; role: UserRole },
@Param('id') id: string,
@Body() dto: UpdateOrgUserDto,
) {
return this.usersService.updateProfile(
user.organizationId,
user.role,
id,
dto,
);
}
@Patch(':id/commission')
@Roles('owner', 'admin')
updateCommission(
+75
View File
@@ -9,6 +9,7 @@ import type {
DataVisibility,
InviteUserRequest,
OrgUserDto,
UpdateOrgUserRequest,
UserRole,
} from '@mento/shared';
import { DATA_VISIBILITIES } from '@mento/shared';
@@ -136,6 +137,80 @@ export class UsersService {
}
}
async updateProfile(
organizationId: string,
actorRole: UserRole,
targetId: string,
dto: UpdateOrgUserRequest,
): Promise<OrgUserDto> {
if (actorRole !== 'owner' && actorRole !== 'admin') {
throw new ForbiddenException('فقط مالک یا مدیر می‌تواند مشخصات کاربر را ویرایش کند');
}
const target = await this.memberships.findUserInOrgById(
organizationId,
targetId,
);
if (target.role === 'owner' && actorRole !== 'owner') {
throw new ForbiddenException('مدیر نمی‌تواند مشخصات مالک را تغییر دهد');
}
const fullName = dto.fullName.trim();
if (fullName.length < 2) {
throw new BadRequestException('نام باید حداقل دو حرف باشد');
}
const mobile = normalizeMobile(dto.mobile);
if (!mobile) {
throw new BadRequestException('شماره موبایل معتبر نیست');
}
const previousMobile = target.user.mobile ?? '';
if (mobile !== previousMobile) {
const taken = await this.prisma.user.findFirst({
where: { mobile, id: { not: targetId } },
select: { id: true },
});
if (taken) {
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
}
}
const data: Prisma.UserUpdateInput = { fullName, mobile };
const prevEmail = target.user.email;
const wasSynthetic =
prevEmail.endsWith('@users.mento.local') ||
prevEmail === inviteEmailFromMobile(previousMobile);
if (wasSynthetic && mobile !== previousMobile) {
const nextEmail = inviteEmailFromMobile(mobile);
const emailTaken = await this.prisma.user.findFirst({
where: { email: nextEmail, id: { not: targetId } },
select: { id: true },
});
if (!emailTaken) data.email = nextEmail;
}
const password = dto.password?.trim() ?? '';
if (password) {
if (password.length < 8) {
throw new BadRequestException('رمز عبور باید حداقل ۸ کاراکتر باشد');
}
data.passwordHash = await bcrypt.hash(password, 10);
}
try {
await this.prisma.user.update({
where: { id: targetId },
data,
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
throw new ConflictException('این شماره موبایل یا ایمیل قبلاً ثبت شده است');
}
throw e;
}
return this.dtoOf(organizationId, targetId);
}
async updateCommission(
organizationId: string,
actorRole: UserRole,