From ec1ec2e65f3bc5289c37a424c39fb131ccc9bc61 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:42:26 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20rewrite=20frontend=20auth=20=E2=80=94?= =?UTF-8?q?=20roles=20from=20org=20store,=20Logto=20org=20role=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace ID-token claim reads with org store lookups in useAuth and usePermissions; add currentOrgRoles to useOrgStore; update role names to Logto org role conventions (admin/member); remove username from Layout (no longer derived from token claims). Co-Authored-By: Claude Sonnet 4.6 --- ui/src/auth/useAuth.ts | 32 +++----------------------------- ui/src/auth/useOrganization.ts | 4 ++++ ui/src/components/Layout.tsx | 6 +++--- ui/src/hooks/usePermissions.ts | 15 +++++++++------ 4 files changed, 19 insertions(+), 38 deletions(-) diff --git a/ui/src/auth/useAuth.ts b/ui/src/auth/useAuth.ts index 3617b67..5ed9c7f 100644 --- a/ui/src/auth/useAuth.ts +++ b/ui/src/auth/useAuth.ts @@ -1,35 +1,11 @@ import { useLogto } from '@logto/react'; -import { useState, useEffect, useCallback } from 'react'; +import { useCallback } from 'react'; import { useOrgStore } from './useOrganization'; -interface IdTokenClaims { - sub?: string; - email?: string; - name?: string; - username?: string; - roles?: string[]; - organization_id?: string; - [key: string]: unknown; -} - export function useAuth() { - const { isAuthenticated, isLoading, getIdTokenClaims, signOut, signIn } = useLogto(); - const [claims, setClaims] = useState(null); + const { isAuthenticated, isLoading, signOut, signIn } = useLogto(); const { currentTenantId, isPlatformAdmin } = useOrgStore(); - useEffect(() => { - if (isAuthenticated) { - getIdTokenClaims().then((c) => setClaims(c as IdTokenClaims)); - } else { - setClaims(null); - } - }, [isAuthenticated, getIdTokenClaims]); - - const username = claims?.username ?? claims?.name ?? claims?.email ?? claims?.sub ?? null; - const roles = (claims?.roles as string[]) ?? []; - // tenantId is the DB UUID from the org store (set by OrgResolver after /api/me) - const tenantId = currentTenantId; - const logout = useCallback(() => { signOut(window.location.origin + '/login'); }, [signOut]); @@ -37,9 +13,7 @@ export function useAuth() { return { isAuthenticated, isLoading, - username, - roles, - tenantId, + tenantId: currentTenantId, isPlatformAdmin, logout, signIn, diff --git a/ui/src/auth/useOrganization.ts b/ui/src/auth/useOrganization.ts index 94c2030..8d12721 100644 --- a/ui/src/auth/useOrganization.ts +++ b/ui/src/auth/useOrganization.ts @@ -10,16 +10,19 @@ export interface OrgInfo { interface OrgState { currentOrgId: string | null; // Logto org ID — used for getAccessToken(resource, orgId) currentTenantId: string | null; // DB UUID — used for API calls like /api/tenants/{id} + currentOrgRoles: string[] | null; // Logto org roles for the current org (e.g. 'admin', 'member') organizations: OrgInfo[]; isPlatformAdmin: boolean; setCurrentOrg: (orgId: string | null) => void; setOrganizations: (orgs: OrgInfo[]) => void; setIsPlatformAdmin: (value: boolean) => void; + setCurrentOrgRoles: (roles: string[] | null) => void; } export const useOrgStore = create((set, get) => ({ currentOrgId: null, currentTenantId: null, + currentOrgRoles: null, organizations: [], isPlatformAdmin: false, setCurrentOrg: (orgId) => { @@ -36,4 +39,5 @@ export const useOrgStore = create((set, get) => ({ }); }, setIsPlatformAdmin: (value) => set({ isPlatformAdmin: value }), + setCurrentOrgRoles: (roles) => set({ currentOrgRoles: roles }), })); diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 9bb13c1..e81eda6 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -100,7 +100,7 @@ function PlatformIcon() { export function Layout() { const navigate = useNavigate(); - const { username, logout, isPlatformAdmin } = useAuth(); + const { logout, isPlatformAdmin } = useAuth(); const [envSectionOpen, setEnvSectionOpen] = useState(true); const [collapsed, setCollapsed] = useState(false); @@ -166,7 +166,7 @@ export function Layout() { {/* User info + logout */} } - label={username ?? 'Account'} + label="Account" onClick={logout} /> @@ -177,7 +177,7 @@ export function Layout() { diff --git a/ui/src/hooks/usePermissions.ts b/ui/src/hooks/usePermissions.ts index a6cc005..3859511 100644 --- a/ui/src/hooks/usePermissions.ts +++ b/ui/src/hooks/usePermissions.ts @@ -1,14 +1,17 @@ -import { useAuth } from '../auth/useAuth'; +import { useOrgStore } from '../auth/useOrganization'; const ROLE_PERMISSIONS: Record = { - OWNER: ['tenant:manage', 'billing:manage', 'team:manage', 'apps:manage', 'apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug', 'settings:manage'], - ADMIN: ['team:manage', 'apps:manage', 'apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug', 'settings:manage'], - DEVELOPER: ['apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug'], - VIEWER: ['observe:read'], + 'admin': [ + 'tenant:manage', 'billing:manage', 'team:manage', 'apps:manage', + 'apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug', + 'settings:manage', + ], + 'member': ['apps:deploy', 'observe:read', 'observe:debug'], }; export function usePermissions() { - const { roles } = useAuth(); + const { currentOrgRoles } = useOrgStore(); + const roles = currentOrgRoles ?? []; const permissions = new Set(); for (const role of roles) {