workspace.ts 8.6 KB

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