"""Setup and health checks. These answer "is this install configured enough to work?" without needing a login, so a fresh operator can see what is missing before the point where a missing piece would otherwise surface as an opaque 500. Nothing here reads or exposes a secret value; it only reports presence and reachability. """ import os from app.settings import settings # Checks whose failure means the app cannot function (can't log in, can't # persist state). Everything else is informational. CRITICAL = {"oidc", "master_key", "config_writable"} def _config_writable() -> bool: probe = settings.alembic_config_dir / ".write-probe" try: probe.write_text("ok") probe.unlink() return True except OSError: return False def oidc_configured() -> bool: return bool( settings.pocketid_issuer and settings.pocketid_client_id and settings.pocketid_client_secret ) def master_key_present() -> bool: return settings.encryption_key_file.exists() def _key_colocated() -> bool: try: key = settings.encryption_key_file.resolve() cfg = settings.alembic_config_dir.resolve() except OSError: return False return cfg == key or cfg in key.parents def checks() -> list[dict]: """Return one dict per check: key, ok, critical, detail.""" results = [ { "key": "oidc", "ok": oidc_configured(), "detail": "Login provider configured." if oidc_configured() else "Set POCKETID_ISSUER, POCKETID_CLIENT_ID and POCKETID_CLIENT_SECRET. Without them, login cannot work.", }, { "key": "master_key", "ok": master_key_present(), "detail": f"Encryption key found at {settings.encryption_key_file}." if master_key_present() else f"No encryption key at {settings.encryption_key_file}. Generate one with `openssl rand -base64 32` and mount it as the alembic_master_key secret. Saving any credential will fail until then.", }, { "key": "config_writable", "ok": _config_writable(), "detail": f"Config directory {settings.alembic_config_dir} is writable." if _config_writable() else f"Cannot write to {settings.alembic_config_dir}. Fix ownership on the host so the container user (uid 1000) can write: chown -R 1000:1000 .", }, { "key": "music_dir", "ok": os.path.isdir(settings.music_data_dir), "detail": f"Music directory {settings.music_data_dir} is present." if os.path.isdir(settings.music_data_dir) else f"Music directory {settings.music_data_dir} is not mounted.", }, { "key": "beets_library", "ok": True, # absence is normal before the first import "detail": "Library database present." if settings.beets_db_path.exists() else "No library database yet. This is normal until your first playlist sync or import.", }, ] if master_key_present() and _key_colocated(): results.append( { "key": "key_location", "ok": True, "detail": "Note: the master key sits inside the config directory. A single leaked backup would expose both the key and the encrypted credentials. Consider mounting it from a separate location.", "warn": True, } ) for r in results: r["critical"] = r["key"] in CRITICAL return results def summary() -> dict: """Overall status plus the per-check list, for /health.""" results = checks() degraded = any(c["critical"] and not c["ok"] for c in results) return {"status": "degraded" if degraded else "ok", "checks": results}