import { TRPCError } from "@trpc/server"; import { and, eq, or } from "drizzle-orm"; import { workspaces, workspaceMembers } from "@tasks/database/schema"; import { db as defaultDb } from "@tasks/database"; /** * Cheap UUID v4-ish detector. We only need to differentiate "this looks like a * UUID" from "this looks like a slug" so the resolver can pick the right column. */ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; export type WorkspaceContext = { id: string; slug: string; name: string; ownerUserId: string; /** Caller's role inside the workspace, or "owner" if they own it directly. */ role: string; }; /** * Resolve a workspace handle (UUID or slug) to a full workspace record AND * authorize the caller against it. Throws NOT_FOUND if the handle doesn't * resolve, FORBIDDEN if the user isn't a member or owner. * * Used by the `workspaceProcedure` middleware and by Server Components at the * `app/(app)/[workspaceSlug]/...` layout boundary. */ export async function resolveWorkspace(args: { handle: string; userId: string; db?: typeof defaultDb; }): Promise { const db = args.db ?? defaultDb; const handle = args.handle.trim(); if (!handle) { throw new TRPCError({ code: "BAD_REQUEST", message: "Workspace handle required" }); } const lookupCondition = UUID_RE.test(handle) ? eq(workspaces.id, handle) : eq(workspaces.slug, handle); const [row] = await db .select({ id: workspaces.id, slug: workspaces.slug, name: workspaces.name, ownerUserId: workspaces.ownerUserId, memberRole: workspaceMembers.role, }) .from(workspaces) .leftJoin( workspaceMembers, and( eq(workspaceMembers.workspaceId, workspaces.id), eq(workspaceMembers.userId, args.userId), ), ) .where(lookupCondition) .limit(1); if (!row) { throw new TRPCError({ code: "NOT_FOUND", message: "Workspace not found" }); } const isOwner = row.ownerUserId === args.userId; if (!isOwner && !row.memberRole) { throw new TRPCError({ code: "FORBIDDEN", message: "Not a member of this workspace" }); } return { id: row.id, slug: row.slug, name: row.name, ownerUserId: row.ownerUserId, role: isOwner ? "owner" : (row.memberRole ?? "member"), }; } /** * Look up a workspace by either UUID or slug WITHOUT authorizing the caller. * Used for the public form-fill flow and for routes that explicitly want to * peek at workspace existence (e.g. URL backcompat redirects). */ export async function findWorkspaceByHandle( handle: string, db = defaultDb, ): Promise<{ id: string; slug: string; name: string } | null> { const cleaned = handle.trim(); if (!cleaned) return null; const cond = UUID_RE.test(cleaned) ? eq(workspaces.id, cleaned) : eq(workspaces.slug, cleaned); const [row] = await db .select({ id: workspaces.id, slug: workspaces.slug, name: workspaces.name }) .from(workspaces) .where(cond) .limit(1); return row ?? null; } export { UUID_RE as WORKSPACE_HANDLE_UUID_RE };