Management API foundation #544

Open
opened 2026-09-29 19:08:34 +01:00 by Smithy-bot · 2 comments
Member

Epic: #543
Story Points: 5


SO THAT management routes share one Express surface with auth middleware and the existing /api/reload-db hook is no longer unauthenticated.

Expand the in-process Express app (src/webhooks.ts) into a Management API foundation: structured routing, shared auth middleware hook-point, and protect /api/reload-db.

Scope

Foundational Express structure for the Web Management UI epic (#543). Discord OAuth and sessions are implemented in #545; this story defines the routing layout and protected /api surface those features plug into.

In scope: Refactor/bootstrap Management API, protected route registration, auth middleware hook, securing existing POST /api/reload-db.

Out of scope: Discord OAuth flows (#545), React SPA shell (#546), feature endpoints (settings/cards/ops #547–#550), static asset serving (#546).

Acceptance criteria

  • GIVEN the bot process starts with EXPRESS_PORT set
    WHEN the Management API boots
    THEN it uses the existing in-process Express entry (src/webhooks.ts or successor) and listens on the same port as today.

  • GIVEN the Management API foundation
    WHEN a new management handler is added (current or future stories)
    THEN it registers on a shared protected router (e.g. under /api/) so all management routes use the same middleware stack.

  • GIVEN a caller without a valid admin session
    WHEN they POST /api/reload-db
    THEN the response is 401 or 403 and CardMetadataFunction does not run.

  • GIVEN a caller with a valid admin session (session shape defined in #545)
    WHEN they POST /api/reload-db with a session belonging to a Discord user ID in BOT_ADMINS
    THEN behaviour matches today (reload card metadata, 200 on success).

  • GIVEN public auth routes from #545 (e.g. OAuth callback/login)
    WHEN they are mounted
    THEN they are not behind the admin-session middleware (mounted on a separate public router).

  • GIVEN protected management routes
    WHEN authorization is evaluated
    THEN v1 checks only Admin membership (BOT_ADMINS), but the middleware/resource-check shape allows finer roles later (#551) without rewriting route registration.

  • GIVEN automated tests exist for the Management API
    WHEN CI runs
    THEN tests cover at least: unauthenticated POST /api/reload-db rejected; authenticated admin can invoke reload (may stub session until #545 lands).

Subtasks

  • Refactor Express bootstrap into a Management API module (router + app wiring from src/webhooks.ts)
  • Add shared requireManagementAuth middleware (admin session / BOT_ADMINS check; integrates with #545 session)
  • Mount POST /api/reload-db on the protected router (move from public registration)
  • Expose a registration pattern for future /api/* management routes (#547–#550)
  • Add/update unit or integration tests for auth gating on /api/reload-db
  • Document breaking change: unauthenticated reload no longer allowed (coordinated release with #545)

Notes

  • Decision: #542 — same bot process; secure existing webhook/management routes.
  • Existing handler: src/hooks/ReloadDB.ts.
  • Release: Do not enable production protection on /api/reload-db until #545 provides login/session (merge or ship both in the same release).
  • Depends on: none for structure; pairs with #545 for end-to-end auth.
  • Blocks: #547–#550 management feature routes should build on this foundation.
Epic: #543 Story Points: 5 --- SO THAT management routes share one Express surface with auth middleware and the existing `/api/reload-db` hook is no longer unauthenticated. Expand the in-process Express app (`src/webhooks.ts`) into a Management API foundation: structured routing, shared auth middleware hook-point, and protect `/api/reload-db`. ## Scope Foundational Express structure for the Web Management UI epic ([#543](https://git.vylpes.xyz/External/card-drop/issues/543)). Discord OAuth and sessions are implemented in #545; this story defines the routing layout and protected `/api` surface those features plug into. **In scope:** Refactor/bootstrap Management API, protected route registration, auth middleware hook, securing existing `POST /api/reload-db`. **Out of scope:** Discord OAuth flows (#545), React SPA shell (#546), feature endpoints (settings/cards/ops #547–#550), static asset serving (#546). ## Acceptance criteria - **GIVEN** the bot process starts with `EXPRESS_PORT` set **WHEN** the Management API boots **THEN** it uses the existing in-process Express entry (`src/webhooks.ts` or successor) and listens on the same port as today. - **GIVEN** the Management API foundation **WHEN** a new management handler is added (current or future stories) **THEN** it registers on a shared protected router (e.g. under `/api/`) so all management routes use the same middleware stack. - **GIVEN** a caller without a valid admin session **WHEN** they `POST /api/reload-db` **THEN** the response is **401** or **403** and `CardMetadataFunction` does **not** run. - **GIVEN** a caller with a valid admin session (session shape defined in #545) **WHEN** they `POST /api/reload-db` with a session belonging to a Discord user ID in `BOT_ADMINS` **THEN** behaviour matches today (reload card metadata, **200** on success). - **GIVEN** public auth routes from #545 (e.g. OAuth callback/login) **WHEN** they are mounted **THEN** they are **not** behind the admin-session middleware (mounted on a separate public router). - **GIVEN** protected management routes **WHEN** authorization is evaluated **THEN** v1 checks only **Admin** membership (`BOT_ADMINS`), but the middleware/resource-check shape allows finer roles later (#551) without rewriting route registration. - **GIVEN** automated tests exist for the Management API **WHEN** CI runs **THEN** tests cover at least: unauthenticated `POST /api/reload-db` rejected; authenticated admin can invoke reload (may stub session until #545 lands). ## Subtasks - [ ] Refactor Express bootstrap into a Management API module (router + app wiring from `src/webhooks.ts`) - [ ] Add shared `requireManagementAuth` middleware (admin session / `BOT_ADMINS` check; integrates with #545 session) - [ ] Mount `POST /api/reload-db` on the protected router (move from public registration) - [ ] Expose a registration pattern for future `/api/*` management routes (#547–#550) - [ ] Add/update unit or integration tests for auth gating on `/api/reload-db` - [ ] Document breaking change: unauthenticated reload no longer allowed (coordinated release with #545) ## Notes - Decision: [#542](https://git.vylpes.xyz/External/card-drop/issues/542) — same bot process; secure existing webhook/management routes. - Existing handler: `src/hooks/ReloadDB.ts`. - **Release:** Do not enable production protection on `/api/reload-db` until #545 provides login/session (merge or ship both in the same release). - Depends on: none for structure; **pairs with #545** for end-to-end auth. - Blocks: #547–#550 management feature routes should build on this foundation.
Smithy-bot added this to the 0.14.0 milestone 2026-09-29 19:08:34 +01:00
Smithy-bot stopped working 2026-10-19 12:02:18 +01:00
15 seconds
Smithy-bot added spent time 2026-10-19 12:03:19 +01:00
15 minutes
Author
Member

Estimate: 5 story points

Area Rationale
Express refactor Split/bootstrap Management API from src/webhooks.ts without changing listen behaviour
Auth middleware requireManagementAuth + admin/BOT_ADMINS check with a shape that #551 can extend
Route wiring Protected /api router, public router hook for #545, move POST /api/reload-db
Tests Integration/unit coverage for unauthenticated reject + authenticated admin reload (session stub OK until #545)
Release coordination Doc note on breaking unauthenticated reload; ship with or after #545

Criteria are clear; no open questions. Moving to needs/tests.

## Estimate: **5** story points | Area | Rationale | |------|-----------| | Express refactor | Split/bootstrap Management API from `src/webhooks.ts` without changing listen behaviour | | Auth middleware | `requireManagementAuth` + admin/`BOT_ADMINS` check with a shape that #551 can extend | | Route wiring | Protected `/api` router, public router hook for #545, move `POST /api/reload-db` | | Tests | Integration/unit coverage for unauthenticated reject + authenticated admin reload (session stub OK until #545) | | Release coordination | Doc note on breaking unauthenticated reload; ship with or after #545 | Criteria are clear; no open questions. Moving to **needs/tests**.
Smithy-bot stopped working 2026-10-20 12:03:35 +01:00
17 seconds
Author
Member

Planning — needs/tests → needs/approval

QA scripts mapped to the acceptance criteria in the issue description. Run on a branch with this story and #545 (or session stubs) on stage; automated rows via yarn test locally/CI.

Preconditions

  • Bot process with EXPRESS_PORT set (same as today).
  • BOT_ADMINS includes at least one Discord user ID used for “admin” tests.
  • Non-admin Discord user ID not in BOT_ADMINS.
  • Ability to POST to management host (curl/httpie) and run yarn test.
  • Release note: full manual unauthenticated-reload checks apply only after #545 login/session is available in the target environment (or use test stubs per AC7).

AC1 — Management API boots on existing Express entry and port

ID Steps Expected
T1.1 Start bot with EXPRESS_PORT configured Process listens on that port; no second HTTP server for management
T1.2 Inspect startup logs / health Express app from src/webhooks.ts (or successor module) is the entry; no regression to Discord bot startup
T1.3 Hit an existing public webhook route (if any) on same port Still responds as before refactor

AC2 — Shared protected router for /api/* management routes

ID Steps Expected
T2.1 Code review: new management handler registration API Handlers mount on shared protected router under /api/
T2.2 Add a dummy protected route in test harness (or inspect registration for reload-db) Route inherits same middleware stack as other protected /api routes
T2.3 Future-story check (#547–#550) Pattern documented/exposed so feature routes do not register ad-hoc on app root

AC3 — Unauthenticated POST /api/reload-db rejected; reload does not run

ID Steps Expected
T3.1 POST /api/reload-db with no session cookie / auth header 401 or 403
T3.2 Same request; observe logs / mocks CardMetadataFunction (reload handler) not invoked
T3.3 Repeat with invalid/expired session cookie 401 or 403; reload not invoked
T3.4 Repeat with session for user not in BOT_ADMINS 401 or 403; reload not invoked

AC4 — Authenticated admin POST /api/reload-db succeeds

ID Steps Expected
T4.1 Establish valid admin session (via #545 OAuth flow on stage, or test stub matching #545 session shape) for Discord ID in BOT_ADMINS Session cookie/header accepted by middleware
T4.2 POST /api/reload-db with that session 200 on success (same as pre-change behaviour)
T4.3 Verify side effect Card metadata reload runs (same observable outcome as today: DB/metadata refresh, no error)
T4.4 Compare response body/status to baseline on main before protection Functional parity for authorized admin

AC5 — Public auth routes (#545) not behind admin middleware

ID Steps Expected
T5.1 GET/POST OAuth/login/callback routes from #545 without admin session Not blocked by requireManagementAuth (may redirect or return OAuth-specific responses)
T5.2 Code review: router mount order Public auth router separate from protected /api router
T5.3 Unauthenticated access to public auth endpoints Does not require BOT_ADMINS session

AC6 — v1 Admin-only check; extensible for #551

ID Steps Expected
T6.1 Admin session (BOT_ADMINS) on protected route Allowed
T6.2 Non-admin authenticated session (if simulatable) on protected route Denied
T6.3 Code review: middleware signature / resource-check hook No hard-coded per-route auth copy-paste; shape allows future role/resource checks (#551) without re-registering all routes

AC7 — Automated tests in CI

ID Steps Expected
U1 yarn test — Management API / reload-db auth tests Green
U2 Test: unauthenticated POST /api/reload-db Assert 401 or 403; reload stub not called
U3 Test: authenticated admin session (stub OK until #545 merges) Assert 200 and reload invoked
U4 Forgejo Actions / CI on PR Test job passes

Subtasks / documentation

ID Steps Expected
D1 Read release/docs note for breaking change States unauthenticated reload no longer allowed; coordinated deploy with #545
R1 Regression: Discord commands and non-management HTTP behaviour Unchanged aside from /api/reload-db auth
R2 Do not enable production-only protection before #545 is live Release checklist satisfied

Sign-off checklist

  • T1–T6, U1–U4, D1, and R1–R2 pass (stage and/or stubs as noted).
  • #545 paired for end-to-end admin session on stage before production cutover.

Moving to needs/approval for your review before implementation. @Vylpes

## Planning — needs/tests → needs/approval QA scripts mapped to the acceptance criteria in the issue description. Run on a branch with this story **and** #545 (or session stubs) on **stage**; automated rows via `yarn test` locally/CI. ### Preconditions - Bot process with `EXPRESS_PORT` set (same as today). - `BOT_ADMINS` includes at least one Discord user ID used for “admin” tests. - Non-admin Discord user ID **not** in `BOT_ADMINS`. - Ability to `POST` to management host (curl/httpie) and run `yarn test`. - **Release note:** full manual unauthenticated-reload checks apply only after #545 login/session is available in the target environment (or use test stubs per AC7). --- ### AC1 — Management API boots on existing Express entry and port | ID | Steps | Expected | |----|--------|----------| | T1.1 | Start bot with `EXPRESS_PORT` configured | Process listens on that port; no second HTTP server for management | | T1.2 | Inspect startup logs / health | Express app from `src/webhooks.ts` (or successor module) is the entry; no regression to Discord bot startup | | T1.3 | Hit an existing public webhook route (if any) on same port | Still responds as before refactor | --- ### AC2 — Shared protected router for `/api/*` management routes | ID | Steps | Expected | |----|--------|----------| | T2.1 | Code review: new management handler registration API | Handlers mount on shared protected router under `/api/` | | T2.2 | Add a dummy protected route in test harness (or inspect registration for `reload-db`) | Route inherits same middleware stack as other protected `/api` routes | | T2.3 | Future-story check (#547–#550) | Pattern documented/exposed so feature routes do not register ad-hoc on app root | --- ### AC3 — Unauthenticated `POST /api/reload-db` rejected; reload does not run | ID | Steps | Expected | |----|--------|----------| | T3.1 | `POST /api/reload-db` with **no** session cookie / auth header | **401** or **403** | | T3.2 | Same request; observe logs / mocks | `CardMetadataFunction` (reload handler) **not** invoked | | T3.3 | Repeat with invalid/expired session cookie | **401** or **403**; reload **not** invoked | | T3.4 | Repeat with session for user **not** in `BOT_ADMINS` | **401** or **403**; reload **not** invoked | --- ### AC4 — Authenticated admin `POST /api/reload-db` succeeds | ID | Steps | Expected | |----|--------|----------| | T4.1 | Establish valid admin session (via #545 OAuth flow on stage, or test stub matching #545 session shape) for Discord ID in `BOT_ADMINS` | Session cookie/header accepted by middleware | | T4.2 | `POST /api/reload-db` with that session | **200** on success (same as pre-change behaviour) | | T4.3 | Verify side effect | Card metadata reload runs (same observable outcome as today: DB/metadata refresh, no error) | | T4.4 | Compare response body/status to baseline on main before protection | Functional parity for authorized admin | --- ### AC5 — Public auth routes (#545) not behind admin middleware | ID | Steps | Expected | |----|--------|----------| | T5.1 | `GET`/`POST` OAuth/login/callback routes from #545 **without** admin session | **Not** blocked by `requireManagementAuth` (may redirect or return OAuth-specific responses) | | T5.2 | Code review: router mount order | Public auth router separate from protected `/api` router | | T5.3 | Unauthenticated access to public auth endpoints | Does **not** require `BOT_ADMINS` session | --- ### AC6 — v1 Admin-only check; extensible for #551 | ID | Steps | Expected | |----|--------|----------| | T6.1 | Admin session (`BOT_ADMINS`) on protected route | Allowed | | T6.2 | Non-admin authenticated session (if simulatable) on protected route | Denied | | T6.3 | Code review: middleware signature / resource-check hook | No hard-coded per-route auth copy-paste; shape allows future role/resource checks (#551) without re-registering all routes | --- ### AC7 — Automated tests in CI | ID | Steps | Expected | |----|--------|----------| | U1 | `yarn test` — Management API / reload-db auth tests | Green | | U2 | Test: unauthenticated `POST /api/reload-db` | Assert **401** or **403**; reload stub **not** called | | U3 | Test: authenticated admin session (stub OK until #545 merges) | Assert **200** and reload invoked | | U4 | Forgejo Actions / CI on PR | Test job passes | --- ### Subtasks / documentation | ID | Steps | Expected | |----|--------|----------| | D1 | Read release/docs note for breaking change | States unauthenticated reload no longer allowed; coordinated deploy with #545 | | R1 | Regression: Discord commands and non-management HTTP behaviour | Unchanged aside from `/api/reload-db` auth | | R2 | **Do not** enable production-only protection before #545 is live | Release checklist satisfied | --- ### Sign-off checklist - [ ] T1–T6, U1–U4, D1, and R1–R2 pass (stage and/or stubs as noted). - [ ] #545 paired for end-to-end admin session on stage before production cutover. Moving to **needs/approval** for your review before implementation. @Vylpes
Smithy-bot stopped working 2026-10-21 12:05:29 +01:00
24 seconds
Smithy-bot removed their assignment 2026-10-21 12:05:29 +01:00
Vylpes removed their assignment 2026-10-21 12:59:24 +01:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Total time spent: 15 minutes 56 seconds
Smithy-bot
15 minutes 56 seconds
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
External/card-drop#544
No description provided.