docs: document /auth/me self surface; mark PLAN §8.1 done

Counterpart to RunicGateway/website#76 (role-agnostic /auth/me/* self surface).

- BACKEND_DESIGN.md: add the /auth/me/account* rows to the /auth API contract and
  a note that the surface reuses account.controller behind requireAuth (any role),
  so a client manages its own account without touching /admin.
- android/PLAN.md: mark §8 item 1 (role-agnostic self-service) DONE and update the
  prerequisite-progress summary; version/health (item 4) and branding (item 6)
  remain open, push (item 3) is post-v1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NgyHnrNa8WwG3doxvxjuCr
This commit is contained in:
2026-07-19 04:56:55 -05:00
parent a3ede38c5d
commit 63bce88bd7
2 changed files with 24 additions and 10 deletions

View File

@@ -6,10 +6,10 @@ API changes it depends on can be landed in `website/` and `docs/` first. When we
authoritative API reference is the committed OpenAPI spec at
`website/server/swagger/swagger-output.json` (regenerated via `npm run swagger`).
**Prerequisite progress (§8):****Password reset** — the "build FIRST, before app work" prerequisite
(§8 item 2) is **done** (RunicGateway/website#75 + docs#8, 2026-07-19). The remaining §8 items are still
open: role-agnostic `/auth/me/*` self surface (item 1), version/health for first-run (item 4), branding
confirmation (item 6); push (item 3) is post-v1.
**Prerequisite progress (§8):****Password reset** (§8 item 2 — the "build FIRST" prerequisite;
website#75 + docs#8) and ✅ **role-agnostic `/auth/me/*` self surface** (§8 item 1) are **done**
(2026-07-19). Still open: version/health for first-run (item 4) and branding confirmation (item 6);
push (item 3) is post-v1.
The workspace already holds `website/`, `link/`, `servuo-plugins/`, and `docs/`. `android-app/` is
the fifth repo. It is **purely an API client of the website backend** — it never talks to the
@@ -290,16 +290,18 @@ web client and the app both consume v1; a second parallel route tree + Swagger s
maintenance cost. Reserve v2 for a real breaking re-shape if one ever arises.
**To build (all additive, v1):**
1. **Role-agnostic self-service under `/auth/**` (§6.4, decided).** Mount the existing
`account.controller` self handlers behind `requireAuth` (any role), so the app has one self surface
and never touches `/admin`. Keep the old `/player/account/*` + `/admin/account/*` routes for web
back-compat. New canonical routes:
- `GET /auth/me` — current `{ id, username, role }` (already exists; the app's role source).
1. **Role-agnostic self-service under `/auth/**` (§6.4, decided).**
✅ **DONE (2026-07-19, RunicGateway/website#76 (+ this docs PR)).** A `me.routes.js`
sub-router mounts the existing `account.controller` self handlers behind `requireAuth` (any role) at
`/auth/me/*`, so the app has one self surface and never touches `/admin`. The old
`/player/account/*` + `/admin/account/*` routes stay for web back-compat. Shipped routes:
- `GET /auth/me` — current `{ id, username, role }` (already existed; the app's role source).
- `GET /auth/me/account` — full self account.
- `PATCH /auth/me/account/username`, `PATCH /auth/me/account/password`.
- `POST /auth/me/account/totp/setup|enable|disable`.
- `GET /auth/me/account/identities`, `DELETE /auth/me/account/identities/:provider`.
- Regenerate Swagger; add `#swagger` annotations for each.
- Swagger regenerated with `#swagger` annotations; `test/authMe.test.js` guards the auth gate; and
an end-to-end smoketest confirmed both a player and an editor (staff) drive the same surface.
2. **Password reset — build on backend + web front end FIRST (a prerequisite, not app scope).**
✅ **DONE (2026-07-19, RunicGateway/website#75 + docs#8).** Full platform flow shipped in `website/`:
request-reset (`POST /auth/password/forgot`, always a generic 200 — no account enumeration) emails a

View File

@@ -172,6 +172,18 @@ accepts `Authorization: Bearer` for API testing).
| POST | `/password/forgot` | — (rate-limited) | `{email}` | email a single-use, ~1h reset link to **every active account** on the address; **always** returns the same generic 200 (no account enumeration). Email is non-unique, so several accounts may each get a link naming their username. Logs `account.password.reset.request`. |
| GET | `/password/reset/:token` | — | — | validate a link → `{username}` for the form, else 404 (never distinguishes expired/used/never-existed) |
| POST | `/password/reset/:token` | — (rate-limited) | `{password}` | consume the single-use link, rotate the hash, and revoke **all** sessions (web cutoff + mobile refresh tokens). Does **not** sign the user in — they log in fresh (so a 2FA account still passes TOTP). Logs `account.password.reset.complete`. |
| GET | `/me/account` | cookie / bearer | — | full self account (`id, username, role, email, status, totp_enabled, has_password`) |
| PATCH | `/me/account/username` | cookie / bearer (rate-limited) | `{username}` | change own username; re-issues the caller's session |
| PATCH | `/me/account/password` | cookie / bearer (rate-limited) | `{newPassword, currentPassword?}` | change/set own password (current required unless the account has none); revokes other sessions, keeps the caller's |
| POST | `/me/account/totp/setup` · `…/enable` · `…/disable` | cookie / bearer | `{code}` on enable/disable | self 2FA enrollment (disable needs a valid current code, not a password) |
| GET | `/me/account/identities` · DELETE `…/:provider` | cookie / bearer | — | list / unlink own SSO identities |
**Role-agnostic self-service (`/auth/me/*`).** The canonical "me" surface for **every** authenticated
role. It reuses the exact `account.controller` handlers as `/player/account/*` and `/admin/account/*`
(no logic duplication) behind `requireAuth` **only** — any active account, never a specific role. This
lets a client (the Android app) manage its own account through one surface without ever touching
`/admin` (docs/android/PLAN.md §6.4). The older `/player/account/*` + `/admin/account/*` routes stay
for web back-compat.
**Password reset.** Uses the same audited pattern as `user_invites`: an opaque 32-byte token
whose **sha256 hash only** is stored in `password_resets`, single-use and short-lived (~1h). It