import "server-only"; import type { Db } from "@/infra/db/types"; import type { Role } from "@/domain/spreadsheet-adapter/types"; export type Membership = { workspaceId: string; workspaceName: string; role: Role; moduleEnabled: boolean; }; export type WorkspaceResolution = | { kind: "none" } | { kind: "resolved"; workspaceId: string } | { kind: "choice"; memberships: Membership[] }; const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; export function isWorkspaceId(value: string | null | undefined): value is string { return typeof value === "string" && UUID_RE.test(value); } /** * Associações reais do usuário, lidas com o papel `app_user` sob RLS. * Esta é a única fonte de autorização de workspace. */ export async function listMemberships(db: Db, userId: string): Promise { const rows = await db.withUser(userId, (query) => query.query<{ workspace_id: string; workspace_name: string; role: Role; module_enabled: boolean; }>( `select w.id as workspace_id, w.name as workspace_name, m.role, coalesce(f.enabled, false) as module_enabled from public.workspace_members m join public.workspaces w on w.id = m.workspace_id left join public.workspace_module_flags f on f.workspace_id = w.id and f.module_key = 'spreadsheet_adapter' where m.user_id = $1 order by w.name asc`, [userId], ), ); return rows.map((row) => ({ workspaceId: row.workspace_id, workspaceName: row.workspace_name, role: row.role, moduleEnabled: row.module_enabled, })); } /** * Resolve o workspace no servidor. O cookie da plataforma é apenas uma dica de * preferência: se não corresponder a uma associação real ele é ignorado, nunca * aceito como prova. Com uma única associação a escolha é automática; com mais * de uma e sem dica válida, quem escolhe é a pessoa. */ export function resolveWorkspace(memberships: Membership[], hint: string | null | undefined): WorkspaceResolution { if (memberships.length === 0) return { kind: "none" }; if (isWorkspaceId(hint)) { const match = memberships.find((item) => item.workspaceId.toLowerCase() === hint.toLowerCase()); if (match) return { kind: "resolved", workspaceId: match.workspaceId }; } if (memberships.length === 1) return { kind: "resolved", workspaceId: memberships[0].workspaceId }; return { kind: "choice", memberships }; } /** Confirma que o usuário é membro antes de gravar a preferência no cookie. */ export async function assertMembership(db: Db, userId: string, workspaceId: string): Promise { if (!isWorkspaceId(workspaceId)) throw new Error("workspace inválido"); const memberships = await listMemberships(db, userId); const match = memberships.find((item) => item.workspaceId.toLowerCase() === workspaceId.toLowerCase()); if (!match) throw new Error("associação não encontrada"); return match; }