load-workspace.ts118 lines · main
1import { apiFetch, apiJson } from '../../../../../lib/api';
2
3import type { AuthV2ProjectRow } from './auth-v2-types';
4
5/**
6 * Server-side load of Auth workspace (all projects + enable flags).
7 * Prefers briven-engine `/v1/auth-core/workspace`, then merges tenants list
8 * so "Auth on" never lags behind be_tenants.
9 */
10export async function loadAuthV2Workspace(): Promise<AuthV2ProjectRow[]> {
11 let projects: AuthV2ProjectRow[] = [];
12
13 try {
14 const data = await apiJson<{
15 projects?: AuthV2ProjectRow[];
16 }>(`/v1/auth-core/workspace?_=${Date.now()}`);
17 if (data.projects?.length) projects = data.projects;
18 } catch {
19 /* fall through */
20 }
21
22 if (projects.length === 0) {
23 try {
24 const data = await apiJson<{
25 projects?: AuthV2ProjectRow[];
26 }>('/v1/auth-v2/workspace');
27 if (data.projects?.length) projects = data.projects;
28 } catch {
29 /* fall through */
30 }
31 }
32
33 if (projects.length === 0) {
34 try {
35 const data = await apiJson<{
36 projects: Array<{ id: string; slug: string; name: string }>;
37 }>('/v1/projects');
38 projects = (data.projects ?? []).map((p) => ({
39 id: p.id,
40 slug: p.slug,
41 name: p.name,
42 authEnabled: false,
43 tenantId: null,
44 providers: null,
45 }));
46 } catch {
47 return [];
48 }
49 }
50
51 // Merge live *active* tenants (API excludes soft-disabled rows).
52 // Active list is the source of truth for on/off so "disable Auth" cannot be
53 // flipped back on just because the tenant row still exists in the database.
54 try {
55 const res = await apiFetch('/v1/auth-core/tenants');
56 if (res.ok) {
57 const body = (await res.json()) as {
58 tenants?: Array<{
59 projectId?: string;
60 tenantId?: string;
61 authEnabled?: boolean;
62 }>;
63 tenantIds?: string[];
64 };
65 const byProject = new Map<string, string>();
66 const tenantSet = new Set<string>();
67 for (const t of body.tenants ?? []) {
68 if (t.authEnabled === false) continue;
69 if (t.tenantId) tenantSet.add(t.tenantId);
70 if (t.projectId) {
71 byProject.set(t.projectId, t.tenantId ?? '');
72 byProject.set(t.projectId.toLowerCase(), t.tenantId ?? '');
73 }
74 }
75 for (const tid of body.tenantIds ?? []) tenantSet.add(tid);
76
77 const activeListLoaded =
78 (body.tenants?.length ?? 0) > 0 || (body.tenantIds?.length ?? 0) > 0;
79
80 projects = projects.map((p) => {
81 const tid =
82 byProject.get(p.id) ||
83 byProject.get(p.id.toLowerCase()) ||
84 p.tenantId ||
85 null;
86 // Map rule: tenant id is always proj-{normalized project id}
87 const mapped = `proj-${p.id
88 .trim()
89 .toLowerCase()
90 .replace(/_/g, '-')
91 .replace(/[^a-z0-9-]/g, '-')
92 .replace(/-+/g, '-')
93 .replace(/^-|-$/g, '')}`.slice(0, 64);
94 const activeTenant =
95 byProject.has(p.id) ||
96 byProject.has(p.id.toLowerCase()) ||
97 (mapped ? tenantSet.has(mapped) : false) ||
98 (p.tenantId ? tenantSet.has(p.tenantId) : false);
99
100 // If we have an active-tenants list, it wins (soft-disable safe).
101 // If the list is empty (no Auth anywhere, or total outage), keep workspace.
102 const on = activeListLoaded
103 ? activeTenant
104 : p.authEnabled === true || activeTenant;
105
106 return {
107 ...p,
108 authEnabled: on,
109 tenantId: on ? tid || mapped || p.tenantId || null : null,
110 };
111 });
112 }
113 } catch {
114 /* keep projects as-is */
115 }
116
117 return projects;
118}