feat(ical): abonnierbarer read-only Kalender-Feed pro Nutzer

Token-gescoper iCal-Feed (/absences/ical/<token>.ics), abonnierbar in
Outlook/Apple/Google. Anders als der CalDAV-Client (Push nach Nextcloud)
pollt der Kalender die URL selbst. Feed zeigt nur die eigenen bestätigten
Abwesenheiten des Token-Inhabers.

- users.ical_token_hash (SHA-256, rotierbar) + Migration 0041 (nur Spalte,
  keine RLS-Aenderung; users-Policy deckt neue nullable Spalte ab)
- Router ical.py: oeffentlicher Feed (kein JWT) + Token-Verwaltung
  POST/GET/DELETE /users/me/ical-token (authentifiziert)
- ProfilePage: Sektion "Kalender-Abo (iCal)" mit Erzeugen/Rotieren/
  Deaktivieren, URL-Anzeige einmalig + Kopieren
- test_ical.py: Token-Lifecycle + oeffentlicher Feed (3 Tests)

Deployed auf 137 (Migration 0041, 196/196 Tests gruen). 164 ausstehend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 16:12:05 +02:00
co-authored by Claude Opus 4.8
parent d27be740a0
commit d13350b38b
6 changed files with 320 additions and 0 deletions
@@ -0,0 +1,29 @@
"""iCal-Abo: users.ical_token_hash
Revision ID: 0041
Revises: 0040
Create Date: 2026-07-08
Gehashtes, rotierbares Token für den read-only iCal-Kalender-Feed pro Nutzer
(/absences/ical/<token>.ics). NULL = Feed deaktiviert. Keine RLS-Änderung
(nur neue nullable Spalte auf bestehender users-Policy).
"""
from alembic import op
revision = "0041"
down_revision = "0040"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("ALTER TABLE users ADD COLUMN IF NOT EXISTS ical_token_hash TEXT")
op.execute(
"CREATE UNIQUE INDEX IF NOT EXISTS ix_users_ical_token_hash "
"ON users (ical_token_hash) WHERE ical_token_hash IS NOT NULL"
)
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_users_ical_token_hash")
op.execute("ALTER TABLE users DROP COLUMN IF EXISTS ical_token_hash")