b135f11557
Security (P0): - Remove committed session-secret default; auto-generate and persist a random secret to the config volume when SESSION_SECRET is unset (prevents forgeable session cookies / auth bypass). - Validate playlist names to a safe charset and render sldl configs via literal Python substitution instead of sed (closes a command-injection and path-traversal path through playlist names). - shlex-quote credential values written to shell-sourced env files, and strip newlines from values patched into .conf files. - Render playlist .conf files 0600; warn at startup if the master key is co-located with the config volume; document keeping it separate. Portability: - Configurable timezone via TZ (default UTC) instead of hardcoded Edmonton. - Remove personal defaults (navidrome user "andrew", ephemeral.club URLs). - Ship generic example seeds; move the cross-album dedup keep-list and the legacy playlist import to editable config files; drop the personal _upgrade.csv. - Generic VPN reference in docker-compose.snippet.yml. First-run experience: - Redirect to /setup instead of 500 when OIDC is unconfigured; surface a missing master key inline; entrypoint exits with an actionable message when the config folder is not writable. - Add unauthenticated /health (JSON) and /setup (checklist) diagnostics. Docs: - Write docs/ARCHITECTURE.md and docs/MIGRATION.md (previously referenced but missing); expand README with ownership, backups, advanced settings, and migration guidance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
from pathlib import Path
|
|
|
|
from pydantic import AliasChoices, Field
|
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
model_config = SettingsConfigDict(env_prefix="", extra="ignore")
|
|
|
|
# Mount points — see docker-compose.snippet.yml. Two volumes total:
|
|
# everything audio-related under MUSIC_DATA_DIR, everything else
|
|
# (app DB, beets config+DB, rendered pipeline credentials, job logs)
|
|
# under ALEMBIC_CONFIG_DIR.
|
|
music_data_dir: Path = Path("/data/music")
|
|
alembic_config_dir: Path = Path("/config")
|
|
pipeline_dir: Path = Path("/app/pipeline")
|
|
|
|
alembic_port: int = 8420
|
|
|
|
# Fernet key for the `secrets` table, read from a Docker secret file
|
|
# (not a plain env var) — see docker-compose.snippet.yml's `secrets:` block.
|
|
encryption_key_file: Path = Path("/run/secrets/alembic_master_key")
|
|
|
|
# Session cookie signing secret for Starlette's SessionMiddleware.
|
|
# Left empty by default on purpose: an empty/placeholder value would let
|
|
# anyone forge a signed session cookie and skip login entirely. When this
|
|
# is unset, resolve_session_secret() generates and persists a random one
|
|
# to the config volume instead of falling back to a known constant.
|
|
session_secret: str = ""
|
|
|
|
# Pocket ID OIDC client, registered once in the Pocket ID admin UI
|
|
# (redirect URI: https://<alembic-host>/auth/callback).
|
|
pocketid_issuer: str = ""
|
|
pocketid_client_id: str = ""
|
|
pocketid_client_secret: str = ""
|
|
|
|
# Defense-in-depth: this is a single-user app. If set, only this email
|
|
# may complete login even if Pocket ID ever grows a second account.
|
|
allowed_email: str = ""
|
|
|
|
# IANA timezone (e.g. "America/Edmonton") used for all scheduling and for
|
|
# times shown in the UI and the status report. Defaults to UTC so a fresh
|
|
# install behaves predictably anywhere. Reads the standard Docker `TZ`
|
|
# variable, or `ALEMBIC_TIMEZONE` if you'd rather be explicit. An
|
|
# unrecognized value falls back to UTC (see scheduler_service).
|
|
timezone: str = Field(
|
|
default="UTC",
|
|
validation_alias=AliasChoices("TZ", "ALEMBIC_TIMEZONE", "TIMEZONE"),
|
|
)
|
|
|
|
@property
|
|
def beets_dir(self) -> Path:
|
|
return self.alembic_config_dir / "beets"
|
|
|
|
@property
|
|
def beets_db_path(self) -> Path:
|
|
return self.beets_dir / "data" / "beets_library.db"
|
|
|
|
@property
|
|
def pipeline_config_dir(self) -> Path:
|
|
"""Rendered/instance pipeline config: per-playlist .conf files,
|
|
credential env files, editable lists — NOT the static code in
|
|
pipeline_dir, which is baked into the image."""
|
|
return self.alembic_config_dir / "pipeline"
|
|
|
|
@property
|
|
def app_db_path(self) -> Path:
|
|
return self.alembic_config_dir / "alembic.db"
|
|
|
|
@property
|
|
def logs_dir(self) -> Path:
|
|
return self.alembic_config_dir / "logs"
|
|
|
|
|
|
settings = Settings()
|