feat(16-03): wire userId from context into all route handlers

- Extract userId via c.get('userId') in every route handler
- Pass userId to all service function calls as second argument
- Update settings routes to use composite key [userId, key]
- Update Env type to include userId in Variables
- Auth routes pass userId to API key management functions
This commit is contained in:
2026-04-05 10:49:51 +02:00
parent 884bec0b35
commit e78002208a
7 changed files with 88 additions and 49 deletions

View File

@@ -10,7 +10,7 @@ import {
listApiKeys,
} from "../services/auth.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const createKeySchema = z.object({ name: z.string().min(1) });
@@ -33,7 +33,8 @@ app.get("/me", async (c) => {
app.get("/keys", requireAuth, async (c) => {
const db = c.get("db");
const keys = await listApiKeys(db);
const userId = c.get("userId")!;
const keys = await listApiKeys(db, userId);
return c.json(keys);
});
@@ -43,8 +44,9 @@ app.post(
zValidator("json", createKeySchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const { name } = c.req.valid("json");
const result = await createApiKey(db, name);
const result = await createApiKey(db, userId, name);
return c.json(
{
@@ -60,9 +62,10 @@ app.post(
app.delete("/keys/:id", requireAuth, async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid key ID" }, 400);
await deleteApiKey(db, id);
await deleteApiKey(db, userId, id);
return c.json({ ok: true });
});

View File

@@ -12,20 +12,22 @@ import {
updateCategory,
} from "../services/category.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
app.get("/", async (c) => {
const db = c.get("db");
const cats = await getAllCategories(db);
const userId = c.get("userId")!;
const cats = await getAllCategories(db, userId);
return c.json(cats);
});
app.post("/", zValidator("json", createCategorySchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const data = c.req.valid("json");
const cat = await createCategory(db, data);
const cat = await createCategory(db, userId, data);
return c.json(cat, 201);
});
@@ -34,10 +36,11 @@ app.put(
zValidator("json", updateCategorySchema.omit({ id: true })),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid category ID" }, 400);
const data = c.req.valid("json");
const cat = await updateCategory(db, id, data);
const cat = await updateCategory(db, userId, id, data);
if (!cat) return c.json({ error: "Category not found" }, 404);
return c.json(cat);
},
@@ -45,9 +48,10 @@ app.put(
app.delete("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid category ID" }, 400);
const result = await deleteCategory(db, id);
const result = await deleteCategory(db, userId, id);
if (!result.success) {
if (result.error === "Cannot delete the Uncategorized category") {

View File

@@ -14,13 +14,14 @@ import {
updateItem,
} from "../services/item.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
app.get("/export", async (c) => {
const db = c.get("db");
const csv = await exportItemsCsv(db);
const userId = c.get("userId")!;
const csv = await exportItemsCsv(db, userId);
c.header("Content-Type", "text/csv");
c.header("Content-Disposition", 'attachment; filename="gearbox-export.csv"');
return c.body(csv);
@@ -28,6 +29,7 @@ app.get("/export", async (c) => {
app.post("/import", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const body = await c.req.parseBody();
// Accept either "file" (direct) or "image" (via apiUpload helper)
const file = body.file ?? body.image;
@@ -35,29 +37,32 @@ app.post("/import", async (c) => {
return c.json({ error: "No CSV file provided" }, 400);
}
const content = await file.text();
const result = await importItemsCsv(db, content);
const result = await importItemsCsv(db, userId, content);
return c.json(result);
});
app.get("/", async (c) => {
const db = c.get("db");
const items = await getAllItems(db);
const userId = c.get("userId")!;
const items = await getAllItems(db, userId);
return c.json(items);
});
app.get("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid item ID" }, 400);
const item = await getItemById(db, id);
const item = await getItemById(db, userId, id);
if (!item) return c.json({ error: "Item not found" }, 404);
return c.json(item);
});
app.post("/", zValidator("json", createItemSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const data = c.req.valid("json");
const item = await createItem(db, data);
const item = await createItem(db, userId, data);
return c.json(item, 201);
});
@@ -66,10 +71,11 @@ app.put(
zValidator("json", updateItemSchema.omit({ id: true })),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid item ID" }, 400);
const data = c.req.valid("json");
const item = await updateItem(db, id, data);
const item = await updateItem(db, userId, id, data);
if (!item) return c.json({ error: "Item not found" }, 404);
return c.json(item);
},
@@ -77,18 +83,20 @@ app.put(
app.post("/:id/duplicate", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid item ID" }, 400);
const newItem = await duplicateItem(db, id);
const newItem = await duplicateItem(db, userId, id);
if (!newItem) return c.json({ error: "Item not found" }, 404);
return c.json(newItem, 201);
});
app.delete("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid item ID" }, 400);
const deleted = await deleteItem(db, id);
const deleted = await deleteItem(db, userId, id);
if (!deleted) return c.json({ error: "Item not found" }, 404);
// Clean up image file if exists

View File

@@ -1,24 +1,26 @@
import { eq } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { Hono } from "hono";
import { settings } from "../../db/schema.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
app.get("/:key", async (c) => {
const database = c.get("db");
const userId = c.get("userId")!;
const key = c.req.param("key");
const [row] = await database
.select()
.from(settings)
.where(eq(settings.key, key));
.where(and(eq(settings.userId, userId), eq(settings.key, key)));
if (!row) return c.json({ error: "Setting not found" }, 404);
return c.json(row);
});
app.put("/:key", async (c) => {
const database = c.get("db");
const userId = c.get("userId")!;
const key = c.req.param("key");
const body = await c.req.json<{ value: string }>();
@@ -28,13 +30,16 @@ app.put("/:key", async (c) => {
await database
.insert(settings)
.values({ key, value: body.value })
.onConflictDoUpdate({ target: settings.key, set: { value: body.value } });
.values({ userId, key, value: body.value })
.onConflictDoUpdate({
target: [settings.userId, settings.key],
set: { value: body.value },
});
const [row] = await database
.select()
.from(settings)
.where(eq(settings.key, key));
.where(and(eq(settings.userId, userId), eq(settings.key, key)));
return c.json(row);
});

View File

@@ -18,7 +18,7 @@ import {
updateSetup,
} from "../services/setup.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
@@ -26,41 +26,46 @@ const app = new Hono<Env>();
app.get("/", async (c) => {
const db = c.get("db");
const setups = await getAllSetups(db);
const userId = c.get("userId")!;
const setups = await getAllSetups(db, userId);
return c.json(setups);
});
app.post("/", zValidator("json", createSetupSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const data = c.req.valid("json");
const setup = await createSetup(db, data);
const setup = await createSetup(db, userId, data);
return c.json(setup, 201);
});
app.get("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
const setup = await getSetupWithItems(db, id);
const setup = await getSetupWithItems(db, userId, id);
if (!setup) return c.json({ error: "Setup not found" }, 404);
return c.json(setup);
});
app.put("/:id", zValidator("json", updateSetupSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
const data = c.req.valid("json");
const setup = await updateSetup(db, id, data);
const setup = await updateSetup(db, userId, id, data);
if (!setup) return c.json({ error: "Setup not found" }, 404);
return c.json(setup);
});
app.delete("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
const deleted = await deleteSetup(db, id);
const deleted = await deleteSetup(db, userId, id);
if (!deleted) return c.json({ error: "Setup not found" }, 404);
return c.json({ success: true });
});
@@ -69,14 +74,15 @@ app.delete("/:id", async (c) => {
app.put("/:id/items", zValidator("json", syncSetupItemsSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid setup ID" }, 400);
const { itemIds } = c.req.valid("json");
const setup = await getSetupWithItems(db, id);
const setup = await getSetupWithItems(db, userId, id);
if (!setup) return c.json({ error: "Setup not found" }, 404);
await syncSetupItems(db, id, itemIds);
await syncSetupItems(db, userId, id, itemIds);
return c.json({ success: true });
});
@@ -85,21 +91,23 @@ app.patch(
zValidator("json", updateClassificationSchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const setupId = parseId(c.req.param("id"));
const itemId = parseId(c.req.param("itemId"));
if (!setupId || !itemId) return c.json({ error: "Invalid ID" }, 400);
const { classification } = c.req.valid("json");
await updateItemClassification(db, setupId, itemId, classification);
await updateItemClassification(db, userId, setupId, itemId, classification);
return c.json({ success: true });
},
);
app.delete("/:id/items/:itemId", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const setupId = parseId(c.req.param("id"));
const itemId = parseId(c.req.param("itemId"));
if (!setupId || !itemId) return c.json({ error: "Invalid ID" }, 400);
await removeSetupItem(db, setupId, itemId);
await removeSetupItem(db, userId, setupId, itemId);
return c.json({ success: true });
});

View File

@@ -24,7 +24,7 @@ import {
updateThread,
} from "../services/thread.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
@@ -32,42 +32,47 @@ const app = new Hono<Env>();
app.get("/", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const includeResolved = c.req.query("includeResolved") === "true";
const threads = await getAllThreads(db, includeResolved);
const threads = await getAllThreads(db, userId, includeResolved);
return c.json(threads);
});
app.post("/", zValidator("json", createThreadSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const data = c.req.valid("json");
const thread = await createThread(db, data);
const thread = await createThread(db, userId, data);
return c.json(thread, 201);
});
app.get("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid thread ID" }, 400);
const thread = await getThreadWithCandidates(db, id);
const thread = await getThreadWithCandidates(db, userId, id);
if (!thread) return c.json({ error: "Thread not found" }, 404);
return c.json(thread);
});
app.put("/:id", zValidator("json", updateThreadSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid thread ID" }, 400);
const data = c.req.valid("json");
const thread = await updateThread(db, id, data);
const thread = await updateThread(db, userId, id, data);
if (!thread) return c.json({ error: "Thread not found" }, 404);
return c.json(thread);
});
app.delete("/:id", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const id = parseId(c.req.param("id"));
if (!id) return c.json({ error: "Invalid thread ID" }, 400);
const deleted = await deleteThread(db, id);
const deleted = await deleteThread(db, userId, id);
if (!deleted) return c.json({ error: "Thread not found" }, 404);
// Clean up candidate image files
@@ -89,15 +94,16 @@ app.post(
zValidator("json", createCandidateSchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const threadId = parseId(c.req.param("id"));
if (!threadId) return c.json({ error: "Invalid thread ID" }, 400);
// Verify thread exists
const thread = await getThreadWithCandidates(db, threadId);
const thread = await getThreadWithCandidates(db, userId, threadId);
if (!thread) return c.json({ error: "Thread not found" }, 404);
const data = c.req.valid("json");
const candidate = await createCandidate(db, threadId, data);
const candidate = await createCandidate(db, userId, threadId, data);
return c.json(candidate, 201);
},
);
@@ -107,10 +113,11 @@ app.put(
zValidator("json", updateCandidateSchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const candidateId = parseId(c.req.param("candidateId"));
if (!candidateId) return c.json({ error: "Invalid candidate ID" }, 400);
const data = c.req.valid("json");
const candidate = await updateCandidate(db, candidateId, data);
const candidate = await updateCandidate(db, userId, candidateId, data);
if (!candidate) return c.json({ error: "Candidate not found" }, 404);
return c.json(candidate);
},
@@ -118,9 +125,10 @@ app.put(
app.delete("/:threadId/candidates/:candidateId", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const candidateId = parseId(c.req.param("candidateId"));
if (!candidateId) return c.json({ error: "Invalid candidate ID" }, 400);
const deleted = await deleteCandidate(db, candidateId);
const deleted = await deleteCandidate(db, userId, candidateId);
if (!deleted) return c.json({ error: "Candidate not found" }, 404);
// Clean up image file if exists
@@ -142,10 +150,11 @@ app.patch(
zValidator("json", reorderCandidatesSchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const threadId = parseId(c.req.param("id"));
if (!threadId) return c.json({ error: "Invalid thread ID" }, 400);
const { orderedIds } = c.req.valid("json");
const result = await reorderCandidates(db, threadId, orderedIds);
const result = await reorderCandidates(db, userId, threadId, orderedIds);
if (!result.success) return c.json({ error: result.error }, 400);
return c.json({ success: true });
},
@@ -155,11 +164,12 @@ app.patch(
app.post("/:id/resolve", zValidator("json", resolveThreadSchema), async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const threadId = parseId(c.req.param("id"));
if (!threadId) return c.json({ error: "Invalid thread ID" }, 400);
const { candidateId } = c.req.valid("json");
const result = await resolveThread(db, threadId, candidateId);
const result = await resolveThread(db, userId, threadId, candidateId);
if (!result.success) {
return c.json({ error: result.error }, 400);
}

View File

@@ -4,14 +4,15 @@ import {
getGlobalTotals,
} from "../services/totals.service.ts";
type Env = { Variables: { db?: any } };
type Env = { Variables: { db?: any; userId?: number } };
const app = new Hono<Env>();
app.get("/", async (c) => {
const db = c.get("db");
const categoryTotals = await getCategoryTotals(db);
const globalTotals = await getGlobalTotals(db);
const userId = c.get("userId")!;
const categoryTotals = await getCategoryTotals(db, userId);
const globalTotals = await getGlobalTotals(db, userId);
return c.json({ categories: categoryTotals, global: globalTotals });
});