diff --git a/.env.example b/.env.example index 83cd43e..35147fd 100644 --- a/.env.example +++ b/.env.example @@ -52,6 +52,12 @@ STALWART_ADMIN_PASSWORD=changeme_use_openssl_rand # OCIS # ──────────────────────────────────────── OCIS_ADMIN_PASSWORD=changeme_use_openssl_rand +# Dedicated OCIS service user (Authentik) used by platform-api to read drive +# quotas for the Storage page via an OIDC password grant. Must exist in +# Authentik, have access to the OCIS application, and hold the OCIS admin role +# (required to list all drives). See docs/NEXT-STEPS.md. +OCIS_SVC_USERNAME=svc-platform-api +OCIS_SVC_PASSWORD=changeme_use_openssl_rand # ──────────────────────────────────────── # Collabora diff --git a/apps/portal/composables/useMe.ts b/apps/portal/composables/useMe.ts index 166358e..de0d838 100644 --- a/apps/portal/composables/useMe.ts +++ b/apps/portal/composables/useMe.ts @@ -9,6 +9,9 @@ interface MeProfile { email: string name: string role: string + // Per-tenant role overrides keyed by tenantId; absent keys fall back to + // `role`. Serialized from platform-api's User.tenantRoles Map. + tenantRoles?: Record active: boolean platformAdmin: boolean tenantIds: string[] @@ -65,11 +68,39 @@ export function useMe() { const partner = computed(() => profile.value?.partner ?? null) const isPartnerStaff = computed(() => !!profile.value?.partnerId) const isPlatformAdmin = computed(() => !!profile.value?.platformAdmin) - // Customer admin of their own workspace — gates access to the /admin surface. - // `role` is 'owner' | 'admin' | 'member' from platform-api (User.role). - const isTenantAdmin = computed( - () => profile.value?.role === 'owner' || profile.value?.role === 'admin', - ) - return { state, profile, partner, isPartnerStaff, isPlatformAdmin, isTenantAdmin, fetchMe } + const isAdminRole = (r: string | undefined) => r === 'owner' || r === 'admin' + + // Effective role for a specific tenant — mirrors platform-api roleForTenant(): + // a per-tenant entry wins, else the legacy global `role`, else 'member'. + function roleForTenant(tenantId: string): 'owner' | 'admin' | 'member' { + const p = profile.value + return p?.tenantRoles?.[tenantId] ?? (p?.role as 'owner' | 'admin' | 'member') ?? 'member' + } + function isTenantAdminOf(tenantId: string): boolean { + return isAdminRole(roleForTenant(tenantId)) + } + + // Gates the /admin surface: true if the user administers AT LEAST ONE of + // their tenants. Per-tenant enforcement of *which* workspace they may admin + // happens once a tenant is in context (backend membership + roleForTenant). + // For existing single-role data this is identical to the old global check. + const isTenantAdmin = computed(() => { + const p = profile.value + if (!p) return false + if (p.tenantIds.length) return p.tenantIds.some((t) => isTenantAdminOf(t)) + return isAdminRole(p.role) + }) + + return { + state, + profile, + partner, + isPartnerStaff, + isPlatformAdmin, + isTenantAdmin, + roleForTenant, + isTenantAdminOf, + fetchMe, + } } diff --git a/apps/portal/pages/admin/security.vue b/apps/portal/pages/admin/security.vue index 32e5410..a73b24d 100644 --- a/apps/portal/pages/admin/security.vue +++ b/apps/portal/pages/admin/security.vue @@ -1,42 +1,135 @@