Files
BadNote/server/badnote_server/config.py
Akiba So 198da00ecd
All checks were successful
CI / Windows build (push) Successful in 7m47s
feat: vault-aligned server v1 + UX polish
Redesign the optional FastAPI companion around vault files (manifest /
PUT/GET/DELETE + OCR jobs) instead of legacy strokes_json notes. Wire a
client Server settings panel for health/login. Polish shell UX: l10n for
settings/home/board, sticky-board empty state, and a narrow-screen
diagnostics FAB.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-05 19:04:48 +08:00

77 lines
3.0 KiB
Python

"""BadNote server configuration via environment variables."""
import os
import secrets
import warnings
def _resolve_jwt_secret() -> str:
"""Resolve the JWT signing secret.
Priority:
1. ``BADNOTE_JWT_SECRET`` environment variable (recommended for prod).
2. A persisted secret file (so the secret survives restarts and is shared
across worker processes).
3. A freshly generated secret, persisted to that file.
A per-process random secret (the previous behaviour) invalidated every
token on restart and gave each worker a different secret in multi-worker
deployments, so tokens were rejected at random. We persist instead.
"""
env_secret = os.environ.get("BADNOTE_JWT_SECRET")
if env_secret:
return env_secret
db_path = os.environ.get("BADNOTE_DB_PATH", "./data/badnote_server.db")
default_secret_file = os.path.join(os.path.dirname(db_path) or ".", ".jwt_secret")
secret_path = os.environ.get("BADNOTE_JWT_SECRET_FILE", default_secret_file)
try:
if os.path.exists(secret_path):
with open(secret_path, "r", encoding="utf-8") as f:
existing = f.read().strip()
if existing:
return existing
secret = secrets.token_urlsafe(48)
os.makedirs(os.path.dirname(secret_path) or ".", exist_ok=True)
# Restrictive permissions: only the owner may read the secret.
fd = os.open(secret_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(secret)
return secret
except OSError:
warnings.warn(
"Could not persist a JWT secret; using an ephemeral one. "
"Set BADNOTE_JWT_SECRET to keep tokens valid across restarts.",
RuntimeWarning,
)
return secrets.token_urlsafe(48)
def _resolve_cors_origins() -> list[str]:
"""Parse the allowed CORS origins from ``BADNOTE_CORS_ORIGINS``.
Comma-separated list of origins. Empty by default; the app falls back to a
permissive ``*`` (without credentials) when none are configured.
"""
raw = os.environ.get("BADNOTE_CORS_ORIGINS", "")
return [o.strip() for o in raw.split(",") if o.strip()]
class Settings:
host: str = os.environ.get("BADNOTE_HOST", "0.0.0.0")
port: int = int(os.environ.get("BADNOTE_PORT", "8080"))
db_path: str = os.environ.get("BADNOTE_DB_PATH", "./data/badnote_server.db")
storage_path: str = os.environ.get("BADNOTE_STORAGE_PATH", "./data/storage")
queue_path: str = os.environ.get("BADNOTE_QUEUE_PATH", "./data/queue")
jwt_secret: str = _resolve_jwt_secret()
jwt_expiry_hours: int = int(os.environ.get("BADNOTE_JWT_EXPIRY_HOURS", "720"))
cors_origins: list[str] = _resolve_cors_origins()
# Per-user vault trees (notebook folders + sidecars), independent of legacy
# notes.strokes_json storage.
vault_path: str = os.environ.get("BADNOTE_VAULT_PATH", "./data/vaults")
settings = Settings()