mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 06:46:42 +02:00
Server-side plumbing for Phase δ. Frontend hookup follows in δ.2.
- services/subscriptions.ts: subscribe/unsubscribe (idempotent
upsert on (user, deck), stamps the latest_version_id at
subscribe-time so the client knows what it pulled). listForUser
returns each sub with `updateAvailable: currentVersion !== latest`
so the client can render an update indicator without a second
round-trip. Refuses paid decks with 403 — that path comes back
in Phase ζ once the credits Marketplace lands.
- versionWithCards: deterministic ord-ordered card payload for a
specific version. Read-public so anonymous browsers can preview
a deck's content.
- diffSince: smart-merge payload between any two versions. Splits
the latest cards into added/changed/unchanged + lists removed
by content_hash. The 'changed' bucket is heuristic (ord-position
pair where one was removed and one was added) — solid enough
until Phase ε's pull-request pipeline gives us real card
lineage.
- routes/subscriptions.ts mounts: GET /v1/me/subscriptions,
POST/DELETE /v1/decks/:slug/subscribe (auth required),
GET /v1/decks/:slug/versions/:semver (public),
GET /v1/decks/:slug/diff?from=<semver> (public).
cards-web layout fix:
- Marketplace surface (/explore, /u/, /d/) was previously gated
behind the AuthGate — anonymous browsers got pushed to /login
via client-side navigate. PUBLIC_PATHS extended so those routes
SSR + render unauthed.
Validated: tsc clean on cards-server, svelte-check 0/0 on cards-web.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { Hono } from 'hono';
|
|
import type { AuthUser } from '../middleware/jwt-auth';
|
|
import type { SubscriptionService } from '../services/subscriptions';
|
|
import { BadRequestError, UnauthorizedError } from '../lib/errors';
|
|
|
|
function requireUser(user: AuthUser | undefined): AuthUser {
|
|
if (!user || !user.userId) throw new UnauthorizedError();
|
|
return user;
|
|
}
|
|
|
|
export function createSubscriptionRoutes(service: SubscriptionService) {
|
|
const router = new Hono<{ Variables: { user?: AuthUser } }>();
|
|
|
|
// User-scoped routes -----------------------------------------------------
|
|
|
|
router.get('/me/subscriptions', async (c) => {
|
|
const user = requireUser(c.get('user'));
|
|
const list = await service.listForUser(user.userId);
|
|
return c.json(list);
|
|
});
|
|
|
|
router.post('/decks/:slug/subscribe', async (c) => {
|
|
const user = requireUser(c.get('user'));
|
|
const result = await service.subscribe(user.userId, c.req.param('slug'));
|
|
return c.json(result, 201);
|
|
});
|
|
|
|
router.delete('/decks/:slug/subscribe', async (c) => {
|
|
const user = requireUser(c.get('user'));
|
|
await service.unsubscribe(user.userId, c.req.param('slug'));
|
|
return c.json({ ok: true });
|
|
});
|
|
|
|
// Public read routes -----------------------------------------------------
|
|
|
|
router.get('/decks/:slug/versions/:semver', async (c) => {
|
|
const semver = c.req.param('semver');
|
|
if (!/^\d+\.\d+\.\d+$/.test(semver)) {
|
|
throw new BadRequestError('semver must look like 1.0.0');
|
|
}
|
|
const payload = await service.versionWithCards(c.req.param('slug'), semver);
|
|
return c.json(payload);
|
|
});
|
|
|
|
router.get('/decks/:slug/diff', async (c) => {
|
|
const url = new URL(c.req.url);
|
|
const from = url.searchParams.get('from');
|
|
if (!from || !/^\d+\.\d+\.\d+$/.test(from)) {
|
|
throw new BadRequestError('?from=<semver> required, e.g. ?from=1.0.0');
|
|
}
|
|
const diff = await service.diffSince(c.req.param('slug'), from);
|
|
return c.json(diff);
|
|
});
|
|
|
|
return router;
|
|
}
|