From 0e580b19c39e5a5ef3f74953c33e81e1dad654e1 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sun, 20 Sep 2026 12:07:05 +0330 Subject: [PATCH] Editing Super Admin permissions and visual changes --- apps/api/src/auth/auth.service.ts | 3 + apps/api/src/billing/billing.service.ts | 35 +- apps/api/src/platform/platform.service.ts | 11 +- apps/web/src/layouts/AdmLayout.vue | 380 ++++++++++++++---- apps/web/src/views/adm/AdmAdminsView.vue | 27 +- apps/web/src/views/adm/AdmDashboardView.vue | 282 ++++++++----- .../src/views/adm/AdmOrganizationsView.vue | 58 ++- apps/web/src/views/adm/AdmUsersView.vue | 51 ++- packages/shared/src/index.ts | 5 + 9 files changed, 621 insertions(+), 231 deletions(-) diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 9e88530..d366ae7 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -651,6 +651,7 @@ export class AuthService { membership?.organization.enabledModules, ), subscription: sub, + isOrgOwner: role === 'owner', }); } @@ -737,6 +738,7 @@ export class AuthService { avatarStorageKey?: string | null; enabledModules?: OrgModuleKey[]; subscription?: SubscriptionSummaryDto | null; + isOrgOwner?: boolean; }): AuthUserDto { return { id: user.id, @@ -749,6 +751,7 @@ export class AuthService { ...(user.mobile ? { mobile: user.mobile } : {}), ...(user.enabledModules ? { enabledModules: user.enabledModules } : {}), ...(user.subscription ? { subscription: user.subscription } : {}), + ...(user.isOrgOwner !== undefined ? { isOrgOwner: user.isOrgOwner } : {}), }; } } diff --git a/apps/api/src/billing/billing.service.ts b/apps/api/src/billing/billing.service.ts index f12e08e..bf61483 100644 --- a/apps/api/src/billing/billing.service.ts +++ b/apps/api/src/billing/billing.service.ts @@ -94,7 +94,7 @@ export class BillingService { }); } - /** اشتراک محیط = BillingAccount اولین مالک فعال */ + /** اشتراک محیط = BillingAccount اولین مالک فعال — برای عضو زیرمجموعه حساب جدید ساخته نمی‌شود */ async resolveForOrganization(organizationId: string) { const owner = await this.prisma.membership.findFirst({ where: { @@ -106,25 +106,25 @@ export class BillingService { select: { userId: true }, }); if (!owner) return null; - let account = await this.getByOwnerUserId(owner.userId); - if (!account) { - account = await this.ensureTrialForOwner(owner.userId); - } - return account; + return this.getByOwnerUserId(owner.userId); } async requireActiveForOrganization(organizationId: string): Promise { const account = await this.resolveForOrganization(organizationId); if (!account) { - throw new UnauthorizedException('اشتراک این محیط یافت نشد'); + throw new UnauthorizedException( + 'اشتراک این محیط یافت نشد؛ مالک باید از پنل سوپرادمین پلن داشته باشد', + ); } return this.assertActive(account); } async requireActiveForOwner(ownerUserId: string): Promise { - let account = await this.getByOwnerUserId(ownerUserId); + const account = await this.getByOwnerUserId(ownerUserId); if (!account) { - account = await this.ensureTrialForOwner(ownerUserId); + throw new UnauthorizedException( + 'اشتراک یافت نشد؛ ابتدا پلن را از سوپرادمین اعمال کنید', + ); } return this.assertActive(account); } @@ -154,7 +154,12 @@ export class BillingService { } async assertCanCreateOrganization(ownerUserId: string) { - const summary = await this.requireActiveForOwner(ownerUserId); + let account = await this.getByOwnerUserId(ownerUserId); + if (!account) { + // اولین محیط مالک: trial خودکار + account = await this.ensureTrialForOwner(ownerUserId, { maxOrganizations: 1 }); + } + const summary = await this.assertActive(account); const ownedCount = await this.prisma.membership.count({ where: { userId: ownerUserId, role: 'owner', isActive: true }, }); @@ -169,6 +174,16 @@ export class BillingService { ownerUserId: string, dto: ApplyBillingPlanRequest, ): Promise { + const ownerMembership = await this.prisma.membership.findFirst({ + where: { userId: ownerUserId, role: 'owner', isActive: true }, + select: { id: true }, + }); + if (!ownerMembership) { + throw new BadRequestException( + 'اشتراک فقط برای مالک محیط تعریف می‌شود؛ این کاربر مالک هیچ محیطی نیست', + ); + } + const planCode = dto.planCode; const now = new Date(); let expiresAt: Date; diff --git a/apps/api/src/platform/platform.service.ts b/apps/api/src/platform/platform.service.ts index 3bf6b46..b034e12 100644 --- a/apps/api/src/platform/platform.service.ts +++ b/apps/api/src/platform/platform.service.ts @@ -688,6 +688,9 @@ export class PlatformService { organization: { id: string; name: string; isActive: boolean }; }>; }): PlatformUserDto { + const isBillingOwner = row.memberships.some( + (m) => m.role === 'owner' && m.isActive, + ); return { id: row.id, email: row.email, @@ -696,9 +699,11 @@ export class PlatformService { isActive: row.isActive, lastLoginAt: row.lastLoginAt?.toISOString() ?? null, createdAt: row.createdAt.toISOString(), - billing: row.billingAccount - ? this.billing.toDto(row.billingAccount) - : null, + isBillingOwner, + billing: + isBillingOwner && row.billingAccount + ? this.billing.toDto(row.billingAccount) + : null, memberships: row.memberships.map((m) => ({ membershipId: m.id, organizationId: m.organization.id, diff --git a/apps/web/src/layouts/AdmLayout.vue b/apps/web/src/layouts/AdmLayout.vue index 3efe177..2e2d4b8 100644 --- a/apps/web/src/layouts/AdmLayout.vue +++ b/apps/web/src/layouts/AdmLayout.vue @@ -1,6 +1,6 @@