feat(access): keep a single owner per organization

The person who registered stays the only owner. Full day-to-day access is granted through the manager role instead of a second owner.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-10 14:59:32 +03:30
co-authored by Cursor
parent 932dc3f1c1
commit 25a24772fc
7 changed files with 56 additions and 13 deletions
+15 -3
View File
@@ -104,8 +104,10 @@ export class UsersService {
orgRoleId: dto.orgRoleId,
role: dto.role,
});
if (resolved.role === 'owner' && actorRole !== 'owner') {
throw new ForbiddenException('فقط مالک می‌تواند نقش مدیر سیستم را واگذار کند');
if (resolved.role === 'owner') {
throw new BadRequestException(
'هر محیط فقط یک مالک دارد و مالک همان کسی است که ثبت‌نام کرده. برای دسترسی کامل نقش مدیر را انتخاب کنید.',
);
}
const existingUser = await this.prisma.user.findFirst({ where: { mobile } });
@@ -449,6 +451,10 @@ export class UsersService {
}
}
if (nextRole === 'owner' && target.role !== 'owner') {
await this.memberships.assertSoleOwner(organizationId, target.userId);
}
if (target.role === 'owner' && nextRole !== 'owner') {
const owners = await this.memberships.countActiveOwners(
organizationId,
@@ -456,7 +462,7 @@ export class UsersService {
);
if (owners < 1) {
throw new BadRequestException(
'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید',
'مالک محیط همان کسی است که ثبت‌نام کرده و نمی‌توان نقش او را عوض کرد',
);
}
}
@@ -528,6 +534,9 @@ export class UsersService {
}
}
if (isActive && !target.isActive) {
if (target.role === 'owner') {
await this.memberships.assertSoleOwner(organizationId, targetId);
}
await this.billing.assertSeatAvailable(organizationId, targetId);
}
await this.prisma.membership.update({
@@ -558,6 +567,9 @@ export class UsersService {
resolved: { role: UserRole; orgRoleId: string },
): Promise<OrgUserDto> {
await this.billing.assertSeatAvailable(organizationId, userId);
if (resolved.role === 'owner') {
await this.memberships.assertSoleOwner(organizationId, userId);
}
const existing = await this.prisma.membership.findFirst({
where: { userId, organizationId },
});