diff --git a/docs-developers/frontend/overview.md b/docs-developers/frontend/overview.md index 8093810..b0dab9f 100644 --- a/docs-developers/frontend/overview.md +++ b/docs-developers/frontend/overview.md @@ -44,7 +44,7 @@ src/components/ ui/ (design-system primitives - Text, Icon, Button, Card, Sh src/stores/ Zustand: session (connections + tokens), settings, search src/i18n/ i18next init, LanguageProvider, locale catalogs (locales/*.json) src/theme/ ThemeProvider + raw color tokens (tokens.ts) -src/lib/ storage, secure-store, paths, format, pairing, recovery, device, +src/lib/ storage, secure-store, paths, format, pairing, known-servers, device, base-url, layout (the one phone->desktop breakpoint), register-sw, and other pure helpers modules/audiosilo-player/ the local Expo module (Swift + Kotlin + TS bridge) @@ -75,7 +75,7 @@ handful of standalone screens. |---|---|---| | - (root layout) | `src/app/_layout.tsx` | Mounts the provider tree (`GestureHandlerRootView` → `SafeAreaProvider` → `LanguageProvider` → `ThemeProvider` → `ApiProvider`), hydrates the session/settings/downloads stores, imports `@/lib/register-sw` for its side effect, mounts the headless `BookEndedListener` (drives the end-of-book flow, see [Playback](playback.md#ending-a-book-end-credits-and-up-next)), and runs `useAppResume` (foreground refresh + the Android swipe-from-recents reset). Declares the `(app)` stack and the `player`/`finished` screens as `fullScreenModal`s. | | - (web HTML shell) | `src/app/+html.tsx` | The static HTML wrapper for every exported web route: PWA manifest/favicon links (base-prefixed) and a dark backdrop painted before React mounts so there is no white flash. | -| `(app)` guard | `src/app/(app)/_layout.tsx` | The auth gate: `loading` → spinner, `unauthenticated` → ``, otherwise wraps children in `AppShell` (header + nav). Also backfills `has_password`/`has_recovery` on sessions persisted before those flags existed. | +| `(app)` guard | `src/app/(app)/_layout.tsx` | The auth gate: `loading` → spinner, `unauthenticated` → ``, otherwise wraps children in `AppShell` (header + nav, which renders the `ReconnectBanner` when a connection's token is rejected - see below). Also backfills `has_password`/`has_recovery` on sessions persisted before those flags existed. | | `/` | `(app)/index.tsx` | Home: continue-listening cards, recently-added shelf, favourites - aggregated **across every connected server** via the `use*All` hooks. | | `/browse?type=recent\|finished` | `(app)/browse.tsx` | The "see all" grid behind a home shelf. | | `/search` | `(app)/search.tsx` | Search across all connections, de-duplicated; shares its query text with the desktop top bar via `useSearchStore`. | @@ -85,13 +85,13 @@ handful of standalone screens. | `/library/[libraryId]/[...path]` | `(app)/library/[libraryId]/[...path].tsx` | Nested folder browse - same `BrowseScreen`, the catch-all segments become the folder `path` (helpers in `src/lib/paths.ts`). | | `/book/[libraryId]/[...path]` | `(app)/book/[libraryId]/[...path].tsx` | Book detail: play/resume, download control, chapters, bookmarks, notes, listening history, other versions of the same book. | | `/downloads` | `(app)/downloads.tsx` | Downloaded books + storage used ([Offline](offline.md)). | -| `/settings` | `(app)/settings.tsx` | Playback tunables, language, theme, connections, self-service password/recovery, per-server API keys (capability-gated), sign-out. | +| `/settings` | `(app)/settings.tsx` | Playback tunables, language, theme, connections, self-service password (the sign-out guard nudges a password-less user to set one via `sign-out-confirm.tsx`), per-server API keys (capability-gated), sign-out. | | `/player` | `src/app/player.tsx` | The full player, presented as a full-screen modal above the shell. Accepts `libraryId`/`path` (+ optional `position`/`track`) params and gates playback start on the chapters query settling. | | `/finished` | `src/app/finished.tsx` | The end-credits screen shown when a book finishes (or from the player's menu). A root modal sibling of the player; renders `EndCredits` with an "up next" suggestion. See [Playback](playback.md#ending-a-book-end-credits-and-up-next). | | `/connect` layout | `src/app/connect/_layout.tsx` | Onboarding stack. An **authenticated** user is bounced home unless they are adding another server (`?add=1`, a pairing `?token=`, or a sign-in mid-flow via `pendingServerUrl`) - the app supports multiple simultaneous server connections. | -| `/connect` | `connect/index.tsx` | Enter a server URL (or auto-redeem a pairing token arriving via deep link / QR `web_url`). | +| `/connect` | `connect/index.tsx` | Enter a server URL (or auto-redeem a pairing token arriving via deep link / QR `web_url`). When signed out of everything, also lists previously-connected servers as one-tap **Reconnect** shortcuts (`src/lib/known-servers.ts`), each pre-filling the address so the user only re-enters a code or password. | | `/connect/scan` | `connect/scan.tsx` | Camera QR scanner (`expo-camera`) for the pairing QR. | -| `/connect/sign-in` | `connect/sign-in.tsx` | Auth-code **or** username/password sign-in against the pending server. The code field accepts both invites and recovery codes - they redeem through the same path. | +| `/connect/sign-in` | `connect/sign-in.tsx` | Auth-code **or** username/password sign-in against the pending server. Reached fresh, from a **Reconnect** shortcut, or from the dead-token `ReconnectBanner` (`src/components/layout/reconnect-banner.tsx`), all with the address pre-filled via `pendingServerUrl`. The code field redeems invite codes (legacy recovery codes still redeem server-side, but the app no longer mints them). | | `/demo` | `src/app/demo.tsx` | Public demo landing: mints a throwaway session on a demo-mode server and shows the pairing QR so the same demo user opens on a phone. | ## Styling conventions diff --git a/docs-developers/frontend/testing.md b/docs-developers/frontend/testing.md index ad64def..63569b8 100644 --- a/docs-developers/frontend/testing.md +++ b/docs-developers/frontend/testing.md @@ -135,9 +135,9 @@ Co-located suites exist for: | Downloads | `src/downloads/store.test.ts` | | Stores | `src/stores/session.test.ts`, `settings.test.ts` | | i18n | `src/i18n/language.test.ts`, `language-provider.test.tsx` | -| Account flows | `src/components/account/use-recovery-code.test.tsx`, `use-sign-out.test.tsx` | +| Account flows | `src/components/account/use-api-keys-manager.test.tsx`, `use-sign-out.test.tsx` | | UI data | `src/components/ui/icon-data.test.ts` (validates every vendored SVG glyph) | -| `src/lib` helpers | `alpha-sections`, `app-resume`, `base-url`, `dedup`, `format`, `nav`, `pairing`, `paths`, `progress-view`, `recovery`, `scroll-memory`, `secure-store`, `share`, `support` | +| `src/lib` helpers | `alpha-sections`, `app-resume`, `auth-failure`, `base-url`, `dedup`, `format`, `known-servers`, `nav`, `pairing`, `paths`, `progress-view`, `scroll-memory`, `secure-store`, `share`, `support` | Not covered by unit tests, by design or necessity: `src/app/**` screens (kept logic-free), and the **native module** (`modules/audiosilo-player`) - Swift and diff --git a/docs-developers/server/api/reference.md b/docs-developers/server/api/reference.md index cb7ee61..b81dd68 100644 --- a/docs-developers/server/api/reference.md +++ b/docs-developers/server/api/reference.md @@ -249,6 +249,13 @@ Response: `204 No Content`. ### `POST /api/v1/auth/recovery` +:::caution Legacy - being retired +The frontend player no longer offers recovery codes; it nudges users to set a +password and relies on the reconnect flow instead. This endpoint (and the two +recovery revoke routes below) is kept only so older app builds keep working, and +is slated for removal once those clients age out. New clients should not call it. +::: + *Session.* Mints (or replaces) the caller's durable **recovery code** - an auth code with unlimited uses and no expiry, owned by the user, redeemable through the normal `/auth/redeem` flow. Returned exactly once; only its hash is stored. @@ -263,8 +270,8 @@ Response `201`: ### `DELETE /api/v1/auth/recovery` -*Session.* Removes the caller's recovery code (no-op if none). Response: -`204 No Content`. +*Legacy (see above).* *Session.* Removes the caller's recovery code (no-op if +none). Response: `204 No Content`. ## Personal API keys @@ -1010,8 +1017,9 @@ logs; the connect page auto-redeems it client-side. ### `DELETE /api/v1/admin/users/{id}/recovery` -Revokes a user's recovery code (the admin's only lever for a leaked one, since -recovery codes are not listable). No-op if none. `204 No Content`. +*Legacy - being retired alongside `POST /auth/recovery` once old clients age +out.* Revokes a user's recovery code (the admin's only lever for a leaked one, +since recovery codes are not listable). No-op if none. `204 No Content`. ### `POST /api/v1/admin/authcodes/{id}/rotate` diff --git a/docs-developers/server/auth-and-security.md b/docs-developers/server/auth-and-security.md index 3cf12fd..ebaff88 100644 --- a/docs-developers/server/auth-and-security.md +++ b/docs-developers/server/auth-and-security.md @@ -155,6 +155,15 @@ password-less user who saved a recovery code can re-pair on any device via the normal connect screen, with no admin involved. `GenerateRecoveryCode` atomically replaces any existing recovery code for the user. +:::note Client UI retired +The frontend player no longer mints or reads recovery codes - it nudges users to +set a password and uses the reconnect flow (see +[Frontend overview](../frontend/overview.md)) instead. The server keeps `POST +/auth/recovery` and the admin revoke route so older app builds still redeem +codes they minted earlier; both are slated for removal once those clients age +out. +::: + ## Demo-account restrictions Public demo mode (`config.demo.*`) creates throwaway `is_demo` accounts. diff --git a/docs-users/admin/users-and-invites.md b/docs-users/admin/users-and-invites.md index 639638c..ba709e0 100644 --- a/docs-users/admin/users-and-invites.md +++ b/docs-users/admin/users-and-invites.md @@ -1,6 +1,6 @@ --- title: "Users and invites" -description: "Creating AudioSilo accounts, getting people connected with invite links, recovery codes, and the safety rails around admin accounts." +description: "Creating AudioSilo accounts, getting people connected with invite links, and the safety rails around admin accounts." --- The **Users** section of the [admin console](console-tour.md) is where you @@ -100,19 +100,6 @@ downgrades it to the defaults). To kill an invite outright, click **Revoke**. -## Recovery codes - -A **recovery code** is a listener's own durable way back in: a code they mint -for themselves from the player's settings (see -[Your account](../listening/account.md)), which never expires and has no use -limit. It means someone with no password can sign out - or lose their phone - -and reconnect without needing you to mint a new invite. - -In the drawer, the **Recovery code** row only tells you whether one is *set* or -*none*. As the admin you can **Revoke** a recovery code (for example if it -leaked), but you can never view it - recovery codes belong to the user, and the -server stores only a fingerprint. - ## The user detail drawer ![The user detail drawer](/img/screenshots/admin/user-detail.png) @@ -122,8 +109,7 @@ Clicking a user opens a drawer with four blocks: - **Account** - the **Role** dropdown; **Status** with an **Enable/Disable** button; **Password** with a **Set password**/**Change** control (for a listener, saving an empty password clears it, returning the account to - invite-only pairing); the **Recovery code** status with its **Revoke** - button; and **Last active**. + invite-only pairing); and **Last active**. - **Access** - the shares granted to this user, each with a **Revoke** button, plus a **Grant access** control: pick **Whole library** or **Share**, choose the target, and click **Grant**. (For an admin this block just notes they diff --git a/docs-users/demo.md b/docs-users/demo.md index bb1987a..333848c 100644 --- a/docs-users/demo.md +++ b/docs-users/demo.md @@ -31,12 +31,8 @@ Demo accounts are deliberately disposable: ## What demo accounts can't do -To keep throwaway accounts throwaway, two things are switched off for them: - -- **Setting a password** - refused for demo accounts. -- **Minting a recovery code** - likewise refused. - -Those are the two ways a normal account makes itself durable +To keep throwaway accounts throwaway, **setting a password is switched off** for +them. That's how a normal account makes itself durable ([Your account](listening/account.md)), so a demo session can never turn itself into a permanent login. Sign out (or go idle long enough) and it's gone. diff --git a/docs-users/listening/account.md b/docs-users/listening/account.md index 52c4a51..ff544a2 100644 --- a/docs-users/listening/account.md +++ b/docs-users/listening/account.md @@ -1,6 +1,6 @@ --- title: Your account and settings -description: "The Settings screen: appearance, language, playback preferences, passwords, recovery codes, pairing extra devices, and signing out safely." +description: "The Settings screen: appearance, language, playback preferences, setting a password, pairing extra devices, and signing out safely." --- The **Settings** tab is where you tune the player and look after your account: @@ -55,23 +55,12 @@ Shows who you're signed in as, your role, and the server address - plus the two ### Set a password -Accounts created by invite often start **without** a password - you signed in with a code, and that's fine day to day. Setting one gives you a normal username + password sign-in: +Accounts created by invite often start **without** a password - you signed in with a code, and that's fine day to day. Setting a password is your **reliable way back in** on any device: - Tap **Set a password** (or **Change password**), enter a new password of at least 8 characters, and save. - Changing an existing password asks for your **current password** first. -### Recovery code - your way back in - -A **recovery code** is a code only you hold that signs you back in on any device - *"A code you keep to sign back in yourself - no admin needed."* - -Why you want one: if you sign out (or get a new phone) and have **no password and no recovery code**, the only way back into your account is asking your admin for a fresh invite. A recovery code removes that dependency entirely - it never expires and works any number of times, typed into the same code box as an invite (see [Connecting](connecting.md)). - -- Tap **Generate recovery code**. The code is shown **once** - *save it somewhere safe* (a password manager is ideal). It won't be shown again. -- **Regenerate recovery code** replaces it; the app warns you first, because the old code stops working the moment a new one exists. - -:::warning -Treat a recovery code like a password: anyone who has it can sign in as you. If yours ever leaks, regenerate it (or ask your admin to revoke it). -::: +Why it's worth doing, especially if you were invited by pairing and never set one: if you ever sign out or get a new phone, a username and password sign you straight back in with no help from anyone. Without one, getting back in means asking your admin for a fresh invite - so setting a password once is the safety net that keeps your account in your own hands. ### API keys @@ -79,10 +68,14 @@ If your server supports them, an **API keys** section here lets you create keys ### Signing out -**Sign out** disconnects this device. If you're about to sign out with **neither a password nor a recovery code**, the app stops you with a warning - *"Without one you'll need a new invite from your admin to sign back in"* - and offers to **Set a recovery code** right there. Take the offer; it's the whole reason the button exists. +**Sign out** disconnects this device. If you're about to sign out **without a password set**, the app stops you with a warning - *"Without one you'll need a new invite from your admin to sign back in on this server"* - and offers to **Set a password** right there. Take the offer; it's the whole reason the button exists. Signing out also **removes this server from the app**, so it deletes that server's downloaded books from the device and any progress that hasn't synced yet. The app warns you when there are downloads to lose. (Books, progress, and bookmarks stored on the server are safe - sign back in and they're all there.) +:::tip +Signing out isn't a big deal. Next time you open the connect screen, any server you've connected to before shows a one-tap **"Reconnect to <your server>"** shortcut with the address already filled in - so you only re-enter your code or password, never the server address. See [Connecting and signing in](connecting.md#getting-back-in-after-signing-out). +::: + ## Devices Pair another phone, tablet, or browser to your account without a new invite: tap **Add a device** and a QR code appears. Scan it with the other device (or share the link to it) and it signs straight in. The details are in [Connecting and signing in](connecting.md). diff --git a/docs-users/listening/api-keys.md b/docs-users/listening/api-keys.md index b273f83..5092786 100644 --- a/docs-users/listening/api-keys.md +++ b/docs-users/listening/api-keys.md @@ -5,11 +5,11 @@ description: "Create a personal API key so a dashboard, script, or monitoring to An **API key** is a long, secret token you create for yourself so another app can talk to your AudioSilo server *as you*, without handing over your password. It is the right tool when you want a home dashboard (such as Heimdall), a script, a cron job, or an uptime monitor to reach your server automatically. -Each key acts as you for everyday requests - browsing your libraries, reading your books, saving your listening progress. You can create as many as you like, name each one, and revoke any of them at any time. (The one thing a key can't do is manage your account itself - it can't create another key, set your password, or generate a recovery code. Those always need you signed in normally, so a leaked key can't lock you out or quietly multiply itself.) +Each key acts as you for everyday requests - browsing your libraries, reading your books, saving your listening progress. You can create as many as you like, name each one, and revoke any of them at any time. (The one thing a key can't do is manage your account itself - it can't create another key or set your password. Those always need you signed in normally, so a leaked key can't lock you out or quietly multiply itself.) ## Where to find it -In the player, open **Settings** and go to your server's account area - the same place you set a password or a recovery code (see [Your account and settings](account.md)). There you'll find an **API keys** section. +In the player, open **Settings** and go to your server's account area - the same place you set a password (see [Your account and settings](account.md)). There you'll find an **API keys** section. :::note The section only appears on servers new enough to support API keys, and it is hidden for **demo accounts** - a throwaway demo login can't create keys. diff --git a/docs-users/listening/connecting.md b/docs-users/listening/connecting.md index 36abf96..3a7ac8c 100644 --- a/docs-users/listening/connecting.md +++ b/docs-users/listening/connecting.md @@ -1,6 +1,6 @@ --- title: Connecting and signing in -description: "All the ways to sign in to an AudioSilo server: invite links, QR codes, invite or recovery codes, and username + password." +description: "All the ways to sign in to an AudioSilo server: invite links, QR codes, invite codes, and username + password." --- To listen to anything, the AudioSilo player first needs to be connected to a server - the computer where your audiobooks live. There are several ways to get connected, and they all end in the same place: signed in, with your libraries ready to browse. @@ -43,7 +43,7 @@ If you have the **AudioSilo app** installed, the surest way to sign *it* in with ## Typing a code into the app -If you have a code rather than a link - an **invite code** from your admin, or a **recovery code** you saved earlier - you can type it in: +If you have an **invite code** from your admin rather than a link, you can type it in: ![The app's connect screen asking for a server address](/img/screenshots/web-player/connect.png) @@ -51,19 +51,24 @@ If you have a code rather than a link - an **invite code** from your admin, or a 2. Enter the **server address** (e.g. `https://books.example.com`) and tap **Connect**. If you don't know the address, ask your admin. 3. On the **Sign in** screen, choose the **Code** tab, enter your code, and tap **Connect**. -The code box accepts either kind of code: - -- **Invite code** - given to you by your admin. Invites can expire or be limited to a few uses, so if one doesn't work, ask for a new invite. -- **Recovery code** - a durable code you can create for yourself in [Settings](account.md) so you can always get back in without bothering your admin. It never expires. +Invite codes are given to you by your admin. They can expire or be limited to a few uses, so if one doesn't work, ask for a new invite. ## Username and password -If you've set a password for your account (see [Your account and settings](account.md)), you can also sign in the classic way: on the **Sign in** screen, switch to the **Password** tab and enter your username and password. +If you've set a password for your account (see [Your account and settings](account.md#set-a-password)), you can also sign in the classic way: on the **Sign in** screen, switch to the **Password** tab and enter your username and password. :::note -Many AudioSilo accounts don't have a password at all - that's normal. Accounts are usually created by invite, and codes and QR pairing cover everyday sign-in. A password is optional and yours to set whenever you like. +Many AudioSilo accounts don't have a password at all - that's normal. Accounts are usually created by invite, and codes and QR pairing cover everyday sign-in. A password is optional and yours to set whenever you like - and it's the most dependable way to sign back in later (see below). ::: +## Getting back in after signing out + +Signing out is not a big deal - the app remembers every server you've connected to before. The next time you open the connect screen, each remembered server shows a one-tap **Reconnect to <server name>** shortcut with its address already filled in, so you only re-enter your code or password - never the server address again. (A **Forget** option removes a server from that list if you no longer use it.) + +If your session is ever rejected while you're using the app - for example your admin revoked this device, or the server was rebuilt - a **Reconnect to <server name>** bar appears at the top of the screen instead of failing silently. Tapping it takes you to the sign-in screen with the address pre-filled, ready for a fresh code or password. + +The most dependable key of all is a [password](account.md#set-a-password) you set in Settings: with one, reconnecting is just your username and password on any device, with no code or invite needed. + ## What the `audiosilo://` link does Links that start with `audiosilo://` are special app links. They don't open a web page - they launch the **AudioSilo app** installed on your device and hand it the sign-in details, so the app connects itself with no typing. The **Open in app** button on the connect page uses one of these. @@ -75,7 +80,7 @@ If nothing happens when you tap one, the app simply isn't installed on that devi Already signed in on one device and want another? You don't need a new invite: - In the app, go to **Settings → Devices → Add a device**. It shows a QR code (and a shareable link) that signs your other phone, tablet, or browser into the same account. -- Or create a **recovery code** in Settings and type it into the new device's code box. +- Or, if you've [set a password](account.md#set-a-password), just sign in with your username and password on the new device. You can even connect the app to **more than one server** - go to **Settings → Servers → Add a server**. Your home screen and search then span all of them. diff --git a/docs-users/listening/mobile-apps.md b/docs-users/listening/mobile-apps.md index a594259..5ba9d6e 100644 --- a/docs-users/listening/mobile-apps.md +++ b/docs-users/listening/mobile-apps.md @@ -26,7 +26,7 @@ Signing the app in is usually a scan, not typing - all the routes are covered in - **Scan a QR code** - tap **Scan QR code** on the app's connect screen and point it at the pairing QR on your server's connect page (or at the **Settings → Devices** QR on a device that's already signed in). - **Tap a link** - an invite link, or an `audiosilo://` link, opens the app and signs it in automatically. -- **Type it in** - server address plus an invite/recovery code or username and password, if you prefer. +- **Type it in** - server address plus an invite code or username and password, if you prefer. ## Background playback diff --git a/docs-users/troubleshooting.md b/docs-users/troubleshooting.md index 8ad5b5b..75b7af1 100644 --- a/docs-users/troubleshooting.md +++ b/docs-users/troubleshooting.md @@ -102,12 +102,26 @@ Invites are managed in [Users & invites](./admin/users-and-invites.md); the connect flow itself is described in [Connecting](./listening/connecting.md). :::tip -If you're signed in but worried about getting locked out later, mint a -**recovery code** from the player's Settings - it's a durable personal code -that never expires and lets you sign back in without an admin. See -[Your account](./listening/account.md). +If you're signed in but worried about getting locked out later, **set a +password** from the player's Settings. It's the durable way to sign back in on +any device without an admin - especially handy if you were invited by pairing +and never set one. See [Your account](./listening/account.md#set-a-password). ::: +### I was signed out, or a "Reconnect" bar appeared + +Being signed out is rare - app updates no longer sign you out. If it does +happen, it usually means the server stopped accepting this device: an admin +revoked it, or the server was rebuilt. The app doesn't fail silently - it shows +a **Reconnect to <your server>** bar (tap it to sign in again, with the +address already filled in), and after a full sign-out the connect screen lists +your previous servers as one-tap **Reconnect** shortcuts. + +Either way you only re-enter your code or password, never the server address. If +you have no password set, [set one](./listening/account.md#set-a-password) so +you can always get back in on any device without waiting on a fresh invite from +your admin. + ## Playback ### A book plays in the app but won't play in the browser diff --git a/screenshots/manifest.mjs b/screenshots/manifest.mjs index 62e0c40..eb720cc 100644 --- a/screenshots/manifest.mjs +++ b/screenshots/manifest.mjs @@ -18,7 +18,7 @@ export const SHOTS = [ {file: 'web-player/book-detail.png', capture: 'web', title: 'Web player - Book detail', hint: 'book page with chapters + Listen button'}, {file: 'web-player/player.png', capture: 'web', title: 'Web player - Now playing', hint: 'player with chapter list, speed, seek bar'}, {file: 'web-player/search.png', capture: 'web', title: 'Web player - Search', hint: 'search results for a query'}, - {file: 'web-player/settings.png', capture: 'web', title: 'Web player - Settings', hint: 'settings screen (account, recovery, language)'}, + {file: 'web-player/settings.png', capture: 'web', title: 'Web player - Settings', hint: 'settings screen (account, password, language)'}, {file: 'web-player/downloads.png', capture: 'web', title: 'Web player - Downloads', hint: 'offline downloads screen'}, {file: 'web-player/connect.png', capture: 'web', title: 'Web player - Connect', hint: 'connect/pairing screen with code field'}, {file: 'web-player/demo.png', capture: 'web', title: 'Web player - Demo mode', hint: 'the /web/demo landing screen'},