feat: replace hardcoded permission map with direct OAuth2 scope checks

Remove role-to-permission mapping (usePermissions, RequirePermission) and replace
with direct scope reads from the Logto access token JWT. OrgResolver decodes the
scope claim after /api/me resolves and stores scopes in Zustand. RequireScope and
useScopes replace the old hooks/components across all pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-05 14:04:06 +02:00
parent 277d5ea638
commit 9c2a1d27b7
13 changed files with 87 additions and 97 deletions

View File

@@ -32,9 +32,9 @@ import {
useLogs,
useCreateApp,
} from '../api/hooks';
import { RequirePermission } from '../components/RequirePermission';
import { RequireScope } from '../components/RequireScope';
import { DeploymentStatusBadge } from '../components/DeploymentStatusBadge';
import { usePermissions } from '../hooks/usePermissions';
import { useScopes } from '../auth/useScopes';
import type { DeploymentResponse } from '../types/api';
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -119,7 +119,9 @@ export function AppDetailPage() {
const navigate = useNavigate();
const { toast } = useToast();
const { tenantId } = useAuth();
const { canManageApps, canDeploy } = usePermissions();
const scopes = useScopes();
const canManageApps = scopes.has('apps:manage');
const canDeploy = scopes.has('apps:deploy');
// Active tab
const [activeTab, setActiveTab] = useState('overview');
@@ -399,7 +401,7 @@ export function AppDetailPage() {
{/* Action bar */}
<Card title="Actions">
<div className="flex flex-wrap gap-2 py-2">
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button
variant="primary"
size="sm"
@@ -408,9 +410,9 @@ export function AppDetailPage() {
>
Deploy
</Button>
</RequirePermission>
</RequireScope>
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button
variant="secondary"
size="sm"
@@ -420,9 +422,9 @@ export function AppDetailPage() {
>
Restart
</Button>
</RequirePermission>
</RequireScope>
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button
variant="secondary"
size="sm"
@@ -431,9 +433,9 @@ export function AppDetailPage() {
>
Stop
</Button>
</RequirePermission>
</RequireScope>
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button
variant="secondary"
size="sm"
@@ -445,9 +447,9 @@ export function AppDetailPage() {
>
Re-upload JAR
</Button>
</RequirePermission>
</RequireScope>
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button
variant="danger"
size="sm"
@@ -455,7 +457,7 @@ export function AppDetailPage() {
>
Delete App
</Button>
</RequirePermission>
</RequireScope>
</div>
</Card>
@@ -552,11 +554,11 @@ export function AppDetailPage() {
</div>
)}
</div>
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button variant="secondary" size="sm" onClick={openRoutingModal}>
Edit Routing
</Button>
</RequirePermission>
</RequireScope>
</div>
</Card>
</div>

View File

@@ -10,7 +10,7 @@ import {
} from '@cameleer/design-system';
import { useAuth } from '../auth/useAuth';
import { useTenant, useEnvironments, useApps } from '../api/hooks';
import { RequirePermission } from '../components/RequirePermission';
import { RequireScope } from '../components/RequireScope';
import type { EnvironmentResponse, AppResponse } from '../types/api';
// Helper: fetches apps for one environment and reports data upward via effect
@@ -126,7 +126,7 @@ export function DashboardPage() {
)}
</div>
<div className="flex items-center gap-2">
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button
variant="secondary"
size="sm"
@@ -134,7 +134,7 @@ export function DashboardPage() {
>
New Environment
</Button>
</RequirePermission>
</RequireScope>
<Button
variant="primary"
size="sm"
@@ -193,11 +193,11 @@ export function DashboardPage() {
title="No environments yet"
description="Create your first environment to get started deploying Camel applications."
action={
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button variant="primary" onClick={() => navigate('/environments/new')}>
Create Environment
</Button>
</RequirePermission>
</RequireScope>
}
/>
)}

View File

@@ -23,7 +23,7 @@ import {
useApps,
useCreateApp,
} from '../api/hooks';
import { RequirePermission } from '../components/RequirePermission';
import { RequireScope } from '../components/RequireScope';
import { DeploymentStatusBadge } from '../components/DeploymentStatusBadge';
import type { AppResponse } from '../types/api';
@@ -188,7 +188,7 @@ export function EnvironmentDetailPage() {
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<RequirePermission
<RequireScope
permission="apps:manage"
fallback={
<h1 className="text-2xl font-semibold text-white">
@@ -201,7 +201,7 @@ export function EnvironmentDetailPage() {
onSave={handleRename}
placeholder="Environment name"
/>
</RequirePermission>
</RequireScope>
<Badge label={environment.slug} color="primary" variant="outlined" />
<Badge
label={environment.status}
@@ -209,12 +209,12 @@ export function EnvironmentDetailPage() {
/>
</div>
<div className="flex items-center gap-2">
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button variant="primary" size="sm" onClick={openNewApp}>
New App
</Button>
</RequirePermission>
<RequirePermission permission="apps:manage">
</RequireScope>
<RequireScope scope="apps:manage">
<Button
variant="danger"
size="sm"
@@ -224,7 +224,7 @@ export function EnvironmentDetailPage() {
>
Delete Environment
</Button>
</RequirePermission>
</RequireScope>
</div>
</div>
@@ -234,11 +234,11 @@ export function EnvironmentDetailPage() {
title="No apps yet"
description="Deploy your first Camel application to this environment."
action={
<RequirePermission permission="apps:deploy">
<RequireScope scope="apps:deploy">
<Button variant="primary" onClick={openNewApp}>
New App
</Button>
</RequirePermission>
</RequireScope>
}
/>
) : (

View File

@@ -15,7 +15,7 @@ import {
import type { Column } from '@cameleer/design-system';
import { useAuth } from '../auth/useAuth';
import { useEnvironments, useCreateEnvironment } from '../api/hooks';
import { RequirePermission } from '../components/RequirePermission';
import { RequireScope } from '../components/RequireScope';
import type { EnvironmentResponse } from '../types/api';
interface TableRow {
@@ -120,11 +120,11 @@ export function EnvironmentsPage() {
{/* Page header */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-white">Environments</h1>
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button variant="primary" size="sm" onClick={openModal}>
Create Environment
</Button>
</RequirePermission>
</RequireScope>
</div>
{/* Table / empty state */}
@@ -133,11 +133,11 @@ export function EnvironmentsPage() {
title="No environments yet"
description="Create your first environment to start deploying Camel applications."
action={
<RequirePermission permission="apps:manage">
<RequireScope scope="apps:manage">
<Button variant="primary" onClick={openModal}>
Create Environment
</Button>
</RequirePermission>
</RequireScope>
}
/>
) : (