From 25a24772fce4304c3208e856fd1418450d1dedc0 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Tue, 10 Nov 2026 14:59:32 +0330 Subject: [PATCH] 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 --- .../api/src/memberships/memberships.service.ts | 14 ++++++++++++++ apps/api/src/platform/platform.service.ts | 8 +++++++- apps/api/src/users/users.service.ts | 18 +++++++++++++++--- apps/web/src/views/TeamSettingsView.vue | 8 ++++---- .../src/views/adm/AdmBillingSettingsView.vue | 3 ++- .../web/src/views/adm/AdmOrganizationsView.vue | 9 +++++++-- apps/web/src/views/adm/AdmUsersView.vue | 9 +++++++-- 7 files changed, 56 insertions(+), 13 deletions(-) diff --git a/apps/api/src/memberships/memberships.service.ts b/apps/api/src/memberships/memberships.service.ts index 1115cb6..6d7ca33 100644 --- a/apps/api/src/memberships/memberships.service.ts +++ b/apps/api/src/memberships/memberships.service.ts @@ -1,4 +1,5 @@ import { + BadRequestException, ForbiddenException, Injectable, NotFoundException, @@ -70,6 +71,16 @@ export class MembershipsService { })); } + /** هر محیط یک مالک فعال دارد: همان کسی که محیط را ثبت کرده. */ + async assertSoleOwner(organizationId: string, userId: string) { + const others = await this.countActiveOwners(organizationId, userId); + if (others > 0) { + throw new BadRequestException( + 'هر محیط فقط یک مالک دارد و مالک همان کسی است که ثبت‌نام کرده. برای دسترسی کامل نقش مدیر را انتخاب کنید.', + ); + } + } + async createMembership(data: { userId: string; organizationId: string; @@ -78,6 +89,9 @@ export class MembershipsService { commissionPercent?: number; isActive?: boolean; }) { + if (data.role === 'owner' && data.isActive !== false) { + await this.assertSoleOwner(data.organizationId, data.userId); + } return this.prisma.membership.create({ data: { userId: data.userId, diff --git a/apps/api/src/platform/platform.service.ts b/apps/api/src/platform/platform.service.ts index a618dcc..c43407e 100644 --- a/apps/api/src/platform/platform.service.ts +++ b/apps/api/src/platform/platform.service.ts @@ -543,6 +543,9 @@ export class PlatformService { }); if (!row) throw new NotFoundException('عضویت پیدا نشد'); if (isActive && !row.isActive) { + if (row.role === 'owner') { + await this.memberships.assertSoleOwner(row.organizationId, row.userId); + } await this.billing.assertSeatAvailable(row.organizationId, row.userId); } if (!isActive && row.role === 'owner') { @@ -573,6 +576,9 @@ export class PlatformService { }); if (!row) throw new NotFoundException('عضویت پیدا نشد'); + if (role === 'owner' && row.role !== 'owner') { + await this.memberships.assertSoleOwner(row.organizationId, row.userId); + } if (row.role === 'owner' && role !== 'owner') { const owners = await this.memberships.countActiveOwners( row.organizationId, @@ -580,7 +586,7 @@ export class PlatformService { ); if (owners < 1) { throw new BadRequestException( - 'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید', + 'مالک محیط همان کسی است که ثبت‌نام کرده و نمی‌توان نقش او را عوض کرد', ); } } diff --git a/apps/api/src/users/users.service.ts b/apps/api/src/users/users.service.ts index ded01ea..ed78944 100644 --- a/apps/api/src/users/users.service.ts +++ b/apps/api/src/users/users.service.ts @@ -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 { 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 }, }); diff --git a/apps/web/src/views/TeamSettingsView.vue b/apps/web/src/views/TeamSettingsView.vue index ba9a5ea..9f54227 100644 --- a/apps/web/src/views/TeamSettingsView.vue +++ b/apps/web/src/views/TeamSettingsView.vue @@ -138,7 +138,7 @@ const editPassword = ref(''); const editRoleLabel = ref(''); const editRoleId = ref(''); const editInitialRoleId = ref(''); -/** مدیر نمی‌تواند نقش مالک را عوض کند؛ آن موقع فقط برچسب نقش نشان داده می‌شود */ +/** نقش مالک قفل است؛ مالک همان ثبت‌نام‌کننده می‌ماند */ const editRoleLocked = ref(false); const roleFormOpen = ref(false); @@ -253,7 +253,7 @@ const parentRoleOptions = computed(() => { }); const inviteableRoles = computed(() => - roles.value.filter((r) => r.systemKey !== 'owner' || auth.user?.role === 'owner'), + roles.value.filter((r) => r.systemKey !== 'owner'), ); function roleTreeLabel(role: OrgRoleDto) { @@ -482,7 +482,7 @@ function openEditUser(user: OrgUserDto) { editRoleLabel.value = user.orgRoleName || '—'; editRoleId.value = user.orgRoleId ?? ''; editInitialRoleId.value = user.orgRoleId ?? ''; - editRoleLocked.value = user.role === 'owner' && auth.user?.role !== 'owner'; + editRoleLocked.value = user.role === 'owner'; editOpen.value = true; error.value = ''; ok.value = ''; @@ -1517,7 +1517,7 @@ onUnmounted(() => { -

نقش فعلی: {{ editRoleLabel }} — نقش مالک را فقط مالک می‌تواند تغییر دهد.

+

نقش فعلی: {{ editRoleLabel }} — مالک همان کسی است که ثبت‌نام کرده و عوض نمی‌شود. برای دسترسی کامل نقش مدیر را بدهید.