feat(portal): real Security & audit page (+ bundled Storage / per-tenant-roles WIP)
Security & audit (admin) - Audit log: real, tenant-scoped — widened GET /tenants/:slug/audit with q/action/outcome/actorEmail/since/before; UI gains search, outcome + time filters, action chips, cursor pagination, and client-side CSV export. - Security policy: new tenant.securityPolicy (mfaMode, session idle/absolute, allowedCountries, ipAllowlist) + PATCH /tenants/:slug/security-policy (membership-gated, audited). Editable, labelled by enforcement status. - MFA: live enrollment overview via GET /tenants/:slug/mfa-status (Authentik countAuthenticators per member). - SSO apps (Dezky as IdP): real Authentik OIDC provider + application CRUD, scoped to the tenant group. New AuthentikClient methods (provider/app/binding + flow/key/scope discovery), TenantSsoApp schema, TenantSsoService (rollback on partial failure; client secret never stored), GET/POST/DELETE /tenants/:slug/sso-apps. Validated end-to-end against live Authentik. - Deferred: shared-flow MFA/geo/session enforcement (global auth-flow blast radius) — to be done as its own reviewed change. Bundled in-progress work that shares the same files (kept together so the tree stays green): - Storage page: StorageService + GET /tenants/:slug/storage (OCIS-backed), storage.get proxy, storage.vue. - Per-tenant roles: User.tenantRoles + MeProfile.tenantRoles plumbing.
This commit is contained in:
@@ -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<string, 'owner' | 'admin' | 'member'>
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user