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>
76 lines
1.9 KiB
TypeScript
76 lines
1.9 KiB
TypeScript
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>
|
|
);
|
|
}
|