Commit Graph

7 Commits

Author SHA1 Message Date
bvandeusen bd362ab384 feat(server/m7-user-mgmt): admin endpoints (invites, users, promote/demote)
Five admin endpoints under existing RequireAdmin middleware:

- GET /api/admin/invites — list active + recently-redeemed
- POST /api/admin/invites — generate 24h invite, returns
  {token, expires_at, ...}. Audits ActionInviteCreate.
- DELETE /api/admin/invites/{token} — revoke unredeemed invite.
  Audits ActionInviteRevoke.
- GET /api/admin/users — list all users (id, username,
  display_name, is_admin, created_at).
- PUT /api/admin/users/{id}/admin — toggle is_admin with
  last-admin guard. Audits ActionPromoteAdmin / ActionDemoteAdmin.

Last-admin guard counts admins, refuses demotion of the sole
admin with 409 'last_admin'. Race window between count and update
is acceptable for v1 — worst case is 'no admins left,' which the
env-driven bootstrap or CLI reset can recover from. Common path
('admin demotes themselves') is now blocked.

Adds ListUsers, CountAdmins, UpdateUserAdmin sqlc queries to
users.sql.

Tests cover: invite create/list/delete round-trip, non-admin gets
403, user list, promote happy path, last-admin demotion refused,
two-admins demotion allowed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 12:01:16 -04:00
bvandeusen 4845628ae4 feat(db/m7-user-mgmt): migration 0022 + audit helper
Schema for user management U1: display_name on users; user_invites;
registration_settings singleton (default 'invite_only'); audit_log.
Plus the internal/audit package centralizing the action-name
vocabulary and JSON metadata marshaling so handlers don't repeat
boilerplate.

Race-safe first-admin uses a query-shape primitive
(CreateUserFirstAdminRace's WHERE NOT EXISTS subquery) rather than
a schema-level constraint. Concurrent empty-state registrations
both see 'no users yet' and both insert as admin — fine, having
two admins from the start is benign; what matters is at-least-one.
users.username uniqueness arbitrates if the two callers picked the
same username.

CreateUser signature gains display_name (nullable); existing
bootstrap call sites pass nil. The audit package declares the full
U1+U2+U3 action vocabulary upfront so subsequent slices are purely
additive on the caller side.

Tests cover audit Write with + without metadata and that every
declared action constant persists correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 11:51:00 -04:00
bvandeusen 1d1119ac32 feat(api): add GET/PUT /api/me/listenbrainz endpoints
Exposes user ListenBrainz config (token_set bool, enabled, last_scrobbled_at)
via write-only token semantics; enforces no-enable-without-token at the API layer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 09:27:06 -04:00
bvandeusen a0e21f21b6 feat(db): add ListenBrainz user-config and scrobble_queue queries 2026-04-28 08:42:38 -04:00
bvandeusen 8cdaf8f0f1 feat(auth): RequireUser middleware (cookie or bearer)
Resolves /api/* callers from session cookie first, Authorization
bearer second. Touches last_seen on success for future active-
sessions UI. Adds GetUserByID query used by the middleware.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 20:53:11 -04:00
bvandeusen bc22ec9a91 feat(db): add SetSubsonicPassword query
Allows setting or clearing the opt-in Subsonic legacy credential.
2026-04-19 17:37:18 +00:00
bvandeusen 34ae7d6cde M1/#292: user queries (create + lookup by username/token + count) 2026-04-19 02:31:39 +00:00