Files
alembic/app/templates/credentials/index.html
T
andrew b135f11557 Security hardening and first-run/portability improvements
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>
2026-07-09 14:25:55 -06:00

69 lines
3.2 KiB
HTML

{% extends "settings/_layout.html" %}
{% block settings_title %}Credentials{% endblock %}
{% block settings_content %}
<h2 style="margin-top:0;">Credentials</h2>
<p class="muted" style="margin-top:-0.75rem;">Write-only: values are encrypted at rest and never sent back to the browser. Leave a field blank to keep it unchanged.</p>
{% if not key_present %}
<div class="panel" style="border:1px solid #b91c1c;margin-bottom:1.5rem;">
<strong>No encryption key found.</strong>
<p class="muted" style="margin:0.5rem 0 0;">Credentials cannot be saved until the master key is in place. Generate one with <code>openssl rand -base64 32</code> and mount it as the <code>alembic_master_key</code> secret, then restart. See the <a href="/setup">setup check</a>.</p>
</div>
{% endif %}
<div class="cred-grid">
{% for scope, fields in scope_fields.items() %}
{% set is_core = scope in core_scopes %}
{% set is_enabled = enabled.get(scope, True) %}
<div class="cred-scope {{ 'cred-disabled' if not is_core and not is_enabled else '' }}">
<div class="panel">
<div class="cred-scope-header">
<div>
<h2>
{{ scope }}
{% if is_core %}
<span class="badge badge-info" title="Needed for the core pipeline to work">Required</span>
{% elif configured.get(scope) %}
<span class="badge badge-success">Configured</span>
{% else %}
<span class="badge badge-muted">Not configured</span>
{% endif %}
</h2>
<p class="muted cred-desc">{{ scope_descriptions.get(scope, "") }}</p>
</div>
{% if not is_core %}
<form method="post" action="/settings/credentials/{{ scope }}/toggle" class="inline">
<label class="switch" title="{{ 'Disable' if is_enabled else 'Enable' }} {{ scope }}">
<input type="checkbox" {{ "checked" if is_enabled else "" }} onchange="this.form.submit()">
<span class="switch-track"><span class="switch-thumb"></span></span>
</label>
</form>
{% endif %}
</div>
{% if not is_core and not is_enabled %}
<p class="cred-disabled-note">Disabled — its scheduled automation is paused (or, for buy-link sources, its rendered credential file is removed) until you turn this back on. Saved values below are kept.</p>
{% endif %}
<form method="post" action="/settings/credentials/{{ scope }}">
{% for field in fields %}
<div class="field wide">
<label for="{{ scope }}_{{ field }}">
{{ field }}
{% if field in configured.get(scope, []) %}<span class="badge badge-success" style="margin-left:0.5rem;">set</span>{% endif %}
</label>
{% if field in ("cookies_txt",) %}
<textarea id="{{ scope }}_{{ field }}" name="{{ field }}" rows="6" placeholder="leave blank to keep current"></textarea>
{% else %}
<input type="password" id="{{ scope }}_{{ field }}" name="{{ field }}" placeholder="leave blank to keep current" autocomplete="off">
{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn">Save {{ scope }}</button>
</form>
</div>
</div>
{% endfor %}
</div>
{% endblock %}