feat: optional auth + backup/restore reliability fixes
Some checks failed
Lint & Test / test (push) Failing after 29s

Auth is now optional: when `auth.api_keys` is empty, all endpoints are
open (no login screen, no Bearer tokens). Health endpoint reports
`auth_required` so the frontend knows which mode to use.

Backup/restore fixes:
- Auto-backup uses atomic writes (was `write_text`, risked corruption)
- Startup backup skipped if recent backup exists (<5 min cooldown),
  preventing rapid restarts from rotating out good backups
- Restore rejects all-empty backups to prevent accidental data wipes
- Store saves frozen after restore to prevent stale in-memory data
  from overwriting freshly-restored files before restart completes
- Missing stores during restore logged as warnings
- STORE_MAP completeness verified at startup against StorageConfig
This commit is contained in:
2026-03-23 14:50:25 +03:00
parent cd3137b0ec
commit 4975a74ff3
18 changed files with 189 additions and 67 deletions

View File

@@ -15,11 +15,19 @@ logger = get_logger(__name__)
security = HTTPBearer(auto_error=False)
def is_auth_enabled() -> bool:
"""Return True when at least one API key is configured."""
return bool(get_config().auth.api_keys)
def verify_api_key(
credentials: Annotated[HTTPAuthorizationCredentials | None, Security(security)]
) -> str:
"""Verify API key from Authorization header.
When no API keys are configured, authentication is disabled and all
requests are allowed through as "anonymous".
Args:
credentials: HTTP authorization credentials
@@ -31,6 +39,10 @@ def verify_api_key(
"""
config = get_config()
# No keys configured → auth disabled, allow all requests
if not config.auth.api_keys:
return "anonymous"
# Check if credentials are provided
if not credentials:
logger.warning("Request missing Authorization header")
@@ -43,14 +55,6 @@ def verify_api_key(
# Extract token
token = credentials.credentials
# Verify against configured API keys
if not config.auth.api_keys:
logger.error("No API keys configured - server misconfiguration")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Server authentication not configured properly",
)
# Find matching key and return its label using constant-time comparison
authenticated_as = None
for label, api_key in config.auth.api_keys.items():
@@ -80,10 +84,14 @@ AuthRequired = Annotated[str, Depends(verify_api_key)]
def verify_ws_token(token: str) -> bool:
"""Check a WebSocket query-param token against configured API keys.
Use this for WebSocket endpoints where FastAPI's Depends() isn't available.
When no API keys are configured, authentication is disabled and all
WebSocket connections are allowed.
"""
config = get_config()
if token and config.auth.api_keys:
# No keys configured → auth disabled, allow all connections
if not config.auth.api_keys:
return True
if token:
for _label, api_key in config.auth.api_keys.items():
if secrets.compare_digest(token, api_key):
return True