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:
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* #322 — long-lived API tokens for programmatic access (n8n, custom
|
||||
* integrations, external apps). Each token belongs to an admin user; the
|
||||
* token's effective permissions are the *intersection* of the user's
|
||||
* role permissions and the token's own scope flags. That way revoking
|
||||
* the user revokes the token, and scope flags let an admin issue a
|
||||
* read-only token even if their account is super_admin.
|
||||
*/
|
||||
|
||||
exports.up = async function up(knex) {
|
||||
if (!(await knex.schema.hasTable('api_tokens'))) {
|
||||
await knex.schema.createTable('api_tokens', (table) => {
|
||||
table.increments('id').primary();
|
||||
table.string('name', 100).notNullable();
|
||||
// SHA-256 of the full token string (`pp_live_<random>`). Lookup
|
||||
// hashes the incoming Authorization header and queries by this.
|
||||
table.string('hashed_token', 64).notNullable().unique();
|
||||
// Scope flags — comma-separated subset of: read, write, admin.
|
||||
// 'read' allows GETs; 'write' adds POST/PATCH/DELETE on
|
||||
// event/photo data; 'admin' allows creating/deleting events and
|
||||
// anything else gated by admin.* permissions.
|
||||
table.string('scopes', 64).notNullable().defaultTo('read');
|
||||
table.integer('created_by').notNullable()
|
||||
.references('id').inTable('admin_users').onDelete('CASCADE');
|
||||
table.timestamp('created_at').defaultTo(knex.fn.now());
|
||||
table.timestamp('expires_at').nullable();
|
||||
table.timestamp('last_used_at').nullable();
|
||||
table.timestamp('revoked_at').nullable();
|
||||
// Cosmetic for the admin UI: first 8 chars of the plaintext
|
||||
// token (after the prefix) so admins can identify which token is
|
||||
// which without seeing the secret half.
|
||||
table.string('preview', 16).nullable();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function down(knex) {
|
||||
if (await knex.schema.hasTable('api_tokens')) {
|
||||
await knex.schema.dropTable('api_tokens');
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user