diff --git a/.env.example b/.env.example index 44d750c..a4a66a2 100644 --- a/.env.example +++ b/.env.example @@ -7,13 +7,8 @@ DOMAIN=media.example.com # ── Admin credentials ── -# Username for the admin panel ADMIN_USERNAME=admin - -# Bcrypt password hash — generate with: -# docker run --rm python:3.12-slim python -c \ -# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())" -ADMIN_PASSWORD_HASH= +ADMIN_PASSWORD=changeme # ── Session mode (optional) ── # isolated = each browser has its own queue (default) diff --git a/README.md b/README.md index c4954ad..3cd640e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ These are the knobs most operators actually touch — all shown commented out in | `MEDIARIP__SESSION__MODE` | `isolated` | Set to `shared` for family/team use, `open` to disable sessions entirely | | `MEDIARIP__DOWNLOADS__MAX_CONCURRENT` | `3` | Increase for faster connections, decrease on low-spec hardware | | `MEDIARIP__PURGE__MAX_AGE_MINUTES` | `1440` | Raise for longer retention, or set `PURGE__ENABLED=false` to keep forever | -| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Pre-set to skip the first-run wizard (useful for automated deployments) | +| `MEDIARIP__ADMIN__PASSWORD` | _(empty)_ | Pre-set to skip the first-run wizard (useful for automated deployments) | ### All Settings @@ -76,7 +76,8 @@ These are the knobs most operators actually touch — all shown commented out in |----------|---------|-------------| | `MEDIARIP__ADMIN__ENABLED` | `true` | Enable admin panel | | `MEDIARIP__ADMIN__USERNAME` | `admin` | Admin username | -| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash of admin password | +| `MEDIARIP__ADMIN__PASSWORD` | _(empty)_ | Admin password (plaintext — hashed on startup, never stored) | +| `MEDIARIP__ADMIN__PASSWORD_HASH` | _(empty)_ | Bcrypt hash (alternative to plaintext — for advanced users) | #### Purge @@ -115,17 +116,21 @@ These are the knobs most operators actually touch — all shown commented out in Enabled by default. On first run, you'll be prompted to set a password in the browser. -To pre-configure for automated deployments (skip the wizard): +To pre-configure for automated deployments (skip the wizard), set the password via environment variable or `.env` file: + +```yaml +# docker-compose.yml +environment: + - MEDIARIP__ADMIN__PASSWORD=your-password-here +``` ```bash -# Generate a bcrypt hash -docker run --rm python:3.12-slim python -c \ - "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())" - -# Then set in docker-compose.yml: -# MEDIARIP__ADMIN__PASSWORD_HASH=$2b$12$...your.hash... +# Or in .env file +MEDIARIP__ADMIN__PASSWORD=your-password-here ``` +The plaintext password is hashed on startup and cleared from memory — it's never stored or logged. + ### Troubleshooting: YouTube 403 Errors YouTube downloads work out of the box — yt-dlp automatically selects the right player clients. If you do hit HTTP 403 errors, it's usually one of: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index a2fe4d3..b88edea 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -97,7 +97,8 @@ class AdminConfig(BaseModel): enabled: bool = True username: str = "admin" - password_hash: str = "" + password: str = "" # Plaintext — hashed on startup, never stored + password_hash: str = "" # Bcrypt hash — set directly or derived from password # --------------------------------------------------------------------------- diff --git a/backend/app/main.py b/backend/app/main.py index 3a64e49..a269ff1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -49,6 +49,17 @@ async def lifespan(app: FastAPI): config = AppConfig() logger.info("Config loaded from defaults + env vars (no YAML file)") + # --- Derive password hash from plaintext if provided --- + if config.admin.password and not config.admin.password_hash: + import bcrypt + config.admin.password_hash = bcrypt.hashpw( + config.admin.password.encode("utf-8"), + bcrypt.gensalt(), + ).decode("utf-8") + logger.info("Admin password hashed from plaintext config") + # Clear plaintext from memory — only the hash is needed at runtime + config.admin.password = "" + # --- TLS warning --- if config.admin.enabled and config.admin.password_hash: logger.warning( diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 7436805..5bf7cbf 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -5,10 +5,6 @@ # Setup: # 1. Copy .env.example to .env and fill in your values # 2. Run: docker compose -f docker-compose.example.yml up -d -# -# Generate a bcrypt password hash: -# docker run --rm python:3.12-slim python -c \ -# "import bcrypt; print(bcrypt.hashpw(b'YOUR_PASSWORD', bcrypt.gensalt()).decode())" services: media-rip: @@ -25,7 +21,7 @@ services: # Admin panel MEDIARIP__ADMIN__ENABLED: "true" MEDIARIP__ADMIN__USERNAME: "${ADMIN_USERNAME:-admin}" - MEDIARIP__ADMIN__PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}" + MEDIARIP__ADMIN__PASSWORD: "${ADMIN_PASSWORD}" # Session mode: isolated (default), shared, or open MEDIARIP__SESSION__MODE: "${SESSION_MODE:-isolated}" expose: diff --git a/docker-compose.yml b/docker-compose.yml index 99259f3..b6b353a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,7 +30,7 @@ services: # - MEDIARIP__PURGE__MAX_AGE_MINUTES=1440 # ## Pre-set admin password (skip first-run wizard): - # - MEDIARIP__ADMIN__PASSWORD_HASH=$2b$12$... + # - MEDIARIP__ADMIN__PASSWORD=changeme restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]