808b15bafb
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.
271 lines
6.9 KiB
YAML
271 lines
6.9 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: PicPeak API
|
|
version: v1
|
|
description: >-
|
|
Public REST API for PicPeak — create gallery events, upload photos, fetch share links.
|
|
Authenticate with a Bearer token issued via the admin **Settings → API Tokens** tab.
|
|
servers:
|
|
- url: /api/v1
|
|
description: Same-origin (production)
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: pp_live_*
|
|
description: >-
|
|
Long-lived API token. Issue via Settings → API Tokens. Token format: `pp_live_<random>`.
|
|
Scopes: `read`, `write`, `admin`.
|
|
schemas:
|
|
EventSummary:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
slug:
|
|
type: string
|
|
event_name:
|
|
type: string
|
|
event_type:
|
|
type: string
|
|
event_date:
|
|
type: string
|
|
format: date
|
|
nullable: true
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
is_active:
|
|
type: boolean
|
|
is_archived:
|
|
type: boolean
|
|
is_draft:
|
|
type: boolean
|
|
created_at:
|
|
type: string
|
|
format: date-time
|
|
security:
|
|
- bearerAuth: []
|
|
paths:
|
|
/events:
|
|
post:
|
|
tags:
|
|
- Events
|
|
summary: Create a gallery event
|
|
description: Returns the new event's id, slug, and absolute share URL.
|
|
security:
|
|
- bearerAuth: []
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- event_name
|
|
- event_type
|
|
properties:
|
|
event_name:
|
|
type: string
|
|
event_type:
|
|
type: string
|
|
enum:
|
|
- wedding
|
|
- birthday
|
|
- corporate
|
|
- other
|
|
- family
|
|
event_date:
|
|
type: string
|
|
format: date
|
|
nullable: true
|
|
customer_name:
|
|
type: string
|
|
nullable: true
|
|
customer_email:
|
|
type: string
|
|
format: email
|
|
nullable: true
|
|
customer_phone:
|
|
type: string
|
|
nullable: true
|
|
description: Only persisted when the global phone-field setting is enabled.
|
|
admin_email:
|
|
type: string
|
|
format: email
|
|
nullable: true
|
|
require_password:
|
|
type: boolean
|
|
default: true
|
|
password:
|
|
type: string
|
|
nullable: true
|
|
description: Required when require_password is true.
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
responses:
|
|
'201':
|
|
description: Event created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
slug:
|
|
type: string
|
|
share_url:
|
|
type: string
|
|
format: uri
|
|
share_token:
|
|
type: string
|
|
'400':
|
|
description: Validation error
|
|
'401':
|
|
description: Missing/invalid token
|
|
'403':
|
|
description: Token lacks admin scope
|
|
get:
|
|
tags:
|
|
- Events
|
|
summary: List gallery events (paginated)
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: query
|
|
name: page
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
default: 1
|
|
- in: query
|
|
name: limit
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 25
|
|
responses:
|
|
'200':
|
|
description: Paginated list
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
events:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/EventSummary'
|
|
pagination:
|
|
type: object
|
|
properties:
|
|
page:
|
|
type: integer
|
|
limit:
|
|
type: integer
|
|
total:
|
|
type: integer
|
|
/events/{id}:
|
|
get:
|
|
tags:
|
|
- Events
|
|
summary: Get a single event
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
'200':
|
|
description: Event details
|
|
'404':
|
|
description: Not found
|
|
/events/{id}/photos:
|
|
post:
|
|
tags:
|
|
- Photos
|
|
summary: Upload a single photo to an event
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required:
|
|
- photo
|
|
properties:
|
|
photo:
|
|
type: string
|
|
format: binary
|
|
responses:
|
|
'201':
|
|
description: Photo uploaded
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: integer
|
|
filename:
|
|
type: string
|
|
path:
|
|
type: string
|
|
thumbnail_path:
|
|
type: string
|
|
nullable: true
|
|
size_bytes:
|
|
type: integer
|
|
'400':
|
|
description: No file or invalid type
|
|
'404':
|
|
description: Event not found
|
|
/events/{id}/share-link:
|
|
get:
|
|
tags:
|
|
- Events
|
|
summary: Get the absolute share URL for an event
|
|
security:
|
|
- bearerAuth: []
|
|
parameters:
|
|
- in: path
|
|
name: id
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
'200':
|
|
description: Share URL
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
slug:
|
|
type: string
|
|
share_token:
|
|
type: string
|
|
share_url:
|
|
type: string
|
|
format: uri
|
|
'404':
|
|
description: Not found
|
|
tags: []
|