Wire operator Integrations page + capability-gating framework
Completes the arcadia-admin operator surface for the integration registry and the capability/route-guard framework it depends on. - Integration registry: route + Data-group nav entry + `platform.integrations` capability; the in-app client now delegates to the shared `@crema/integration-registry-client` lib (vite alias + tsconfig); the operator Integrations page (committed earlier) is now reachable. - Capability gating: capabilities map + route-guard + jwt helpers + the apps/plan/entitlements routes and supporting tenants/session changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,14 @@
|
||||
import { useEffect, useSyncExternalStore } from "react"
|
||||
|
||||
import { profileInitials } from "~/lib/profile"
|
||||
import { decodeJwt, type AvailableTenantClaim } from "~/lib/jwt"
|
||||
|
||||
export type AvailableTenant = {
|
||||
id: string
|
||||
slug?: string
|
||||
name?: string
|
||||
roles: string[]
|
||||
}
|
||||
|
||||
export type Session = {
|
||||
userId: string
|
||||
@@ -14,6 +22,11 @@ export type Session = {
|
||||
token: string
|
||||
// Issued at, ms since epoch.
|
||||
issuedAt: number
|
||||
// Active membership context — derived from the JWT.
|
||||
tenantId?: string
|
||||
tenantSlug?: string
|
||||
roles: string[]
|
||||
availableTenants: AvailableTenant[]
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "crema.session"
|
||||
@@ -41,6 +54,18 @@ function readFromStorage(): Session | null {
|
||||
token: parsed.token,
|
||||
issuedAt:
|
||||
typeof parsed.issuedAt === "number" ? parsed.issuedAt : Date.now(),
|
||||
tenantId: typeof parsed.tenantId === "string" ? parsed.tenantId : undefined,
|
||||
tenantSlug:
|
||||
typeof parsed.tenantSlug === "string" ? parsed.tenantSlug : undefined,
|
||||
roles: Array.isArray(parsed.roles)
|
||||
? parsed.roles.filter((r): r is string => typeof r === "string")
|
||||
: [],
|
||||
availableTenants: Array.isArray(parsed.availableTenants)
|
||||
? (parsed.availableTenants.filter(
|
||||
(t): t is AvailableTenant =>
|
||||
!!t && typeof (t as AvailableTenant).id === "string",
|
||||
) as AvailableTenant[])
|
||||
: [],
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@@ -72,12 +97,31 @@ export function persistFromArcadiaLogin(
|
||||
[user?.first_name, user?.last_name].filter(Boolean).join(" ") ||
|
||||
user?.email ||
|
||||
"Signed-in user"
|
||||
const claims = decodeJwt(tokens.access_token) ?? {}
|
||||
const availableTenants: AvailableTenant[] = Array.isArray(
|
||||
claims.available_tenants,
|
||||
)
|
||||
? (claims.available_tenants as AvailableTenantClaim[])
|
||||
.filter((t) => t && typeof t.id === "string")
|
||||
.map((t) => ({
|
||||
id: t.id as string,
|
||||
slug: t.slug,
|
||||
name: t.name,
|
||||
roles: Array.isArray(t.roles) ? t.roles : [],
|
||||
}))
|
||||
: []
|
||||
const session: Session = {
|
||||
userId: user?.id ?? `arcadia-${Date.now().toString(36)}`,
|
||||
name,
|
||||
email: user?.email ?? "",
|
||||
token: tokens.access_token,
|
||||
issuedAt: Date.now(),
|
||||
tenantId:
|
||||
typeof claims.tenant_id === "string" ? claims.tenant_id : undefined,
|
||||
tenantSlug:
|
||||
typeof claims.tenant_slug === "string" ? claims.tenant_slug : undefined,
|
||||
roles: Array.isArray(claims.roles) ? claims.roles : [],
|
||||
availableTenants,
|
||||
}
|
||||
if (typeof window !== "undefined") {
|
||||
sessionStorage.setItem("arcadia_access_token", tokens.access_token)
|
||||
|
||||
Reference in New Issue
Block a user