feat: bootstrap 2 users, tenant, org-scoped tokens, platform admin UI
Bootstrap script now creates: - SaaS Owner (admin/admin) with platform-admin role - Tenant Admin (camel/camel) in Example Tenant org - Traditional Web App for cameleer3-server OIDC - DB records: tenant, default environment, license - Configures cameleer3-server OIDC via its admin API All credentials configurable via env vars. Backend: - Fix LogtoManagementClient resource URL (https://default.logto.app/api) - Add getUserRoles/getUserOrganizations to LogtoManagementClient - Add GET /api/me endpoint (user info, platform admin status, tenants) - Add GET /api/tenants list-all for platform admins - Remove insecure X-header forwarding from Traefik Frontend: - Org-scoped tokens: getAccessToken(resource, orgId) for tenant context - OrgResolver component populates org store from /api/me - useOrganization Zustand store (currentOrgId + currentTenantId) - Platform admin sidebar section + AdminTenantsPage - View Dashboard link points to cameleer3-server on port 8081 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
75
ui/src/pages/AdminTenantsPage.tsx
Normal file
75
ui/src/pages/AdminTenantsPage.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useNavigate } from 'react-router';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
DataTable,
|
||||
Spinner,
|
||||
} from '@cameleer/design-system';
|
||||
import type { Column } from '@cameleer/design-system';
|
||||
import { useAllTenants } from '../api/hooks';
|
||||
import { useOrgStore } from '../auth/useOrganization';
|
||||
import type { TenantResponse } from '../types/api';
|
||||
|
||||
const columns: Column<TenantResponse>[] = [
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'slug', header: 'Slug' },
|
||||
{
|
||||
key: 'tier',
|
||||
header: 'Tier',
|
||||
render: (_v: unknown, row: TenantResponse) => <Badge label={row.tier} color="primary" />,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
header: 'Status',
|
||||
render: (_v: unknown, row: TenantResponse) => (
|
||||
<Badge
|
||||
label={row.status}
|
||||
color={row.status === 'ACTIVE' ? 'success' : 'warning'}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{ key: 'createdAt', header: 'Created' },
|
||||
];
|
||||
|
||||
export function AdminTenantsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data: tenants, isLoading } = useAllTenants();
|
||||
const { setCurrentOrg } = useOrgStore();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleRowClick = (tenant: TenantResponse) => {
|
||||
// Find the matching org from the store and switch context
|
||||
const orgs = useOrgStore.getState().organizations;
|
||||
const match = orgs.find(
|
||||
(o) => o.name === tenant.name || o.slug === tenant.slug,
|
||||
);
|
||||
if (match) {
|
||||
setCurrentOrg(match.id);
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-semibold text-white">All Tenants</h1>
|
||||
<Badge label="Platform Admin" color="warning" />
|
||||
</div>
|
||||
|
||||
<Card title={`${tenants?.length ?? 0} Tenants`}>
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={tenants ?? []}
|
||||
onRowClick={handleRowClick}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user