feat(anhaenge): Endpunkte zum Hochladen, Abrufen und Loeschen

Neuer Router unter /api/local mit Upload, Liste, Download, Thumbnail und
Loeschen, dazu permissions.readable_local_event als fehlendes Gegenstueck
zu _writable_event.

Der Lese-Helfer prueft nicht nur den Kalender, sondern auch die
Privatsphaere: ein fremder privater Termin wird im Lesepfad maskiert, also
duerfen auch seine Anhaenge nicht ueber die UID erreichbar sein. Fremde
bekommen durchgaengig 404 statt 403, damit die Existenz nicht durchsickert.

Die Mengenbegrenzung zaehlt nach dem Insert innerhalb derselben
Transaktion -- zaehlen-dann-einfuegen waere ein Wettlauf, bei dem zwei
gleichzeitige Uploads beide 9 sehen.

Ausgeliefert wird immer als Download mit nosniff, nie inline. Dazu die
Capability-URL unter /api/attach/<token> fuer externe CalDAV-Clients,
abschaltbar ueber ATTACHMENT_PUBLIC_LINKS=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 10:08:47 +02:00
co-authored by Claude Opus 5
parent 22a469d449
commit 0e8a1a6a3d
4 changed files with 493 additions and 1 deletions
+22
View File
@@ -82,6 +82,28 @@ def accessible_local_calendar(
return cal
def readable_local_event(db: Session, user: models.User, uid: str) -> models.LocalEvent:
"""Return the event if the user may READ it, else raise 404.
The read-side mirror of local_router._writable_event. Beyond the calendar
check it enforces the private-event rule: the merge read hides or busy-masks
another user's private events, so anything hanging off such an event must
not be reachable by uid either — otherwise a share recipient could read the
attachments of events whose very title is deliberately withheld from them.
Always 404, never 403: a user who may not see the event may not learn that
it exists.
"""
ev = db.query(models.LocalEvent).filter(models.LocalEvent.uid == uid).first()
if not ev:
raise HTTPException(404, "Event not found")
cal = accessible_local_calendar(db, user, ev.calendar_id)
owner_id = ev.creator_id or cal.user_id
if ev.is_private and owner_id != user.id:
raise HTTPException(404, "Event not found")
return ev
def is_calendar_owner(db: Session, user: models.User, calendar_id: int) -> models.LocalCalendar:
"""Return the calendar only if the user owns it, else raise 404."""
cal = (