feat: publish local calendars via two-way CalDAV (opt-in, secret token URL)
Backend:
- LocalCalendar gains caldav_published + dav_token + dav_ctag; LocalEvent gains
etag (migrations in main.py). bump_dav() refreshes ctag/etag on every local
event write (create/update/delete/import).
- local_router: PUT /calendars/{id} accepts caldav_published (mints/revokes
token), new POST /calendars/{id}/dav-token/rotate, _cal_dict exposes
caldav_published + caldav_url.
- New dav_router mounted at root (/dav/{token}/...): a minimal two-way CalDAV
server (OPTIONS/PROPFIND/REPORT/GET/PUT/DELETE) reusing ical_io build/parse,
ctag-based change detection. Secret token = auth, no login.
Frontend:
- Settings calendar table: per-local-calendar publish toggle + subscribe URL
with copy and token-rotate; i18n (de/en) and styling.
Note: reverse proxy must allow WebDAV methods (PROPFIND/REPORT/PUT/DELETE).
VALARM/reminders are not round-tripped via CalDAV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
41
backend/dav_util.py
Normal file
41
backend/dav_util.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""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 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 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}/"
|
||||
Reference in New Issue
Block a user