- 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>
64 lines
2.1 KiB
Python
64 lines
2.1 KiB
Python
"""Shared helpers for CalDAV publishing of local calendars.
|
|
|
|
Publishing is opt-in per calendar: a published calendar gets a secret
|
|
``dav_token`` and is reachable as a two-way CalDAV collection at
|
|
``/dav/{token}/``. ``dav_ctag`` changes on every event write so clients detect
|
|
changes; each event carries an ``etag`` that changes on write. Rotating the
|
|
token revokes existing subscriptions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import secrets
|
|
import uuid
|
|
|
|
|
|
def new_token() -> str:
|
|
"""A URL-safe, unguessable token used as the CalDAV collection path."""
|
|
return secrets.token_urlsafe(24)
|
|
|
|
|
|
def new_tag() -> str:
|
|
"""A fresh ctag/etag value."""
|
|
return uuid.uuid4().hex
|
|
|
|
|
|
def bump_dav(cal, event=None) -> None:
|
|
"""Mark a calendar (and optionally an event) as changed for CalDAV clients.
|
|
|
|
Safe to call unconditionally on every local-event write — it only refreshes
|
|
opaque change tags, so unpublished calendars are unaffected.
|
|
"""
|
|
if cal is not None:
|
|
cal.dav_ctag = new_tag()
|
|
if event is not 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 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/"
|