workspace.ts311 lines · main
1/**
2 * briven-engine dashboard workspace — projects the operator can manage,
3 * with Auth on/off based on be_tenants (Doltgres).
4 */
5
6import { listProjectsForUser } from '../projects.js';
7import { getEnginePool } from './db.js';
8import { isAuthCoreInitialized } from './engine.js';
9import { ensureBrivenEngineTenant } from './multitenancy.js';
10import { getBrivenEngineProjectConfig } from './project-config.js';
11import { mapProjectToAuthCore } from './project-map.js';
12
13export type BrivenEngineWorkspaceProject = {
14 id: string;
15 slug: string;
16 name: string;
17 authEnabled: boolean;
18 tenantId: string | null;
19 providers: {
20 emailPassword: boolean;
21 magicLink: boolean;
22 emailOtp: boolean;
23 passkey: boolean;
24 } | null;
25 error?: boolean;
26};
27
28/**
29 * Enable Auth for a project = create briven-engine tenant island on Doltgres.
30 * Idempotent. Re-enables if previously soft-disabled.
31 */
32export async function enableBrivenEngineAuth(projectId: string): Promise<{
33 ok: boolean;
34 engine: 'briven-engine';
35 projectId: string;
36 tenantId: string;
37 authEnabled: boolean;
38 created: boolean;
39 message?: string;
40 storage: 'doltgres';
41}> {
42 const result = await ensureBrivenEngineTenant(projectId);
43 if (result.ok) {
44 try {
45 const pool = getEnginePool();
46 // Clear soft-disable so Auth is on again (users/data stay intact).
47 await pool.query(
48 `UPDATE be_tenants SET disabled_at = NULL
49 WHERE tenant_id = $1 OR project_id = $2`,
50 [result.tenantId, result.projectId],
51 );
52 } catch {
53 /* column may not exist yet on very old engines — treat as enabled */
54 }
55 }
56 return {
57 ok: result.ok,
58 engine: 'briven-engine',
59 projectId: result.projectId,
60 tenantId: result.tenantId,
61 authEnabled: result.ok,
62 created: result.created,
63 message: result.message,
64 storage: 'doltgres',
65 };
66}
67
68/**
69 * Turn Auth off for a project without deleting end-users or credentials.
70 * Soft-disable: tenant stays, disabled_at is set; app login should treat Auth as off.
71 */
72export async function disableBrivenEngineAuth(projectId: string): Promise<{
73 ok: boolean;
74 engine: 'briven-engine';
75 projectId: string;
76 tenantId: string;
77 authEnabled: boolean;
78 message?: string;
79 storage: 'doltgres';
80}> {
81 const map = mapProjectToAuthCore(projectId);
82 const base = {
83 engine: 'briven-engine' as const,
84 storage: 'doltgres' as const,
85 projectId: map.projectId,
86 tenantId: map.tenantId,
87 };
88 if (!isAuthCoreInitialized()) {
89 return {
90 ...base,
91 ok: false,
92 authEnabled: false,
93 message: 'briven-engine not ready on Doltgres',
94 };
95 }
96 try {
97 const pool = getEnginePool();
98 // Ensure soft-disable column exists (older engines may not have run migration).
99 try {
100 await pool.query(`ALTER TABLE be_tenants ADD COLUMN disabled_at TIMESTAMPTZ`);
101 } catch {
102 /* already exists or unsupported — continue */
103 }
104 const existing = await pool.query(
105 `SELECT tenant_id FROM be_tenants
106 WHERE tenant_id = $1 OR project_id = $2
107 LIMIT 1`,
108 [map.tenantId, map.projectId],
109 );
110 if (!existing.rowCount) {
111 return {
112 ...base,
113 ok: true,
114 authEnabled: false,
115 message: 'Auth was already off for this project',
116 };
117 }
118 try {
119 await pool.query(
120 `UPDATE be_tenants SET disabled_at = NOW()
121 WHERE tenant_id = $1 OR project_id = $2`,
122 [map.tenantId, map.projectId],
123 );
124 } catch (err) {
125 // Fallback if disabled_at column missing: leave row (still "on") and report.
126 const message = err instanceof Error ? err.message : String(err);
127 return {
128 ...base,
129 ok: false,
130 authEnabled: true,
131 message: `could not disable Auth: ${message}`,
132 };
133 }
134 return {
135 ...base,
136 ok: true,
137 authEnabled: false,
138 message:
139 'Auth disabled for this project. User data is kept — enable Auth again anytime.',
140 };
141 } catch (err) {
142 return {
143 ...base,
144 ok: false,
145 authEnabled: true,
146 message: err instanceof Error ? err.message : String(err),
147 };
148 }
149}
150
151/**
152 * Whether Auth is on for a project (tenant row exists and not soft-disabled).
153 */
154export async function isBrivenEngineAuthEnabled(
155 projectId: string,
156): Promise<boolean> {
157 if (!isAuthCoreInitialized()) return false;
158 try {
159 const map = mapProjectToAuthCore(projectId);
160 const pool = getEnginePool();
161 // Prefer disabled_at IS NULL; if column missing, any tenant row means on.
162 try {
163 const res = await pool.query(
164 `SELECT 1 FROM be_tenants
165 WHERE (tenant_id = $1 OR project_id = $2)
166 AND disabled_at IS NULL
167 LIMIT 1`,
168 [map.tenantId, map.projectId],
169 );
170 return Boolean(res.rowCount && res.rowCount > 0);
171 } catch {
172 const res = await pool.query(
173 `SELECT 1 FROM be_tenants
174 WHERE tenant_id = $1 OR project_id = $2
175 LIMIT 1`,
176 [map.tenantId, map.projectId],
177 );
178 return Boolean(res.rowCount && res.rowCount > 0);
179 }
180 } catch {
181 return false;
182 }
183}
184
185function markEnabled(
186 set: Set<string>,
187 projectId: string | null | undefined,
188): void {
189 if (!projectId) return;
190 set.add(projectId);
191 set.add(projectId.toLowerCase());
192}
193
194/**
195 * All projects the user can see + Auth on/off from briven-engine.
196 */
197export async function listBrivenEngineWorkspace(
198 userId: string,
199): Promise<{ engine: 'briven-engine'; projects: BrivenEngineWorkspaceProject[] }> {
200 const projects = await listProjectsForUser(userId);
201
202 // Build maps first — Auth on = be_tenants row for that project's tenant_id.
203 const maps = projects
204 .map((p) => {
205 try {
206 return mapProjectToAuthCore(p.id);
207 } catch {
208 return null;
209 }
210 })
211 .filter((m): m is NonNullable<typeof m> => m != null);
212
213 const tenantToProject = new Map(maps.map((m) => [m.tenantId, m.projectId]));
214 let enabledProjectIds = new Set<string>();
215
216 if (isAuthCoreInitialized() && maps.length > 0) {
217 try {
218 const pool = getEnginePool();
219 // Active Auth only: tenant row and not soft-disabled.
220 let res;
221 try {
222 res = await pool.query(
223 `SELECT project_id, tenant_id FROM be_tenants
224 WHERE disabled_at IS NULL`,
225 );
226 } catch {
227 res = await pool.query(`SELECT project_id, tenant_id FROM be_tenants`);
228 }
229 for (const row of res.rows as Array<{
230 project_id: string;
231 tenant_id: string;
232 }>) {
233 markEnabled(enabledProjectIds, row.project_id);
234 const viaTenant = tenantToProject.get(row.tenant_id);
235 markEnabled(enabledProjectIds, viaTenant);
236 // Also match tenant_id → project when project_id column is stale/mismatched
237 if (row.tenant_id.startsWith('proj-')) {
238 const fromTenant = tenantToProject.get(row.tenant_id);
239 markEnabled(enabledProjectIds, fromTenant);
240 }
241 }
242 } catch {
243 enabledProjectIds = new Set();
244 }
245 }
246
247 const rows: BrivenEngineWorkspaceProject[] = await Promise.all(
248 projects.map(async (p) => {
249 const name = (p as { name?: string | null }).name?.trim() || p.slug;
250 let tenantId: string | null = null;
251 try {
252 tenantId = mapProjectToAuthCore(p.id).tenantId;
253 } catch {
254 tenantId = null;
255 }
256
257 let authEnabled =
258 enabledProjectIds.has(p.id) ||
259 enabledProjectIds.has(p.id.toLowerCase());
260
261 // Direct probe when batch missed (should be rare).
262 if (!authEnabled && isAuthCoreInitialized()) {
263 authEnabled = await isBrivenEngineAuthEnabled(p.id);
264 }
265
266 if (!authEnabled) {
267 return {
268 id: p.id,
269 slug: p.slug,
270 name,
271 authEnabled: false,
272 // Do not show mapped tenant as if Auth were on
273 tenantId: null,
274 providers: null,
275 };
276 }
277 try {
278 const config = await getBrivenEngineProjectConfig(p.id);
279 return {
280 id: p.id,
281 slug: p.slug,
282 name,
283 authEnabled: true,
284 tenantId: config.tenantId,
285 providers: {
286 emailPassword: config.recipes.emailPassword,
287 magicLink: config.recipes.passwordless,
288 emailOtp: config.recipes.passwordless,
289 passkey: config.recipes.webauthn,
290 },
291 };
292 } catch {
293 return {
294 id: p.id,
295 slug: p.slug,
296 name,
297 authEnabled: true,
298 tenantId,
299 providers: {
300 emailPassword: true,
301 magicLink: true,
302 emailOtp: true,
303 passkey: true,
304 },
305 };
306 }
307 }),
308 );
309
310 return { engine: 'briven-engine', projects: rows };
311}