import { eq } from "drizzle-orm"; import type { Context, Next } from "hono"; import { HTTPException } from "hono/http-exception"; import db, { schema } from "../database"; import { validateWorkspaceAccess } from "./validate-workspace-access"; type WorkspaceIdSource = | { type: "query"; key: string } | { type: "body"; key: string } | { type: "param"; key: string } | { type: "lookup"; resource: | "project" | "task" | "label" | "timeEntry" | "activity" | "comment" | "column" | "workflowRule"; idKey: string; }; type WorkspaceAccessMiddlewareConfig = { sources: WorkspaceIdSource[]; }; async function readJsonObjectBody( c: Context, ): Promise> { const raw = (await c.req.json().catch(() => ({}))) || {}; if (typeof raw !== "object" || raw === null || Array.isArray(raw)) { return {}; } return raw as Record; } export function workspaceAccessMiddleware( config: WorkspaceAccessMiddlewareConfig, ) { return async (c: Context, next: Next) => { const userId = c.get("userId"); if (!userId) { throw new HTTPException(401, { message: "Unauthorized" }); } let workspaceId: string | null = null; for (const source of config.sources) { if (source.type === "query") { workspaceId = c.req.query(source.key) || null; } else if (source.type === "body") { const body = await readJsonObjectBody(c); workspaceId = typeof body[source.key] === "string" ? body[source.key] : null; } else if (source.type === "param") { workspaceId = c.req.param(source.key) || null; } else if (source.type === "lookup") { const body = await readJsonObjectBody(c); const idFromBody = typeof body[source.idKey] === "string" ? body[source.idKey] : null; const id = c.req.param(source.idKey) || c.req.query(source.idKey) || idFromBody; if (id) { workspaceId = await lookupWorkspaceId(source.resource, id); } } if (workspaceId) { break; } } if (!workspaceId) { throw new HTTPException(400, { message: "Workspace ID could not be determined", }); } const apiKey = c.get("apiKey"); const apiKeyId = apiKey?.id; await validateWorkspaceAccess(userId, workspaceId, apiKeyId); c.set("workspaceId", workspaceId); return next(); }; } async function lookupWorkspaceId( resource: | "project" | "task" | "label" | "timeEntry" | "activity" | "comment" | "column" | "workflowRule", id: string, ): Promise { try { switch (resource) { case "project": { const [project] = await db .select({ workspaceId: schema.projectTable.workspaceId }) .from(schema.projectTable) .where(eq(schema.projectTable.id, id)) .limit(1); return project?.workspaceId || null; } case "task": { const [task] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.taskTable) .innerJoin( schema.projectTable, eq(schema.taskTable.projectId, schema.projectTable.id), ) .where(eq(schema.taskTable.id, id)) .limit(1); return task?.workspaceId || null; } case "label": { const [label] = await db .select({ workspaceId: schema.labelTable.workspaceId }) .from(schema.labelTable) .where(eq(schema.labelTable.id, id)) .limit(1); return label?.workspaceId || null; } case "timeEntry": { const [timeEntry] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.timeEntryTable) .innerJoin( schema.taskTable, eq(schema.timeEntryTable.taskId, schema.taskTable.id), ) .innerJoin( schema.projectTable, eq(schema.taskTable.projectId, schema.projectTable.id), ) .where(eq(schema.timeEntryTable.id, id)) .limit(1); return timeEntry?.workspaceId || null; } case "activity": { const [activity] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.activityTable) .innerJoin( schema.taskTable, eq(schema.activityTable.taskId, schema.taskTable.id), ) .innerJoin( schema.projectTable, eq(schema.taskTable.projectId, schema.projectTable.id), ) .where(eq(schema.activityTable.id, id)) .limit(1); return activity?.workspaceId || null; } case "comment": { const [comment] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.commentTable) .innerJoin( schema.taskTable, eq(schema.commentTable.taskId, schema.taskTable.id), ) .innerJoin( schema.projectTable, eq(schema.taskTable.projectId, schema.projectTable.id), ) .where(eq(schema.commentTable.id, id)) .limit(1); return comment?.workspaceId || null; } case "column": { const [column] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.columnTable) .innerJoin( schema.projectTable, eq(schema.columnTable.projectId, schema.projectTable.id), ) .where(eq(schema.columnTable.id, id)) .limit(1); return column?.workspaceId || null; } case "workflowRule": { const [workflowRule] = await db .select({ workspaceId: schema.projectTable.workspaceId, }) .from(schema.workflowRuleTable) .innerJoin( schema.projectTable, eq(schema.workflowRuleTable.projectId, schema.projectTable.id), ) .where(eq(schema.workflowRuleTable.id, id)) .limit(1); return workflowRule?.workspaceId || null; } default: return null; } } catch (error) { console.error(`Error looking up workspaceId for ${resource}:`, error); return null; } } export const workspaceAccess = { fromQuery: (key = "workspaceId") => workspaceAccessMiddleware({ sources: [{ type: "query", key }] }), fromBody: (key = "workspaceId") => workspaceAccessMiddleware({ sources: [{ type: "body", key }] }), fromParam: (key = "workspaceId") => workspaceAccessMiddleware({ sources: [{ type: "param", key }] }), fromProject: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "query", key: "workspaceId" }, { type: "lookup", resource: "project", idKey }, ], }), fromTask: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "task", idKey }, { type: "query", key: "workspaceId" }, ], }), fromTaskId: (idKey = "taskId") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "task", idKey }, { type: "query", key: "workspaceId" }, ], }), fromLabel: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "label", idKey }, { type: "query", key: "workspaceId" }, ], }), fromTimeEntry: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "timeEntry", idKey }, { type: "query", key: "workspaceId" }, ], }), fromActivity: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "activity", idKey }, { type: "query", key: "workspaceId" }, ], }), fromComment: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "comment", idKey }, { type: "query", key: "workspaceId" }, ], }), fromColumn: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "column", idKey }, { type: "query", key: "workspaceId" }, ], }), fromWorkflowRule: (idKey = "id") => workspaceAccessMiddleware({ sources: [ { type: "lookup", resource: "workflowRule", idKey }, { type: "query", key: "workspaceId" }, ], }), };