diff --git a/app.py b/app.py index 35dff5e..8349d33 100644 --- a/app.py +++ b/app.py @@ -22,7 +22,7 @@ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # Docker tab caches stale container IDs, so the footer is the reliable way # to confirm a `docker compose up -d --build` actually took effect. # Both are env-overridable if the deploy pipeline ever injects them. -APP_VERSION = os.environ.get("APP_VERSION", "1.6.0") +APP_VERSION = os.environ.get("APP_VERSION", "1.6.1") APP_VERSION_DATE = os.environ.get("APP_VERSION_DATE", "2026-08-27") # Must point at a path that survives `docker compose up --build` — the @@ -45,6 +45,19 @@ def save_settings(data): with open(SETTINGS_FILE, "w") as f: json.dump(existing, f, indent=2) +def delete_setting(key): + """Remove a key outright. save_settings() only ever merges, so it can't + take something away — used to retire the plaintext shared password once + real accounts exist. Returns True if the key was there.""" + settings = load_settings() + if key not in settings: + return False + del settings[key] + os.makedirs(os.path.dirname(SETTINGS_FILE) or ".", exist_ok=True) + with open(SETTINGS_FILE, "w") as f: + json.dump(settings, f, indent=2) + return True + app = Flask(__name__) app.secret_key = os.environ.get("SECRET_KEY") or secrets.token_hex(32) diff --git a/manage_users.py b/manage_users.py index 1e64dcd..22f804e 100644 --- a/manage_users.py +++ b/manage_users.py @@ -12,10 +12,17 @@ Run it inside the container, where Werkzeug and the real settings.json are: docker exec -it arr-summary python3 manage_users.py add docker exec -it arr-summary python3 manage_users.py passwd docker exec -it arr-summary python3 manage_users.py remove + docker exec -it arr-summary python3 manage_users.py retire-shared-password The first `add` switches the dashboard from its single shared password to per-user logins. Until then the old LOGIN_PASSWORD keeps working, so you can't lock yourself out by deploying this. + +Once accounts exist and you've confirmed they sign in, +`retire-shared-password` deletes the old plaintext login_password from +settings.json. It refuses to run while no accounts exist, because with +nothing configured at all the dashboard treats that as dev mode and lets +anyone in without a password. """ import sys @@ -24,7 +31,10 @@ import getpass from app import ( SETTINGS_FILE, PASSWORD_HASH_METHOD, + LOGIN_PASSWORD, save_settings, + delete_setting, + load_settings, load_users, generate_password_hash, ) @@ -122,7 +132,49 @@ def cmd_remove(args): return 0 -COMMANDS = {"list": cmd_list, "add": cmd_add, "passwd": cmd_passwd, "remove": cmd_remove} +def cmd_retire_shared_password(_args): + """Delete the plaintext login_password from settings.json. + + Guarded, because removing it while nothing else is configured makes + get_active_password() empty — and the login route reads "no credentials + at all" as dev mode and lets anyone straight in. + """ + users = load_users() + if not users: + print("Refusing: no accounts exist yet, so the shared password is still", + file=sys.stderr) + print("the only way in. Run `add ` first and check you can", + file=sys.stderr) + print("sign in with it, then run this again.", file=sys.stderr) + return 1 + + if "login_password" not in load_settings(): + print("Nothing to do — settings.json has no login_password.") + if LOGIN_PASSWORD: + print() + print("Note: LOGIN_PASSWORD is still set in the environment. It's") + print("ignored while accounts exist, but clear it from .env too if") + print("you want the old shared password gone entirely.") + return 0 + + delete_setting("login_password") + print(f"Removed the plaintext login_password from {SETTINGS_FILE}.") + print(f"{len(users)} account(s) remain in use: {', '.join(sorted(users))}") + if LOGIN_PASSWORD: + print() + print("LOGIN_PASSWORD is still set in the environment. It's ignored while") + print("accounts exist, but clear it from .env and restart to be rid of it.") + print("Don't clear it before you've confirmed the accounts work.") + return 0 + + +COMMANDS = { + "list": cmd_list, + "add": cmd_add, + "passwd": cmd_passwd, + "remove": cmd_remove, + "retire-shared-password": cmd_retire_shared_password, +} def main(argv):