feat(caldav): app-specific passwords so MFA accounts can use CalDAV

CalDAV clients send only user+password over Basic Auth and can't provide a TOTP
code, so account passwords would bypass 2FA. Add revocable app passwords:

- models: AppPassword table (bcrypt hash, label, last_used); auto-created via
  create_all
- profile_router: GET/POST/DELETE /profile/app-passwords (plaintext shown once)
- dav_router: Basic Auth accepts any app password; the account password is
  accepted only when 2FA is disabled
- frontend: "App-Passwörter (CalDAV)" section in the profile modal (create/show-
  once/copy/revoke) + i18n (de/en); login hint now says app password

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-01 13:15:35 +02:00
parent fb32f0424f
commit f662163185
8 changed files with 213 additions and 9 deletions

View File

@@ -995,6 +995,26 @@
</div>
</div>
<!-- App passwords (CalDAV) -->
<div class="settings-section">
<h4 data-i18n="app_pw_title">App-Passwörter (CalDAV)</h4>
<p class="text-muted" data-i18n="app_pw_desc">Eigene Passwörter für CalDAV-Clients. Bei aktivem 2FA nötig, da Apps keinen 2FA-Code eingeben können. Jederzeit widerrufbar.</p>
<div class="form-group app-pw-create">
<input type="text" id="app-pw-label" data-i18n-placeholder="app_pw_label_ph" placeholder="Name (z.B. iPhone)" maxlength="100" />
<button class="btn btn-primary btn-sm" id="app-pw-create-btn" data-i18n="app_pw_create">Erstellen</button>
</div>
<div id="app-pw-new" class="app-pw-new hidden">
<label data-i18n="app_pw_new_label">Neues App-Passwort (nur jetzt sichtbar):</label>
<div class="totp-secret-row">
<code id="app-pw-new-value"></code>
<button class="btn btn-ghost btn-sm" id="app-pw-copy" title="Kopieren">
<svg viewBox="0 0 24 24" fill="currentColor" width="16" height="16"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>
</button>
</div>
</div>
<div id="app-pw-list"></div>
</div>
<!-- Calendars -->
<div class="settings-section">
<h4>Meine Kalender</h4>