Add multi-user: Google OAuth, roles, quotas, admin panel
Phase 2. The schema landed last commit; this makes it real. AUTH is now multi-provider. MUSE_AUTH takes a comma list, so github,google runs both. The callback URL is unchanged — /auth/callback for every provider, with the provider carried in the state COOKIE rather than the query string — so adding Google needs no edit to an already-registered GitHub app, and the callback can't be steered to a different provider by the redirect. With more than one enabled, /auth/login shows a chooser built from style attributes only, which the CSP already permits, so it needs no stylesheet and no extra hash. Google identities are the VERIFIED email. An unverified one is refused: anyone can put someone else's address on an account they control, and the allowlist is keyed on exactly that string. WHO MAY LOG IN is now two rules — the MUSE_ALLOWED_USERS bootstrap list, then a non-disabled users row. Inviting is adding a row; no email, no token, no expiry to manage. MUSE_ALLOWED_USERS stays mandatory only while the table is empty, so a working multi-user install isn't held hostage by an env var it has outgrown, but an empty database with no allowlist still refuses to start because nobody could ever get in. The rule is one function used at login AND on every request: checking only at sign-in isn't revocation, it's a delay. QUOTAS are requests per calendar month and bytes of imported audio, 0 = unlimited. Calendar month, not a rolling window — "12 left until the 1st" is something a person can plan around. Batches are checked against the whole batch, so a 30-track album with 5 requests left is refused up front rather than accepted and abandoned halfway. Storage is measured from the file actually written, not the size the peer advertised. Admins are exempt. ADMIN PANEL: host CPU/memory/disk read straight from /proc — no psutil, no compiled dependency, and in a container /proc reports the host, which is what you want for a homelab box. Plus library totals, slskd connection state, a 30-day chart, per-user and per-artist breakdowns, and inline role/quota editing. The chart is hand-rolled SVG: a charting library would add ~150KB for one stacked bar chart and most of them inject <style> at runtime, which this app's CSP blocks. Guard rails: the last active admin cannot be demoted, disabled or deleted; disabling revokes sessions immediately; deleting a user keeps their jobs, because the history records what is in the library and why. Requests are private by default — users see their own, admins see all — and retrying someone else's job returns 404 rather than 403, since a distinct error would confirm the id exists. Also drops AnimatePresence mode="wait" from the tab container. It gates the incoming panel on the outgoing one's exit animation reporting finished, so anything that stalls the frame loop leaves the UI stuck on the old tab with no way forward — found while testing, where a backgrounded page froze navigation entirely. A cosmetic transition should never gate navigation. Verified: admin endpoints 200/403/403/401 for admin/user/guest/anon; last-admin demote, disable and delete all 409; disable revokes mid-session; quota refuses the 3rd of 2 and a 30-track batch, then succeeds once raised; users see only their own jobs; cross-user retry 404s; bad role/provider/duplicate/empty patch all rejected; multi-provider chooser and both redirects correct; three CSRF variants 400; preflight refuses unknown providers, missing secrets, and an empty database with no allowlist. CPU, memory and disk confirmed populated inside the linux/amd64 container. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3b4b68daaf
commit
f3c64d4fd0
8 files changed
+1642
-171
No files matched your search
@@ -163,13 +163,30 @@ loses to a good-enough one available now.
|
||||
|
||||
## Authentication
|
||||
|
||||
Single-user GitHub OAuth, built so this can face the internet.
|
||||
OAuth against GitHub and/or Google, built so this can face the internet.
|
||||
|
||||
**It fails closed.** Auth is on unless `MUSE_AUTH=off` is set explicitly, and
|
||||
if it is on with incomplete config the app *refuses to start*. The failure mode
|
||||
worth engineering against isn't a broken login page — it's a misconfigured
|
||||
deploy quietly serving an unauthenticated download client to the internet.
|
||||
|
||||
### Who may log in
|
||||
|
||||
Two rules, checked in order:
|
||||
|
||||
1. **`MUSE_ALLOWED_USERS`** — the bootstrap allowlist. Always admitted. This is
|
||||
how the first admin gets in when the database is empty, and the recovery
|
||||
path if someone locks themselves out.
|
||||
2. **A `users` row that isn't disabled** — how everyone invited later gets in,
|
||||
with no env change and no redeploy.
|
||||
|
||||
Anyone matching neither is rejected and the attempt is logged with their
|
||||
username. The check runs on **every request**, not just at login: an
|
||||
authorization check that only runs at sign-in isn't revocation, it's a delay.
|
||||
|
||||
The first account to ever sign in becomes **admin**. No bootstrap ceremony, no
|
||||
default password.
|
||||
|
||||
### Setting it up
|
||||
|
||||
1. <https://github.com/settings/developers> → **New OAuth App**
|
||||
@@ -179,16 +196,70 @@ deploy quietly serving an unauthenticated download client to the internet.
|
||||
3. Set `MUSE_GITHUB_CLIENT_ID`, `MUSE_GITHUB_CLIENT_SECRET`,
|
||||
`MUSE_ALLOWED_USERS` (your GitHub login), and `MUSE_BASE_URL`.
|
||||
|
||||
To add Google, set `MUSE_AUTH=github,google` plus the two Google variables.
|
||||
**The callback URL is the same** — `/auth/callback` for every provider, with
|
||||
the provider carried in the state cookie rather than the URL — so adding
|
||||
Google needs no change to an already-registered GitHub app. With more than one
|
||||
provider enabled, `/auth/login` shows a chooser.
|
||||
|
||||
Google identities are the **verified** email address; an unverified one is
|
||||
refused outright, since anyone can put someone else's address on an account
|
||||
they control and the allowlist is keyed on exactly that string.
|
||||
|
||||
| variable | default | meaning |
|
||||
|---|---|---|
|
||||
| `MUSE_AUTH` | `github` | `github` or `off` |
|
||||
| `MUSE_GITHUB_CLIENT_ID` | — | OAuth app client id |
|
||||
| `MUSE_GITHUB_CLIENT_SECRET` | — | OAuth app secret |
|
||||
| `MUSE_ALLOWED_USERS` | — | comma-separated GitHub logins; **nobody else gets in** |
|
||||
| `MUSE_AUTH` | `github` | comma list of `github`, `google`, or `off` |
|
||||
| `MUSE_GITHUB_CLIENT_ID` / `_SECRET` | — | GitHub OAuth app |
|
||||
| `MUSE_GOOGLE_CLIENT_ID` / `_SECRET` | — | Google OAuth client |
|
||||
| `MUSE_ALLOWED_USERS` | — | bootstrap allowlist; required only while the users table is empty |
|
||||
| `MUSE_BASE_URL` | — | external URL, e.g. `https://muse.yourdomain.com` |
|
||||
| `MUSE_COOKIE_SECURE` | `true` | set `false` only for plain-http LAN testing |
|
||||
| `MUSE_SESSION_DAYS` | `7` | session lifetime |
|
||||
|
||||
## Multiple users
|
||||
|
||||
Roles are **admin**, **user** and **guest**. Admins get an extra tab.
|
||||
|
||||
**Inviting is just adding a row** — no email, no token, no expiry to manage.
|
||||
Add someone's GitHub username or Google address in the admin panel and they can
|
||||
sign in. Remove them and they can't.
|
||||
|
||||
### Quotas
|
||||
|
||||
Two per-user limits, both `0` = unlimited:
|
||||
|
||||
- **Requests per month** — calendar month, resets on the 1st. A rolling 30-day
|
||||
window was rejected on purpose: "12 left until the 1st" is something a person
|
||||
can plan around, where a rolling window drips capacity back at unpredictable
|
||||
times.
|
||||
- **Storage** — bytes of imported audio attributable to that user, measured
|
||||
from the file actually written rather than the size the Soulseek peer
|
||||
advertised.
|
||||
|
||||
Batches are checked against the **whole batch**: queueing a 30-track album with
|
||||
5 requests left is refused up front, not accepted and then abandoned halfway.
|
||||
Admins are exempt — somebody has to be able to fix the library when a quota is
|
||||
what's in the way.
|
||||
|
||||
### What the admin panel shows
|
||||
|
||||
Host CPU, memory and disk (read straight from `/proc` — no psutil, no compiled
|
||||
dependency, and inside a container `/proc` reports the *host*, which is what
|
||||
you want for a homelab box), library size, slskd connection state, a 30-day
|
||||
request chart, per-user and per-artist breakdowns, and the user table with
|
||||
inline role and quota editing.
|
||||
|
||||
### Guard rails
|
||||
|
||||
- **The last active admin cannot be demoted, disabled or deleted.** An install
|
||||
with no admin can't be administered and nothing in the UI can undo it.
|
||||
- **Disabling revokes sessions immediately** rather than at expiry.
|
||||
- **Deleting a user keeps their jobs.** The history is a record of what's in
|
||||
the library and why; removing a person shouldn't rewrite it.
|
||||
- **Requests are private by default.** Users see their own; admins see all
|
||||
(`?mine=true` to scope down). Retrying someone else's job returns **404**,
|
||||
not 403 — a distinct error would confirm the id exists.
|
||||
|
||||
### What it does
|
||||
|
||||
- **Server-side sessions.** The cookie carries a 256-bit random token and
|
||||
@@ -331,10 +402,18 @@ package page → Package settings → Change visibility.
|
||||
| `POST /api/request/batch` | queue many known tracks (a whole album) |
|
||||
| `POST /api/import` | queue many from pasted text |
|
||||
| `POST /api/retry/{id}` | requeue a failed job |
|
||||
| `GET /api/jobs` | request history and state |
|
||||
| `GET /api/jobs` | request history and state (own, or all for admins) |
|
||||
| `GET /api/me` | who you are, your role and quota usage |
|
||||
| `GET /api/health` | slskd reachability, mount writability |
|
||||
| `GET /api/admin/users` | list users with usage |
|
||||
| `POST /api/admin/users` | invite |
|
||||
| `PATCH /api/admin/users/{u}` | role, quotas, disable |
|
||||
| `DELETE /api/admin/users/{u}` | remove (jobs retained) |
|
||||
| `GET /api/admin/system` | CPU, memory, disk, slskd, library totals |
|
||||
| `GET /api/admin/activity` | daily volume, per-user and per-artist breakdowns |
|
||||
|
||||
Everything except `/healthz` requires a session.
|
||||
Everything except `/healthz` requires a session; `/api/admin/*` requires the
|
||||
admin role.
|
||||
|
||||
## Releases
|
||||
|
||||
|
||||
Reference in new issue
Block a user