feat: add share link service, API routes, and short URL redirect

Create share.service.ts with token generation (128-bit base64url),
CRUD operations, validation, and visibility transition side effects.
Add share endpoints under /api/setups/:id/shares, shared access at
/api/shared/:token, and /s/:token short URL redirect.

Plan: 32-02 (Setup Sharing System - Share Link Backend)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 17:59:39 +02:00
parent 7a696f39a5
commit da159d10b8
6 changed files with 507 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import { zValidator } from "@hono/zod-validator";
import { Hono } from "hono";
import {
createSetupSchema,
createShareLinkSchema,
syncSetupItemsSchema,
updateClassificationSchema,
updateSetupSchema,
@@ -18,6 +19,11 @@ import {
updateItemClassification,
updateSetup,
} from "../services/setup.service.ts";
import {
createShareLink,
getShareLinks,
revokeShareLink,
} from "../services/share.service.ts";
import { withImageUrls } from "../services/storage.service.ts";
type Env = { Variables: { db?: any; userId?: number } };
@@ -125,4 +131,43 @@ app.delete("/:id/items/:itemId", async (c) => {
return c.json({ success: true });
});
// Share Links
app.post(
"/:id/shares",
zValidator("json", createShareLinkSchema),
async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const setupId = parseId(c.req.param("id"));
if (!setupId) return c.json({ error: "Invalid setup ID" }, 400);
const data = c.req.valid("json");
const share = await createShareLink(db, userId, setupId, {
expiresInDays: data.expiresInDays,
});
if (!share) return c.json({ error: "Setup not found" }, 404);
return c.json(share, 201);
},
);
app.get("/:id/shares", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const setupId = parseId(c.req.param("id"));
if (!setupId) return c.json({ error: "Invalid setup ID" }, 400);
const links = await getShareLinks(db, userId, setupId);
if (!links) return c.json({ error: "Setup not found" }, 404);
return c.json(links);
});
app.delete("/:id/shares/:shareId", async (c) => {
const db = c.get("db");
const userId = c.get("userId")!;
const shareId = parseId(c.req.param("shareId"));
if (!shareId) return c.json({ error: "Invalid share ID" }, 400);
const share = await revokeShareLink(db, userId, shareId);
if (!share) return c.json({ error: "Share not found" }, 404);
return c.json(share);
});
export { app as setupRoutes };