import { Elysia, t } from "elysia"; import { db } from "../db"; import { projects, tasks } from "../db/schema"; import { eq, asc, desc } from "drizzle-orm"; import { auth } from "../lib/auth"; const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token"; async function requireSessionOrBearer( request: Request, headers: Record ) { const authHeader = headers["authorization"]; if (authHeader === `Bearer ${BEARER_TOKEN}`) return; try { const session = await auth.api.getSession({ headers: request.headers }); if (session) return; } catch {} throw new Error("Unauthorized"); } export const projectRoutes = new Elysia({ prefix: "/api/projects" }) .onError(({ error, set }) => { const msg = error?.message || String(error); if (msg === "Unauthorized") { set.status = 401; return { error: "Unauthorized" }; } if (msg === "Project not found") { set.status = 404; return { error: "Project not found" }; } console.error("Project route error:", msg); set.status = 500; return { error: "Internal server error" }; }) // GET all projects .get("/", async ({ request, headers }) => { await requireSessionOrBearer(request, headers); const allProjects = await db .select() .from(projects) .orderBy(asc(projects.name)); return allProjects; }) // POST create project .post( "/", async ({ body, request, headers }) => { await requireSessionOrBearer(request, headers); const newProject = await db .insert(projects) .values({ name: body.name, description: body.description, context: body.context, repos: body.repos || [], links: body.links || [], }) .returning(); return newProject[0]; }, { body: t.Object({ name: t.String(), description: t.Optional(t.String()), context: t.Optional(t.String()), repos: t.Optional(t.Array(t.String())), links: t.Optional( t.Array(t.Object({ label: t.String(), url: t.String() })) ), }), } ) // GET single project with its tasks .get( "/:id", async ({ params, request, headers }) => { await requireSessionOrBearer(request, headers); const project = await db .select() .from(projects) .where(eq(projects.id, params.id)); if (!project.length) throw new Error("Project not found"); const projectTasks = await db .select() .from(tasks) .where(eq(tasks.projectId, params.id)) .orderBy(asc(tasks.position), desc(tasks.createdAt)); return { ...project[0], tasks: projectTasks }; }, { params: t.Object({ id: t.String() }) } ) // PATCH update project .patch( "/:id", async ({ params, body, request, headers }) => { await requireSessionOrBearer(request, headers); const updates: Record = { updatedAt: new Date() }; if (body.name !== undefined) updates.name = body.name; if (body.description !== undefined) updates.description = body.description; if (body.context !== undefined) updates.context = body.context; if (body.repos !== undefined) updates.repos = body.repos; if (body.links !== undefined) updates.links = body.links; const updated = await db .update(projects) .set(updates) .where(eq(projects.id, params.id)) .returning(); if (!updated.length) throw new Error("Project not found"); return updated[0]; }, { params: t.Object({ id: t.String() }), body: t.Object({ name: t.Optional(t.String()), description: t.Optional(t.String()), context: t.Optional(t.String()), repos: t.Optional(t.Array(t.String())), links: t.Optional( t.Array(t.Object({ label: t.String(), url: t.String() })) ), }), } ) // DELETE project (unlinks tasks, doesn't delete them) .delete( "/:id", async ({ params, request, headers }) => { await requireSessionOrBearer(request, headers); // Unlink tasks first await db .update(tasks) .set({ projectId: null, updatedAt: new Date() }) .where(eq(tasks.projectId, params.id)); // Delete project const deleted = await db .delete(projects) .where(eq(projects.id, params.id)) .returning(); if (!deleted.length) throw new Error("Project not found"); return { success: true }; }, { params: t.Object({ id: t.String() }) } );