2026-03-16 19:19:22 +01:00
{% extends "base.html" %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
{% block title %}Einstellungen — diora{% endblock %}
2026-03-16 19:19:22 +01:00
{% block content %}
< div class = "settings-container" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< h1 class = "settings-title" > Einstellungen< / h1 >
2026-03-16 19:19:22 +01:00
{% if lastfm_error %}
< div class = "message message-error" > {{ lastfm_error }}< / div >
{% endif %}
<!-- Last.fm section -->
< section class = "settings-section" >
< h2 > Last.fm< / h2 >
{% if has_lastfm %}
< div class = "lastfm-connected" >
< p class = "connected-status" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
Verbunden als < strong > {{ profile.lastfm_username }}< / strong >
2026-03-16 19:19:22 +01:00
< / p >
< form method = "post" class = "settings-form" id = "scrobble-form" >
{% csrf_token %}
< label class = "checkbox-label" >
< input type = "checkbox" name = "lastfm_scrobble" id = "lastfm_scrobble"
{% if profile.lastfm_scrobble %}checked{% endif %}
onchange="document.getElementById('scrobble-form').submit()">
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
Titel an Last.fm scrobbeln
2026-03-16 19:19:22 +01:00
< / label >
< / form >
< form method = "post" action = "{% url 'lastfm_disconnect' %}" class = "inline-form" style = "margin-top: 1rem;" >
{% csrf_token %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button type = "submit" class = "btn btn-danger" > Last.fm trennen< / button >
2026-03-16 19:19:22 +01:00
< / form >
< / div >
{% else %}
< p class = "lastfm-description" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
Verbinde dein Last.fm-Konto, damit gehörte Titel automatisch gescrobbelt werden.
2026-03-16 19:19:22 +01:00
< / p >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< a href = "{% url 'lastfm_connect' %}" class = "btn btn-lastfm" > Last.fm verbinden< / a >
2026-03-16 19:19:22 +01:00
{% endif %}
< / section >
<!-- Background section -->
< section class = "settings-section" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< h2 > Hintergrund< / h2 >
2026-03-19 13:39:59 +01:00
{% if request.user.profile.background_encrypted %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< p class = "lastfm-description" > Ein verschlüsselter Hintergrund ist gesetzt. < span class = "muted" > (Keine Vorschau – er wird erst auf der Hauptseite im Browser entschlüsselt.)< / span > < / p >
2026-03-19 13:39:59 +01:00
< form method = "post" action = "{% url 'delete_background' %}" class = "inline-form" >
{% csrf_token %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button type = "submit" class = "btn btn-danger" > Hintergrund entfernen< / button >
2026-03-19 13:39:59 +01:00
< / form >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< p style = "margin-top:12px;" > Neues Bild hochladen, um ihn zu ersetzen:< / p >
2026-03-19 13:39:59 +01:00
{% elif request.user.profile.background_image_data %}
2026-03-16 19:19:22 +01:00
< p >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< img src = "{{ request.user.profile.background_image_data }}" class = "bg-preview" alt = "Dein Hintergrund" >
2026-03-16 19:19:22 +01:00
< / p >
< form method = "post" action = "{% url 'delete_background' %}" class = "inline-form" >
{% csrf_token %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button type = "submit" class = "btn btn-danger" > Hintergrund entfernen< / button >
2026-03-16 19:19:22 +01:00
< / form >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< p style = "margin-top:12px;" > Neues Bild hochladen, um ihn zu ersetzen (wird verschlüsselt gespeichert):< / p >
2026-03-16 19:19:22 +01:00
{% else %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< p class = "lastfm-description" > Eigenes Hintergrundbild hochladen (JPG, PNG oder WebP, höchstens 5 MB). Es wird Ende-zu-Ende-verschlüsselt gespeichert.< / p >
2026-03-16 19:19:22 +01:00
{% endif %}
< div style = "margin-top:8px; display:flex; align-items:center; gap:10px;" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< label class = "btn" for = "bg-upload-input" > Datei auswählen< / label >
2026-03-16 19:19:22 +01:00
< input type = "file" id = "bg-upload-input" accept = ".jpg,.jpeg,.png,.webp" style = "display:none;" onchange = "uploadBackground(this)" >
< span id = "bg-upload-status" style = "font-size:0.85rem; color:#888;" > < / span >
< / div >
< / section >
<!-- Account section -->
< section class = "settings-section" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< h2 > Konto< / h2 >
< p > Angemeldet als < strong > {{ request.user.username }}< / strong > < / p >
2026-05-25 16:26:07 +02:00
< details { % if password_form_open % } open { % endif % } style = "margin-top:1rem;" >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< summary class = "btn" style = "display:inline-block;cursor:pointer;" > Passwort ändern< / summary >
2026-05-25 16:37:41 +02:00
< form id = "pw-change-form" method = "post" action = "{% url 'change_password' %}" style = "margin-top:1rem; display:flex; flex-direction:column; gap:0.6rem; max-width:320px;" >
2026-05-25 16:26:07 +02:00
{% csrf_token %}
{% for field in password_form %}
< div >
< label style = "display:block; font-size:0.85rem; margin-bottom:2px;" > {{ field.label }}< / label >
{{ field }}
{% if field.errors %}
< div class = "message message-error" style = "margin-top:4px; padding:4px 8px; font-size:0.8rem;" > {{ field.errors|join:", " }}< / div >
{% endif %}
< / div >
{% endfor %}
{% if password_form.non_field_errors %}
< div class = "message message-error" style = "padding:4px 8px; font-size:0.8rem;" > {{ password_form.non_field_errors|join:", " }}< / div >
{% endif %}
2026-05-25 16:37:41 +02:00
< div id = "pw-change-status" style = "font-size:0.85rem; color:#888;" > < / div >
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< div > < button type = "submit" class = "btn" > Speichern< / button > < / div >
2026-05-25 16:26:07 +02:00
< / form >
< / details >
< form method = "post" action = "{% url 'logout' %}" class = "inline-form" style = "margin-top:1rem;" >
2026-03-16 19:19:22 +01:00
{% csrf_token %}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
< button type = "submit" class = "btn" > Abmelden< / button >
2026-03-16 19:19:22 +01:00
< / form >
< / section >
2026-08-15 16:20:54 +02:00
<!-- API token section -->
< section class = "settings-section" >
< h2 > API-Zugriff für lokale Apps< / h2 >
< p class = "lastfm-description" >
Mit einem persönlichen Token können eigene Apps auf deine diora-Daten zugreifen (Bücher,
Lesefortschritt, Notizen, Podcasts, Radio) — ohne dein Passwort zu teilen. Token als
< code > Authorization: Bearer < token> < / code > -Header mitschicken, z. B. gegen
< code > GET /api/sync/< / code > für einen kompletten Datenabzug.
< / p >
{% if api_token %}
< p style = "margin-top:0.75rem;" >
< code style = "user-select:all; word-break:break-all; background:#1a1a1a; padding:4px 8px; border-radius:4px; display:inline-block;" > {{ api_token.token }}< / code >
< / p >
< form method = "post" action = "{% url 'regenerate_api_token' %}" class = "inline-form" style = "margin-top:0.75rem;"
onsubmit="return confirm('Neues Token generieren? Bereits verbundene Apps können sich dann nicht mehr anmelden.');">
{% csrf_token %}
< button type = "submit" class = "btn btn-danger" > Token neu generieren< / button >
< / form >
{% else %}
< form method = "post" action = "{% url 'regenerate_api_token' %}" class = "inline-form" style = "margin-top:0.75rem;" >
{% csrf_token %}
< button type = "submit" class = "btn" > Token generieren< / button >
< / form >
{% endif %}
< / section >
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
<!-- Cloud connections (WebDAV / Nextcloud) for the ebook import -->
< section class = "settings-section" >
< h2 > Cloud-Verbindungen für Bücher< / h2 >
< p class = "lastfm-description" >
Verbinde einen WebDAV-Server — Nextcloud, ownCloud, Synology oder was auch immer WebDAV
spricht — und importiere < code > .epub< / code > - und < code > .pdf< / code > -Dateien direkt daraus in
deine Bibliothek. Die Datei läuft dabei nur durch den Server hindurch; verschlüsselt wird sie
wie immer erst in deinem Browser, gespeichert wird ausschließlich der Geheimtext.
< / p >
< p class = "lastfm-description" >
Lege dafür bitte ein < strong > App-Passwort< / strong > an (bei Nextcloud unter
Einstellungen → Sicherheit) statt dein Konto-Passwort einzutragen — es wird serverseitig
gespeichert und lässt sich jederzeit einzeln widerrufen.
< / p >
{% if webdav_sources %}
< ul class = "webdav-list" >
{% for source in webdav_sources %}
< li class = "webdav-item" >
< div class = "webdav-item-info" >
2026-10-24 18:12:39 +02:00
< strong > {{ source.label }}< / strong > {% if source.inbox %} < span class = "muted" > · Eingang< / span > {% endif %}
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
< span class = "muted" > {{ source.base_url }}{% if source.root_path %} · /{{ source.root_path }}{% endif %}< / span >
< / div >
< div class = "webdav-item-actions" >
2026-10-08 19:22:04 +02:00
< button type = "button" class = "btn" aria-expanded = "false"
onclick="const f = document.getElementById('webdav-edit-{{ source.pk }}'); f.hidden = !f.hidden; this.setAttribute('aria-expanded', String(!f.hidden));">Bearbeiten< / button >
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
< form method = "post" action = "{% url 'webdav_test' source.pk %}" class = "inline-form" >
{% csrf_token %}
< button type = "submit" class = "btn" > Testen< / button >
< / form >
< form method = "post" action = "{% url 'webdav_delete' source.pk %}" class = "inline-form"
onsubmit="return confirm('Verbindung „{{ source.label|escapejs }}“ entfernen?');">
{% csrf_token %}
< button type = "submit" class = "btn btn-danger" > Entfernen< / button >
< / form >
< / div >
2026-10-08 19:22:04 +02:00
< form method = "post" action = "{% url 'webdav_edit' source.pk %}" class = "webdav-form webdav-edit"
id="webdav-edit-{{ source.pk }}" hidden>
{% csrf_token %}
< label class = "webdav-field" >
< span > Name< / span >
< input type = "text" name = "label" required value = "{{ source.label }}" >
< / label >
< label class = "webdav-field" >
< span > Server-URL< / span >
< input type = "url" name = "base_url" required value = "{{ source.base_url }}" >
< / label >
< label class = "webdav-field" >
< span > Benutzername< / span >
< input type = "text" name = "username" autocomplete = "off" value = "{{ source.username }}" >
< / label >
< label class = "webdav-field" >
< span > App-Passwort < span class = "muted" > (leer lassen = unverändert)< / span > < / span >
< input type = "password" name = "password" autocomplete = "new-password" >
< / label >
< label class = "webdav-field" >
< span > Unterordner < span class = "muted" > (optional, Import startet dort)< / span > < / span >
< input type = "text" name = "root_path" value = "{{ source.root_path }}" placeholder = "Buecher" >
< / label >
2026-10-24 18:12:39 +02:00
< label class = "webdav-field webdav-check" >
< input type = "checkbox" name = "inbox" value = "1" { % if source . inbox % } checked { % endif % } >
< span > Eingang: neue Bücher hier kommen von selbst in die Bibliothek
< span class = "muted" > (beim Öffnen der Bücher; der Ordner bleibt unverändert)< / span > < / span >
< / label >
2026-10-08 19:22:04 +02:00
< button type = "submit" class = "btn" > Speichern< / button >
< / form >
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
< / li >
{% endfor %}
< / ul >
{% endif %}
< form method = "post" action = "{% url 'webdav_add' %}" class = "webdav-form" >
{% csrf_token %}
< label class = "webdav-field" >
< span > Name< / span >
< input type = "text" name = "label" required placeholder = "Meine Nextcloud" >
< / label >
< label class = "webdav-field" >
< span > Server-URL< / span >
< input type = "url" name = "base_url" required placeholder = "https://cloud.example.com" >
< / label >
< label class = "webdav-field" >
< span > Benutzername< / span >
< input type = "text" name = "username" autocomplete = "off" placeholder = "dein-login" >
< / label >
< label class = "webdav-field" >
< span > App-Passwort< / span >
< input type = "password" name = "password" autocomplete = "new-password" >
< / label >
< label class = "webdav-field" >
< span > Unterordner < span class = "muted" > (optional)< / span > < / span >
< input type = "text" name = "root_path" placeholder = "Buecher" >
< / label >
2026-10-24 18:12:39 +02:00
< label class = "webdav-field webdav-check" >
< input type = "checkbox" name = "inbox" value = "1" >
< span > Eingang: neue Bücher hier kommen von selbst in die Bibliothek< / span >
< / label >
Bücher: Cloud-Import aus WebDAV/Nextcloud (SW v41)
Verbindungen werden pro Nutzer in den Einstellungen angelegt (Nextcloud,
ownCloud, Synology oder generisches WebDAV) — der Server ist bewusst nicht
auf eine feste Instanz verdrahtet. Im Bücher-Tab lässt sich der entfernte
Ordner durchblättern und eine .epub/.pdf direkt in die Bibliothek ziehen.
Der Download läuft über den Server, weil der WebDAV-Host cross-origin ist
und keine CORS-Header schickt. Verschlüsselt wird trotzdem erst im Browser:
uploadEbook ist in _importEbookBuffer aufgeteilt, das sich lokaler Upload
und Cloud-Import teilen. Gespeichert wird wie bisher nur Geheimtext.
Weil jeder registrierte Nutzer die Ziel-URL bestimmt und diora im Docker-Netz
neben anderen Diensten läuft, ist der Import eine SSRF-Fläche. Dagegen:
- assert_safe_url weist Hosts ab, die auf nicht-öffentliche Adressen
auflösen (inkl. NAT64 und IPv4-kompatibler v6-Adressen, die is_global
durchlässt)
- _assert_peer_is_safe prüft die tatsächliche Peer-Adresse nach dem
Verbinden — requests löst den Namen ein zweites Mal auf, sonst wäre der
Guard per DNS-Rebinding umgehbar
- Redirects werden abgelehnt statt verfolgt
- identische Fehlermeldung für "nicht auflösbar" und "privat", ohne die
IP zu nennen, damit der Endpunkt kein Scanner für interne Dienste wird
Antwort-Bodies laufen durch _read_capped, und DTDs werden vor dem Parsen
abgewiesen: ElementTree expandiert interne Entities, und seit Python 3.12
gibt es XMLParser.parser nicht mehr, um einen Handler zu setzen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:26:22 +00:00
< p class = "lastfm-description" style = "margin:0;" >
Bei Nextcloud/ownCloud genügt die Server-Adresse — der WebDAV-Pfad wird aus dem
Benutzernamen ergänzt. Andere Server brauchen die vollständige WebDAV-URL.
< / p >
< button type = "submit" class = "btn" > Verbindung hinzufügen< / button >
< / form >
< / section >
2026-03-16 19:19:22 +01:00
< / div >
{% endblock %}
{% block extra_js %}
< style >
.bg-preview { max-width: 240px; max-height: 135px; object-fit: cover; border-radius: 4px; border: 1px solid #333; }
< / style >
< script >
function getCsrfToken() {
return document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1] || '';
}
2026-03-19 13:39:59 +01:00
// Minimal crypto helpers for settings page (duplicated from app.js — settings page doesn't load app.js)
function _bytesToBase64(buf) {
const bytes = new Uint8Array(buf);
let str = '';
for (const b of bytes) str += String.fromCharCode(b);
return btoa(str);
}
function _bytesToHex(buf) {
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
}
function _base64ToBytes(b64) {
const str = atob(b64);
const buf = new Uint8Array(str.length);
for (let i = 0; i < str.length ; i + + ) buf [ i ] = str . charCodeAt ( i ) ;
return buf;
}
async function _getOrCreateEncKey() {
const userId = {{ request.user.id }};
const storageKey = `diora_enc_key_${userId}`;
const stored = localStorage.getItem(storageKey);
if (stored) {
try {
const raw = _base64ToBytes(stored);
return crypto.subtle.importKey('raw', raw, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt']);
} catch (e) {}
}
const key = await crypto.subtle.generateKey({name: 'AES-GCM', length: 256}, true, ['encrypt', 'decrypt']);
const raw = await crypto.subtle.exportKey('raw', key);
localStorage.setItem(storageKey, _bytesToBase64(raw));
return key;
}
2026-05-25 17:03:59 +02:00
document.getElementById('pw-change-form')?.addEventListener('submit', function(e) {
const ok = confirm('Nach dem Ändern des Passworts können alle hochgeladenen Bücher nicht mehr geöffnet werden und müssen erneut hochgeladen werden. Fortfahren?');
if (!ok) e.preventDefault();
2026-05-25 16:37:41 +02:00
});
2026-03-16 19:19:22 +01:00
async function uploadBackground(input) {
const file = input.files[0];
if (!file) return;
const status = document.getElementById('bg-upload-status');
2026-03-19 13:39:59 +01:00
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status.textContent = 'Nur JPEG-, PNG- oder WebP-Bilder sind erlaubt.';
2026-03-19 13:39:59 +01:00
return;
}
if (file.size > 5 * 1024 * 1024) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status.textContent = 'Das Bild darf höchstens 5 MB groß sein.';
2026-03-19 13:39:59 +01:00
return;
}
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status.textContent = 'Verschlüssele …';
2026-03-16 19:19:22 +01:00
try {
2026-03-19 13:39:59 +01:00
const key = await _getOrCreateEncKey();
const buf = await file.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, buf);
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status.textContent = 'Lade hoch …';
2026-03-19 13:39:59 +01:00
const res = await fetch('/accounts/background/upload/', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'X-CSRFToken': getCsrfToken()},
body: JSON.stringify({iv: _bytesToHex(iv), ciphertext: _bytesToBase64(ct), mime_type: file.type, file_size: file.size}),
});
2026-03-16 19:19:22 +01:00
const data = await res.json();
if (data.ok) { location.reload(); }
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
else { status.textContent = data.error === 'only jpeg, png, or webp allowed' ? 'Nur JPEG, PNG oder WebP erlaubt.'
: data.error === 'file too large (max 5 MB)' ? 'Datei zu groß (höchstens 5 MB).' : (data.error || 'Hochladen fehlgeschlagen'); }
2026-03-19 13:39:59 +01:00
} catch (e) {
Oberfläche eingedeutscht: Radio, Bücher/Reader, Fokus, Konto, Anmeldung; sw v71
- app.js, Templates, Stream-Fenster, Manifest: alle sichtbaren Texte deutsch
(Eingabefenster "Abbrechen", Markierungs-Export, Lesezeichen, Reader-Einstellungen …)
- LANGUAGE_CODE 'de': Formular-, Passwort- und Validierungsmeldungen von Django deutsch
- Empfehlungen: "Was du unter der Woche abends oft hörst:"
- Fehlercodes der API bleiben englisch (Vertrag für Clients und Tests); was die
Seite davon zeigt, übersetzt _germanError()
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 13:53:01 +02:00
status.textContent = 'Hochladen fehlgeschlagen: ' + e.message;
2026-03-19 13:39:59 +01:00
}
2026-03-16 19:19:22 +01:00
input.value = '';
}
< / script >
{% endblock %}