feat(caldav): username/password (Basic Auth) access with discovery + https URLs

- dav_router: add Basic-Auth principal-discovery tree at /caldav/ (and
  /.well-known/caldav) so clients can add a CalDAV account with server URL +
  username + password; lists all published calendars. Token URL /dav/{token}/
  still works without login. Handlers generalised over a base href.
- dav_util: derive the public origin from X-Forwarded-Proto/-Host (or
  PUBLIC_BASE_URL) so published URLs are https, not internal http:8080.
- local_router: expose caldav_login_url alongside caldav_url.
- frontend/i18n: show both the no-login token URL and the login URL + hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-07-01 12:44:16 +02:00
parent 34e701fa78
commit fb32f0424f
6 changed files with 258 additions and 90 deletions

View File

@@ -9,6 +9,7 @@ token revokes existing subscriptions.
from __future__ import annotations
import os
import secrets
import uuid
@@ -35,7 +36,28 @@ def bump_dav(cal, event=None) -> None:
event.etag = new_tag()
def public_base(request) -> str:
"""Public origin (scheme://host) as clients actually reach us.
Behind a reverse proxy (e.g. Nginx Proxy Manager) the app only sees
``http://…:8080`` internally, so honour ``X-Forwarded-Proto/-Host`` and an
optional ``PUBLIC_BASE_URL`` override so published URLs are the real https
ones.
"""
env = os.environ.get("PUBLIC_BASE_URL")
if env:
return env.rstrip("/")
h = request.headers
proto = (h.get("x-forwarded-proto") or request.url.scheme or "http").split(",")[0].strip()
host = (h.get("x-forwarded-host") or h.get("host") or request.url.netloc).split(",")[0].strip()
return f"{proto}://{host}"
def caldav_url(request, token: str) -> str:
"""Absolute CalDAV collection URL for a token, based on the request origin."""
base = str(request.base_url).rstrip("/")
return f"{base}/dav/{token}/"
"""Absolute per-calendar CalDAV collection URL (secret token, no login)."""
return f"{public_base(request)}/dav/{token}/"
def caldav_login_url(request) -> str:
"""Absolute discovery URL for username/password (Basic Auth) CalDAV access."""
return f"{public_base(request)}/caldav/"