feat: public v1 API + token management + OpenAPI docs (#322)

Adds a long-lived bearer-token mechanism + scoped REST surface designed
for n8n-style automation: create a gallery, upload photos, fetch the
share URL — all via documented HTTPS endpoints instead of poking at the
admin UI's internal routes.

API
- Migration 081 adds `api_tokens` (hashed_token, scopes, owner FK,
  last_used/expires/revoked timestamps).
- New apiTokenAuth middleware: parses `Authorization: Bearer pp_live_…`,
  resolves to the owner admin user, attaches `req.admin` so existing
  permission decorators (events.create etc.) still work. Token-level
  scope check (read/write/admin) layers on top as defence in depth —
  a leaked read-only token cannot mutate even if its owner is super_admin.
- adminApiTokens route exposes list/create/revoke for admins (cookie-
  authed). Plaintext token is returned exactly once on creation.
- v1 surface mounted at /api/v1: POST/GET /events, GET /events/:id,
  POST /events/:id/photos (multipart, single file), GET
  /events/:id/share-link. Each endpoint annotated with @openapi JSDoc.

Documentation
- swagger-jsdoc + swagger-ui-express produce a live spec at
  /api/openapi.json and a Swagger UI at /api/docs (admin-gated).
- backend/scripts/generate-openapi.js writes docs/openapi.{json,yaml}
  to the repo so the spec is versioned.
- scripts/sync-api-docs.sh runs in pre-push: regenerates the spec and
  copies it into the picpeak-docs Nextra site at app/api/. Writes only,
  never commits or pushes the docs repo (PUSH_SKIP_DOCS=1 to bypass).

Frontend
- New Settings → API Tokens tab: generate, list, revoke. Plaintext
  tokens are shown once with a copy-to-clipboard control.
This commit is contained in:
Paul Nothaft
2026-04-27 22:38:00 +02:00
parent be6cb28c80
commit 808b15bafb
15 changed files with 2064 additions and 5 deletions
+20
View File
@@ -532,6 +532,26 @@ app.use('/api/admin/css-templates', require('./src/routes/adminCssTemplates'));
app.use('/api/admin/events', require('./src/routes/adminEventRename'));
app.use('/api/admin/users', require('./src/routes/adminUsers'));
app.use('/api/admin/event-types', require('./src/routes/adminEventTypes'));
app.use('/api/admin/api-tokens', require('./src/routes/adminApiTokens'));
// Public v1 API for n8n / external integrations (#322). Mounted under
// /api/v1; auth handled per-route via apiTokenAuth (Bearer tokens).
app.use('/api/v1', require('./src/routes/v1/events'));
// Swagger UI for the v1 API. Admin-gated since it lists endpoint shapes
// that should not be enumerable to anonymous users (a common reduce-info-leak hardening).
{
const swaggerUi = require('swagger-ui-express');
const { adminAuth } = require('./src/middleware/auth');
const { getOpenApiSpec } = require('./src/openapi/spec');
app.get('/api/openapi.json', adminAuth, (_req, res) => res.json(getOpenApiSpec()));
app.use(
'/api/docs',
adminAuth,
swaggerUi.serve,
swaggerUi.setup(getOpenApiSpec(), { customSiteTitle: 'PicPeak API · v1' })
);
}
app.use('/api/invite', require('./src/routes/acceptInvite'));
app.use('/api/public/settings', require('./src/routes/publicSettings'));
app.use('/api/public', require('./src/routes/publicCMS'));