v0.321.0: FileBrowser's stale password and its own banners; one backup section on the Data tab; install address of an installed app
gates / gates (push) Successful in 43s

R-1101: the change-it line is gone for FileBrowser; in the generated state the page asks FileBrowser whether
the stored password still logs in (only its own 401/403 counts) and, refused, says so and offers the
new-password press. The rendered FileBrowser config switches off its update notice and its Install-app
prompt. The Data tab's backup part is one section in plain words (no off-drive, no PBS); the export
password eye is a text button. R-1095: /apps/{slug}/install of an installed app 302s to its settings tab.

Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-16 17:17:26 +02:00
parent c02c9d833a
commit 7e791d9b5d
30 changed files with 1262 additions and 363 deletions
@@ -186,7 +186,8 @@ func fbSelfID(do fbHTTPDo, base, token string) (int, error) {
// The password is 16 letters and digits (generateValue "password:16") — never a comma, which the CLI's `user,pw`
// form would split on. It is on the one-shot container's command line for about a second, inside the box.
// ErrFileBrowserNotOperator: the reset is offered only while the box does not know the password.
// ErrFileBrowserNotOperator: the reset is offered only while the box does not know the password (`operator`, or
// `generated` with the stored value refused — R-1101).
var ErrFileBrowserNotOperator = fmt.Errorf("the file manager's password is not in the operator state")
// fileBrowserContainer is the infra container's fixed name (infra.RenderFileBrowserCompose).
@@ -203,7 +204,9 @@ func (m *Manager) ResetFileBrowserAdminPassword(ctx context.Context) error {
// The base-stack tick must not start the file browser while it is stopped for the CLI.
m.infraMu.Lock()
defer m.infraMu.Unlock()
if state, _, _ := m.settings.GetFileBrowserAdmin(); state != settings.FileBrowserAdminOperator {
// v0.321.0 (R-1101): also when the stored password is refused — the household changed it in FileBrowser.
if state, _, _ := m.settings.GetFileBrowserAdmin(); state != settings.FileBrowserAdminOperator &&
!(state == settings.FileBrowserAdminGenerated && m.FileBrowserStoredPasswordRefused()) {
return ErrFileBrowserNotOperator
}
if len(m.encKey) == 0 {
@@ -303,3 +306,44 @@ func fileBrowserCLISetPassword(ctx context.Context, pw string) error {
}
return nil
}
// ── v0.321.0 (R-1101): the stored password, checked against FileBrowser ─────────────────────────────────────────────
//
// In the `generated` state the box shows the password it set. If the household changed it in FileBrowser, that value
// is refused, and the page used to keep offering it. The page now asks FileBrowser first (once per page load, the same
// login R-513 uses, the value never logged) and, on a refusal, says so and offers the new-password press.
// fbCheckTimeout bounds the page's check, so a FileBrowser that does not answer never holds the page.
const fbCheckTimeout = 3 * time.Second
// FileBrowserStoredPasswordRefused reports true ONLY when FileBrowser positively refuses the stored password (401/403)
// in the `generated` state. Anything else — another state, no stored value, a decrypt error, FileBrowser not
// answering — is false: an unproven refusal must not tell the household its password changed. Pinned by
// TestFileBrowserStoredPassword_RefusedOnlyOnA401.
func (m *Manager) FileBrowserStoredPasswordRefused() bool {
if m.settings == nil || len(m.encKey) == 0 {
return false
}
state, enc, _ := m.settings.GetFileBrowserAdmin()
if state != settings.FileBrowserAdminGenerated || enc == "" {
return false
}
pw, err := crypto.Decrypt(m.encKey, enc)
if err != nil || strings.TrimSpace(pw) == "" {
return false
}
do := m.fbHTTP
if do == nil {
do = (&http.Client{Timeout: fbCheckTimeout}).Do
}
base := fileBrowserBaseURL
if m.fbBaseURL != "" {
base = m.fbBaseURL
}
_, code, err := fbLogin(do, base, pw)
refused := err == nil && (code == http.StatusUnauthorized || code == http.StatusForbidden)
if refused {
m.logger.Printf("[WARN] [infra] filebrowser: the stored admin password is refused (HTTP %d) — changed in FileBrowser; the page offers a new one (value never logged)", code)
}
return refused
}