fix(auth-ui): admin credential fields are always visible in Authentication settings#623
Conversation
There was a problem hiding this comment.
Thanks for fixing the visibility issue here!
I do have a concern though, that is that the broader save flow still has a lockout path outside this component. SettingsView.saveSettings() saves application settings first, then persists authenticationRequired to startup config. On the backend, ConfigurationService.SaveApplicationSettingsAsync() catches admin user create/update failures, logs them, and still returns success:
listenarr.application/Common/ConfigurationService.cs, around lines 243-270.
So if admin credentials are supplied but user creation/update fails, the frontend can still proceed to save AuthenticationRequired=true. That leaves the instance requiring login without the expected working admin account, which is the same kind of lockout your PR is trying to avoid.
I don’t think the component visibility fix is wrong, just perhaps incompelte, so I’d like us to either:
- surface admin credential provisioning failures back to the caller when credentials were supplied, or
- have the auth-enable flow validate that at least one usable admin exists before persisting
AuthenticationRequired=true.
Otherwise this PR fixes the most visible symptom, but not the full auth-enable safety issue.
…oggle aborts `ConfigurationService.SaveApplicationSettingsAsync` caught any exception from the admin user create/update block, logged it, and returned successfully. That left a hard lockout vector around the credential-visibility fix in this same PR: when admin credentials were supplied but the user-service rejected them (password policy violation, repo I/O error, race with a concurrent admin write), `SettingsView.saveSettings()` would still go on to persist `AuthenticationRequired=true` on its second request — the instance ends up requiring login with no working admin to log in as. Per upstream review feedback on Listenarrs#623: re-throw the failure from the admin block so `SettingsView` aborts before the auth-toggle write. The non-admin settings row is still saved before the admin block runs (intentionally outside the admin try/catch — notification triggers, webhooks, etc. shouldn't be lost because credential provisioning failed) and the no-credentials path remains an unchanged silent skip. Two new ConfigurationService tests: 1. Failing user-service propagates to the caller; non-admin settings bundled in the same payload still land. 2. No-credentials path doesn't touch the user-service.
|
Thanks — you're right, the catch at I considered option 2 as well (validating that ≥1 admin exists before persisting Two new tests on |
…n-provisioning re-throw) Brings in 50c79de: ConfigurationService.SaveApplicationSettingsAsync now re-throws when admin provisioning fails so SettingsView aborts before persisting AuthenticationRequired=true. Addresses upstream review feedback on Listenarrs#623. # Conflicts: # CHANGELOG.md
There was a problem hiding this comment.
The backend failure path looks fixed now, thanks. Although I think we should also probably add in option 2 here because, looking over it again, there is one lockout edge case during first-time setup I missed...
If there are zero admin users, the UI still allows enabling the login screen with blank credentials or username-only. The admin fields are optional, blank values are deleted before save, ConfigurationService silently skips admin provisioning unless both username and password are non-empty, and SettingsView.saveSettings() can still persist AuthenticationRequired=true.
Now just to be clear, these lockouts are recoverable by editing config/config.json back to "AuthenticationRequired": "false",, so I don’t think it’s super severe, but it can still be confusing for the user.
…oggle aborts `ConfigurationService.SaveApplicationSettingsAsync` caught any exception from the admin user create/update block, logged it, and returned successfully. That left a hard lockout vector around the credential-visibility fix in this same PR: when admin credentials were supplied but the user-service rejected them (password policy violation, repo I/O error, race with a concurrent admin write), `SettingsView.saveSettings()` would still go on to persist `AuthenticationRequired=true` on its second request — the instance ends up requiring login with no working admin to log in as. Per upstream review feedback on Listenarrs#623: re-throw the failure from the admin block so `SettingsView` aborts before the auth-toggle write. The non-admin settings row is still saved before the admin block runs (intentionally outside the admin try/catch — notification triggers, webhooks, etc. shouldn't be lost because credential provisioning failed) and the no-credentials path remains an unchanged silent skip. Two new ConfigurationService tests: 1. Failing user-service propagates to the caller; non-admin settings bundled in the same payload still land. 2. No-credentials path doesn't touch the user-service.
50c79de to
7ce13f4
Compare
|
Good catch — you're right, the blank-credentials-with-auth-toggle-on case isn't covered by option 1 since
Also rebased onto the new |
therobbiedavis
left a comment
There was a problem hiding this comment.
I tested this manually and the backend guard does reject the no-admin auth-enable case, but the frontend currently handles that rejection as a startup-config disk persistence failure.
apiService.saveStartupConfig(newCfg) throws the 400 from the backend, then SettingsView catches it and generates a downloadable config.json containing the same rejected AuthenticationRequired=true value. The toast tells the user to save that file manually, which bypasses the new backend guard and can still produce the lockout state.
Could we distinguish validation failures from actual disk-write failures here? For the no-admin response, the UI should surface the backend error and not offer/download a replacement config. The download fallback should only run for genuine “server could not persist config to disk” cases.
Additionally there's a small test-framework nit: tests/README.md says backend test classes should define class-level Trait("Name", ...) and Trait("Category", ...) metadata. Since this PR is adding several tests to ConfigurationServiceTests, could we add:
[Trait("Name", "ConfigurationServiceTests")]
[Trait("Category", "ConfigurationService")]
above the class declaration?
…ersistence failures, plus test traits Two follow-ups to the upstream review on PR Listenarrs#623: (1) FE bypass. SettingsView.saveSettings() wrapped saveStartupConfig in a bare catch {} that treated every failure as a disk-persistence problem — offering the user a downloadable config.json containing the *server-rejected* values. This bypassed the new backend admin-existence guard entirely: a user who tries to enable the login screen with no admin user gets the backend's 400, the FE catches it, and the FE then helpfully offers a download of the same AuthenticationRequired=true config the server just refused. The catch now inspects the error's `status` property: - 4xx → validation refusal, surface as a hard error toast, skip the download. Letting the user manually save a server-rejected config would defeat the backend guard. - 5xx / network → genuine disk-persistence failure, keep the existing download fallback so the operator can save the file by hand. (2) Test conventions. tests/README.md says backend test classes should carry class-level [Trait("Name", ...)] and [Trait("Category", ...)] attributes — added them to ConfigurationServiceTests. The FE behaviour change is testable in principle but the SettingsView mount surface is large and the existing test file doesn't exercise the save flow at all; a regression test for this specific branch would add a fragile multi-mock setup that's likely to break on unrelated changes. Surfacing the trade-off in the PR reply.
|
Both addressed in FE bypass fix.
The split keeps the operator-recovery affordance for genuine persistence failures while closing the bypass. Test traits. Added One thing I didn't do — happy to add if you'd like: a regression test for the FE branch (mock 10 / 10 ConfigurationService tests passing on the rebased branch; full backend suite was 681 / 681 in the prior commit and this commit only touches FE + class attribute. |
There was a problem hiding this comment.
@kevinheneveld Manual testing confirms the no-admin auth-enable path is refused and does not persist AuthenticationRequired=true therefore i am marking this approved however, yes, I think we eventually need a test after the FE test framework is revamped (#611). Lets add a TODO in the settingsview.spec.ts and comeback to it after.
Additional non-blocking finding: the refused-save toast should eventually show a clearer, user-facing reason. Right now this path can surface raw API wording, and even the backend message could be more direct. Something like: “Login cannot be enabled until an admin username and password are saved. Enter both fields and save again.” No need to handle right now unless you want to go ahead and take care of it, but worth adding another TODO for a later UX pass.
…oggle aborts `ConfigurationService.SaveApplicationSettingsAsync` caught any exception from the admin user create/update block, logged it, and returned successfully. That left a hard lockout vector around the credential-visibility fix in this same PR: when admin credentials were supplied but the user-service rejected them (password policy violation, repo I/O error, race with a concurrent admin write), `SettingsView.saveSettings()` would still go on to persist `AuthenticationRequired=true` on its second request — the instance ends up requiring login with no working admin to log in as. Per upstream review feedback on Listenarrs#623: re-throw the failure from the admin block so `SettingsView` aborts before the auth-toggle write. The non-admin settings row is still saved before the admin block runs (intentionally outside the admin try/catch — notification triggers, webhooks, etc. shouldn't be lost because credential provisioning failed) and the no-credentials path remains an unchanged silent skip. Two new ConfigurationService tests: 1. Failing user-service propagates to the caller; non-admin settings bundled in the same payload still land. 2. No-credentials path doesn't touch the user-service.
…n-provisioning re-throw) Brings in 2e2cf59: ConfigurationService.SaveApplicationSettingsAsync now re-throws when admin provisioning fails so SettingsView aborts before persisting AuthenticationRequired=true. Addresses upstream review feedback on Listenarrs#623. # Conflicts: # CHANGELOG.md
|
Hey @kevinheneveld, please rebase this, also there are 2 codeql errors, I am not sure if you can see them (https://github.com/Listenarrs/Listenarr/pull/623/checks?check_run_id=78840520878), if not please let me know and I can provide details. |
|
The failing CodeQL check ( The flagged I'd rather not refactor clean validation logic just to dodge the scanner (and pushing would dismiss the approval) — so a dismissal as "false positive" seems right. Happy to add a code comment noting the suppression rationale if you'd prefer that on the record. |
…ation settings The Admin Account Management subpanel was gated by 'v-if="authEnabledComputed"', which made the username/password inputs invisible whenever the login screen toggle was off. Combined with the toggle reflecting the server-side AuthenticationRequired value from config.json, this created a lockout: 1. User toggles 'Enable login screen' on, but the form hasn't appeared yet because there's no admin user to manage (or just because they haven't ticked it before opening the page). 2. User saves anyway. authenticationRequired flips to 'true' in config.json. 3. Next request needs auth. No admin user exists. Login screen rejects every credential. User is locked out of their own instance with no UI affordance to provision an admin. Workaround was a curl POST to /api/v1/configuration/settings with X-Api-Key (which bypasses CSRF) and a JSON body containing AdminUsername / AdminPassword — not something a user should be expected to do. Fix: drop the v-if gate so the credential inputs render whenever the Authentication section is on screen. Updated help text and password placeholder to reflect the actual create-or-update semantics (the backend treats username+password as 'create if missing, update if present', both gated on both fields being non-empty). Added a regression test asserting the inputs render with authEnabled=false.
…een' toggle
The CheckboxCard description claimed:
'Changes here are local and will not modify server files — edit
config/config.json on the host to persist.'
That's stale and demonstrably wrong. SettingsView.onSave writes
authenticationRequired ('true' / 'false') back to the server's startup
config on every save, which is then persisted to config.json on the host.
The 'local-only' text was probably accurate during early development of
the toggle, before the persistence path was wired through.
Replaced the text with an accurate description that also points the user
at the admin credentials below — the new always-visible admin form makes
this prompt useful: the user now knows to set credentials in the same
save when they're enabling auth for the first time.
…oggle aborts `ConfigurationService.SaveApplicationSettingsAsync` caught any exception from the admin user create/update block, logged it, and returned successfully. That left a hard lockout vector around the credential-visibility fix in this same PR: when admin credentials were supplied but the user-service rejected them (password policy violation, repo I/O error, race with a concurrent admin write), `SettingsView.saveSettings()` would still go on to persist `AuthenticationRequired=true` on its second request — the instance ends up requiring login with no working admin to log in as. Per upstream review feedback on Listenarrs#623: re-throw the failure from the admin block so `SettingsView` aborts before the auth-toggle write. The non-admin settings row is still saved before the admin block runs (intentionally outside the admin try/catch — notification triggers, webhooks, etc. shouldn't be lost because credential provisioning failed) and the no-credentials path remains an unchanged silent skip. Two new ConfigurationService tests: 1. Failing user-service propagates to the caller; non-admin settings bundled in the same payload still land. 2. No-credentials path doesn't touch the user-service.
…e check
Option 1 (the prior commit) re-throws when the operator *supplied* admin
credentials but provisioning failed. It doesn't cover the carveout
Robbie identified in PR review: the settings DTO strips blank fields
before save, so a user who flips "Enable login screen" with empty (or
username-only) admin credentials silently skips provisioning entirely
and still reaches the startup-config write, locking themselves out of
an instance that now requires login but has no working admin.
`SaveStartupConfigAsync` now queries `IUserService.GetAdminUsersAsync`
when the incoming save transitions `AuthenticationRequired` from
disabled (or unset) to enabled, and throws if the admin list is empty.
Scoped to the *transition* on purpose:
- Once auth is already on, the admin must already exist (or no save
would have ever flipped it on through this same check), so every
subsequent unrelated save — API key regenerations, port changes,
log-level tweaks — must NOT re-query the admin list. This keeps
the backstop narrow and avoids breaking the session-cookie
integration tests that stub auth-on factories without populating
IUserService.
- Demotion or deletion of the last admin row while auth is enabled
is a separate concern, and belongs in the user management path
(DeleteUserAsync / demotion logic), not here.
- The common "I'm just updating other startup fields with auth off"
path stays unaffected — the admin query only fires on the
transition edge.
Three new ConfigurationService tests cover: refuse-on-transition-no-admin,
allow-on-transition-with-admin, and the "auth-already-on subsequent save"
case that the failing-test investigation surfaced.
…ersistence failures, plus test traits Two follow-ups to the upstream review on PR Listenarrs#623: (1) FE bypass. SettingsView.saveSettings() wrapped saveStartupConfig in a bare catch {} that treated every failure as a disk-persistence problem — offering the user a downloadable config.json containing the *server-rejected* values. This bypassed the new backend admin-existence guard entirely: a user who tries to enable the login screen with no admin user gets the backend's 400, the FE catches it, and the FE then helpfully offers a download of the same AuthenticationRequired=true config the server just refused. The catch now inspects the error's `status` property: - 4xx → validation refusal, surface as a hard error toast, skip the download. Letting the user manually save a server-rejected config would defeat the backend guard. - 5xx / network → genuine disk-persistence failure, keep the existing download fallback so the operator can save the file by hand. (2) Test conventions. tests/README.md says backend test classes should carry class-level [Trait("Name", ...)] and [Trait("Category", ...)] attributes — added them to ConfigurationServiceTests. The FE behaviour change is testable in principle but the SettingsView mount surface is large and the existing test file doesn't exercise the save flow at all; a regression test for this specific branch would add a fragile multi-mock setup that's likely to break on unrelated changes. Surfacing the trade-off in the PR reply.
42b85d7 to
a0535b4
Compare
|
Rebased onto |
Summary
The Admin Account Management subpanel in Settings → Authentication was gated by
v-if="authEnabledComputed", which meant the username/password inputs were invisible whenever the Enable login screen toggle was off. Combined with the toggle reflecting the server-sideAuthenticationRequiredvalue fromconfig.json, this produced a hard lockout for new installs:config/config.jsonto setAuthenticationRequired: "true"(or uses the UI toggle and clicks Save — which writes the same value).config.jsonback tofalseand restart, orcurlthe/api/v1/configuration/settingsendpoint with the API key + a JSON body containingAdminUsername/AdminPassword— which bypasses CSRF and creates the user — neither of which a user should be expected to know.Fix
Drop the
v-ifgate. The credential inputs now render whenever the Authentication section is on screen, regardless of the login-screen toggle state. Operators can configure (or pre-configure) admin credentials before, during, or after enabling auth — eliminating the lockout window.Help text and the password placeholder were updated to reflect the actual backend semantics:
ConfigurationServiceto create/update the admin user (seelistenarr.application/Common/ConfigurationService.cs~line 245).Test plan
npm run type-check— cleannpx vitest run AuthenticationSection.spec.ts— 4 / 4 passingrenders the admin credential inputs even when authEnabled is false) — asserts the form is visible in the previously-lockout-prone state. Would have caught the original regression.AuthenticationRequired: "false"inconfig.json, restart, open Settings → Authentication. Admin Username + Password fields are visible. Enter creds, click Save. Tick Enable login screen. Save again. Reload. Login screen accepts the credentials.Notes
fe/src/components/settings/AuthenticationSection.vue. No backend changes — the/api/v1/configuration/settingsendpoint already supports the create-or-update flow; it just lacked a UI affordance to drive it from a fresh install.kevinheneveld/Listenarr#7.