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:
@@ -124,6 +124,12 @@ class LocalCalendar(Base):
|
||||
enabled = Column(Boolean, default=True)
|
||||
# Whether events of this calendar generate reminders/notifications on clients.
|
||||
reminders_enabled = Column(Boolean, default=True, nullable=False)
|
||||
# CalDAV publishing (opt-in): expose this calendar as a two-way CalDAV
|
||||
# collection reachable via a secret token URL. dav_ctag changes on every
|
||||
# event write so clients detect changes; rotating the token revokes access.
|
||||
caldav_published = Column(Boolean, default=False, nullable=False)
|
||||
dav_token = Column(String(64), nullable=True, unique=True)
|
||||
dav_ctag = Column(String(32), nullable=True)
|
||||
|
||||
user = relationship("User", back_populates="local_calendars")
|
||||
events = relationship("LocalEvent", back_populates="calendar", cascade="all, delete-orphan")
|
||||
@@ -152,6 +158,8 @@ class LocalEvent(Base):
|
||||
creator_name_external = Column(Text, nullable=True)
|
||||
# Private events are filtered for other group members per their visibility setting.
|
||||
is_private = Column(Boolean, default=False)
|
||||
# CalDAV entity tag — changes on every write so CalDAV clients detect updates.
|
||||
etag = Column(String(32), nullable=True)
|
||||
|
||||
calendar = relationship("LocalCalendar", back_populates="events")
|
||||
creator = relationship("User")
|
||||
|
||||
Reference in New Issue
Block a user