Make the admin a member with a flag, and drop the second listener
The admin was a set of env credentials on its own loopback listener. That bought network isolation, and charged a second port to tunnel and proxy and a second credential in the password manager. It also sat outside the SameSite protection the member cookie already had, and left every ban and password reset with no actor to log. is_admin on users reuses what was already there: the session, the login rate limiter, ban-drops-sessions, CSRF. /admin is now a route on the member mux. A member without the flag gets 404 rather than 403 — the pages are none of their business, and "forbidden" confirms there is something to be forbidden from. Registration needs an invite and invites come from /admin, so an empty database cannot grow its first user. seedAdmin breaks that circle exactly once, from ADMIN_EMAIL and ADMIN_PASSWORD, and does nothing against a database that already has users. An admin cannot ban themselves: banning drops the target's sessions, and nothing would be left that could undo it. This reverses decision 8, which is rewritten rather than deleted, along with the admin entry in the CONTEXT.md vocabulary.
This commit is contained in:
+14
-8
@@ -33,15 +33,21 @@ and `storage` was test data, so **the schema has no legacy to respect.**
|
||||
retention `DELETE`, a table and a filtered page — ~150 lines to avoid `docker compose logs`. If
|
||||
in-app visibility is ever wanted, build an *audit* view of domain events instead; those are
|
||||
queries over tables that already exist.
|
||||
8. **The admin is not a user.** Env credentials, Basic Auth, its own loopback listener. This deletes
|
||||
the `role` column, first-launch seeding, admin sessions, the "cannot ban the last admin" rules,
|
||||
and every "exclude the admin" clause that would otherwise appear in user and stats queries.
|
||||
9. **Same process, two listeners** — not a second binary. A management binary would need its own
|
||||
deploy and would race the startup migrations. Two listeners give the network isolation, which was
|
||||
the only real benefit.
|
||||
8. **The admin is a member with `is_admin` set.** ~~The admin is not a user.~~ *Reversed.* The
|
||||
original call — env credentials, Basic Auth, its own loopback listener — bought network isolation
|
||||
at the price of a second port to tunnel and proxy, and a second credential in the password
|
||||
manager. Basic Auth also sat outside the `SameSite` protection the member cookie already had, and
|
||||
left admin actions with no actor to log. One boolean column reuses the session, the login rate
|
||||
limiter, the ban-drops-sessions path and CSRF protection that all existed anyway. The costs the
|
||||
original entry named are real but small here: seeding is `seedAdmin` on an empty database, and
|
||||
the only lockout rule is that an admin cannot ban themselves. Banning a *second* admin is allowed
|
||||
— with one admin per installation there is no last-admin case to protect.
|
||||
9. **No moderator tier.** A four-level role enum was considered and dropped: nothing in the admin
|
||||
surface distinguishes a superadmin from an admin, and moderator is a second column on the day
|
||||
somebody needs to resolve reports without also being able to reset passwords.
|
||||
10. **The admin recovery endpoint is dropped.** The old app had a key-gated credential reset with a
|
||||
`qwerty123` default in `docker-compose.yml`. The password is an env var now, so recovery is
|
||||
editing it and restarting. No route, no key, no default.
|
||||
`qwerty123` default in `docker-compose.yml`. There is no route, no key and no default: an admin
|
||||
who loses their password is reset from the database, the same as any locked-out member.
|
||||
11. **Conversion runs in the background; nothing enters `songs` until it succeeds and the submitter
|
||||
confirms.** Costs a `submissions` table, buys a `songs` table where every row is a real song and
|
||||
no query filters on readiness.
|
||||
|
||||
Reference in New Issue
Block a user