diff --git a/serverAppConfigs-docs/Dankserver/Audiobookshelf.md b/serverAppConfigs-docs/Dankserver/Audiobookshelf.md index f81a2e5..c4aefed 100644 --- a/serverAppConfigs-docs/Dankserver/Audiobookshelf.md +++ b/serverAppConfigs-docs/Dankserver/Audiobookshelf.md @@ -1,6 +1,6 @@ --- type: reference -description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, laws." +description: "Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, authentik SSO, laws." tags: - dankserver - audiobooks @@ -17,7 +17,7 @@ server — it shares nothing with [[Plex]] or [[Jellyfin]] and owns no files on array. **Host:** Dankserver — **Unit:** `audiobookshelf.service` (rootless Quadlet, user -manager) — **Internal-only:** LAN address, no vhost, no DNS name. +manager) — **Internal-only:** LAN address `http://192.168.0.235:8000/audiobookshelf`, no vhost, no DNS name. ## Shape @@ -26,7 +26,7 @@ manager) — **Internal-only:** LAN address, no vhost, no DNS name. | Unit file | `dankserver/audiobookshelf/quadlet/audiobookshelf.container` (the authority for every number here) | | Image | `ghcr.io/advplyr/audiobookshelf` pinned by **amd64 manifest digest** — see Laws | | Port | `Environment=PORT=8000` + `PublishPort=8000:8000` — web UI and API on one port | -| Base path | `Environment=ROUTER_BASE_PATH=/` — the URL is the plain `host:port`, no `/audiobookshelf` prefix | +| Base path | `Environment=ROUTER_BASE_PATH=/audiobookshelf` — every URL below carries this prefix, and the shipped SPA cannot boot without it (Laws) | | Network | `pasta` (standalone; publishes nothing to other containers) | | Audio bind | `/mnt/entropy/Audiobooks:/audiobooks:ro` — read-only, by design | | State | `/home/dank/Containers/audiobookshelf/config` → `/config`, `…/metadata` → `/metadata` (both NVMe) | @@ -36,6 +36,7 @@ manager) — **Internal-only:** LAN address, no vhost, no DNS name. | Self-healing | `Restart=always`, `RestartSec=5`, `WantedBy=default.target` | | Deploy | `./scripts/deploy.sh dankserver --service audiobookshelf` | | Handler | `deploy_audiobookshelf` in `scripts/deploy-remote.sh` | +| Single sign-on | authentik provider `audiobookshelf`; the local `frank` account stays enabled as break-glass — see *Single sign-on* | | Library layout | one book per folder — ABS groups by the first directory holding a media file (see Laws); every move is journalled at `/mnt/entropy/.media-campaign/media-relayout-audiobooks.jsonl` | ## Bootstrap @@ -54,7 +55,7 @@ difference. The script is idempotent; re-running it costs one incremental scan. | scan | `POST /api/libraries//scan` | `scan: accepted` | | proof | `GET /api/libraries//items?limit=1` | `items: ` | -Sign in at `http://192.168.0.235:8000` as `frank`; the password is +Sign in at `http://192.168.0.235:8000/audiobookshelf` as `frank`; the password is `awk -F= '/^ABS_ADMIN_PASSWORD=/{print $2}' ~/.config/secrets/audiobookshelf` on Dankserver. Change it in **Settings → Users → frank → Password**; the secret file then holds a stale value and only matters again if the `config` bind is ever @@ -68,8 +69,9 @@ the file, not the bind: the same line count grows while the total climbs. ## Phone app The official **Audiobookshelf** app connects with Server Address -`http://192.168.0.235:8000`, username `frank`, and the same password as the web UI — -no base-path prefix, plain HTTP. Verified from a second host 2026-11-03: `/healthcheck` +`http://192.168.0.235:8000/audiobookshelf` — the base path belongs in the address — username +`frank`, and the same password as the web UI, or the SSO button; plain HTTP. Verified from a +second host 2026-11-02: `/healthcheck` and `/ping` answer 200 across the LAN and `iptables -S INPUT` carries no rule for 8000, so a phone on `192.168.0.0/24` connects with no further setup; `download: true` on the root account is what makes offline listening work. Away from the LAN it cannot connect — @@ -81,6 +83,25 @@ inside the user object. A client that reads `response.token` gets the empty stri `Authorization: Bearer ` with nothing after it, and every `/api/*` call then answers `401` as though the password were wrong. +## Single sign-on (authentik) + +Both doors take the household identity: the web UI puts **Sign in with Authentik** beside +the local form, and the phone app offers SSO on its server-address screen. The local +`frank` account stays enabled on purpose — `authActiveAuthMethods` is `local` **and** +`openid`, and `local` is how you get in when the IdP is down. + +| Piece | Owner | +|-------|-------| +| Provider `audiobookshelf`, the two groups, the scope mapping | `scripts/authentik-audiobookshelf-sso.sh` — re-run it after an authentik rebuild, because this state lives in the IdP's database and a blueprint re-import reverts it silently | +| Client secret | `ABS_OIDC_CLIENT_SECRET` in `~/.config/secrets/audiobookshelf` on Dankserver (mode 600; never in the repo, a URL or an argv) | +| The ABS half: issuer, the four IdP URLs, claim, auto-register | `scripts/audiobookshelf-init.sh`, armed automatically when that secret exists in the credential file | +| Role at first SSO sign-in | the `abs_groups` claim: `audiobookshelf_admins` → `admin`, `audiobookshelf_users` → `user`, in neither → denied (fail closed) | + +The IdP saying yes is not the same as being allowed in: with `authOpenIDAutoRegister` on, +a first sign-in creates the account, and the claim alone decides the type. A user in +neither group is refused after a successful authentication — that is the mapping working, +not a broken login. + ## Laws - **Speak the image's env names, not the README's.** The server reads `PORT`, @@ -91,6 +112,21 @@ as though the password were wrong. `active (running)` while every probe of `:8000` was refused and the deploy gate failed at 180 s. Two seconds with the pinned digest settles it: `podman inspect --format '{{.Config.Env}}' `. +- **The URL carries `/audiobookshelf`, and that is not a style choice.** The SPA in + `/app/client/dist/index.html` is built with `routerBasePath:"/audiobookshelf"`, so it asks + for its assets under that prefix whatever the server is mounted at. Mounted at `/`, every + script request falls through to the SPA fallback, comes back `text/html`, Chrome refuses to + execute it, and the page never leaves "Loading…" — while `/healthcheck`, the API and the + phone app stay perfectly healthy, which is how this hid behind an API-only verification + (measured 2026-11-02). Prove a UI claim in a browser, never from `/health`. +- **The OIDC redirect subfolder is a value you send, not a default you inherit.** The 2.17.4 + upgrade only fills `authOpenIDSubfolderForRedirectURLs` on a database that ALREADY had OIDC + enabled, so a DB created before SSO leaves it unset and the server builds + `http://host/undefined/auth/openid/callback`. authentik answers the authorize request with + "Redirect URI Error" and nothing on the ABS side looks wrong (measured 2026-11-02). The + provider's registered URI must equal `/auth/openid/callback`, so each + script derives the value from one variable of its own (`ABS_URL`, `ABS_BASE`) and + `tests/audiobookshelf_init.bats` pins what reaches the wire. - **The audio bind stays `:ro`.** ABS writes nothing that belongs beside the audio: its library, covers, progress and sidecar metadata all land in `/metadata` on the SSD. A writable bind would put stray writes on the spindle @@ -207,12 +243,12 @@ as though the password were wrong. ## Verification `./tests/run.sh` covers this unit through `tests/dankserver_quadlet.bats` -(boot persistence, the SSD state roots, the `:ro` array bind, the mount gate, the -`wget` health check), `tests/audiobookshelf_init.bats` (the bootstrapper against a -stub of the API above: create-once, idempotent re-run, no secret in a URL, refusal -by URL when the server does not answer, and the deployer calling it only after the -health gate) and `tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may -reference the metadata view). +(boot persistence, the SSD state roots, the `:ro` array bind, the mount gate, the `wget` +health check, the base path the shipped SPA needs), `tests/audiobookshelf_init.bats` (the +bootstrapper against a stub of the API above: create-once, idempotent re-run, no secret in a +URL, refusal by URL when the server does not answer, the deployer calling it only after the +health gate, and the SSO arm sending the mounted base path as the redirect subfolder) and +`tests/media_overlay.bats` (only [[Plex]] and [[Jellyfin]] may reference the metadata view). Five book folders hold media no scanner can read, so they stay out of the library: three 2024 Nebula nominees in the `_unsorted` box (two `.m4b` with no `moov` atom, diff --git a/serverAppConfigs-docs/index.md b/serverAppConfigs-docs/index.md index 498a94d..2c5eeaa 100644 --- a/serverAppConfigs-docs/index.md +++ b/serverAppConfigs-docs/index.md @@ -44,7 +44,7 @@ is a locator: one row, one clause, no numbers. | Page | Owns | |---|---| -| [[Audiobookshelf]] | Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, laws. | +| [[Audiobookshelf]] | Audiobookshelf on Dankserver: quadlet shape, the read-only array bind, state on the SSD, the API bootstrap, authentik SSO, laws. | | [[Calibre]] | Calibre is the e-book library manager that backs Calibre-Web. | | [[Calibre-Web]] | Calibre-Web is a web application that browses, reads, and downloads eBooks from an existing Calibre library database. | | [[Copyparty]] | Copyparty is a lightweight file server and media streamer that runs as a single Python binary. |