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:
@@ -26,6 +26,9 @@ import { setupRoutes } from "./routes/setups.ts";
|
||||
import { tagRoutes } from "./routes/tags.ts";
|
||||
import { threadRoutes } from "./routes/threads.ts";
|
||||
import { totalRoutes } from "./routes/totals.ts";
|
||||
import { getSetupWithItemsById } from "./services/setup.service.ts";
|
||||
import { validateShareToken } from "./services/share.service.ts";
|
||||
import { withImageUrls } from "./services/storage.service.ts";
|
||||
|
||||
// Seed default data on startup
|
||||
await seedDefaults();
|
||||
@@ -163,6 +166,27 @@ app.use("/api/users/:id/profile", async (c, next) => {
|
||||
return next();
|
||||
});
|
||||
|
||||
// Shared setup access via token (no auth required)
|
||||
app.get("/api/shared/:token", async (c) => {
|
||||
const db = c.get("db");
|
||||
const token = c.req.param("token");
|
||||
const result = await validateShareToken(db, token);
|
||||
if (!result) return c.json({ error: "Not found" }, 404);
|
||||
const setup = await getSetupWithItemsById(db, result.setupId);
|
||||
if (!setup) return c.json({ error: "Not found" }, 404);
|
||||
const enrichedItems = await withImageUrls(setup.items);
|
||||
return c.json({ ...setup, items: enrichedItems });
|
||||
});
|
||||
|
||||
// Short share URL redirect (no auth required — before SPA catch-all)
|
||||
app.get("/s/:token", async (c) => {
|
||||
const db = c.get("db");
|
||||
const token = c.req.param("token");
|
||||
const result = await validateShareToken(db, token);
|
||||
if (!result) return c.redirect("/", 302);
|
||||
return c.redirect(`/setups/${result.setupId}?share=${token}`, 302);
|
||||
});
|
||||
|
||||
// Auth middleware for all data routes (userId must be available for per-user scoping)
|
||||
app.use("/api/*", async (c, next) => {
|
||||
// Skip auth routes — they handle their own auth
|
||||
@@ -178,6 +202,9 @@ app.use("/api/*", async (c, next) => {
|
||||
// Skip public tags endpoint (GET /api/tags)
|
||||
if (c.req.path.startsWith("/api/tags") && c.req.method === "GET")
|
||||
return next();
|
||||
// Skip shared setup access (GET /api/shared/:token)
|
||||
if (c.req.path.startsWith("/api/shared/") && c.req.method === "GET")
|
||||
return next();
|
||||
// Skip public discovery endpoints (GET /api/discovery/*)
|
||||
if (c.req.path.startsWith("/api/discovery") && c.req.method === "GET")
|
||||
return next();
|
||||
|
||||
Reference in New Issue
Block a user