feat(calendar): read-only ICS feeds for tenant and staff

GET /v1/public/calendar/{token} serves an RFC 5545 calendar under a
token-as-secret URL (private Google/Outlook link model). Staff feeds
contain only that member's bookings; the tenant feed tags each event
with the staff member. Window is -30d to +180d; cancelled bookings map
to STATUS:CANCELLED, waitlisted/pending to TENTATIVE + TRANSPARENT.
Summaries resolve service or class-session titles, descriptions carry
localized contact labels, locations resolve to names.

Feed tokens live on tenants/staff (migration 00015) and are managed via
GET /v1/calendar/feed plus POST /v1/calendar/feed/rotate — rotating a
scope invalidates the old URL. ICS rendering is hand-rolled: TEXT
escaping, CRLF, 75-octet folding on rune boundaries.

Settings gains a "Kalendář (ICS)" card with copyable URLs and two-step
rotation per scope; demo mode points at the real seeded tokens.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 15:20:22 +02:00
1 parent 796c06ae66
commit 6c3b2bf6bf
14 files changed
+1354 -3

No files matched your search

+110
View File
@@ -16,6 +16,7 @@ tags:
- name: Tenant
- name: Catalog
- name: Billing
- name: Calendar
- name: SMS
- name: Jobs
paths:
@@ -1258,6 +1259,85 @@ paths:
$ref: "#/components/schemas/SessionRoster"
"404":
description: Class session not found
# ---------------- Calendar ----------------
/v1/calendar/feed:
get:
tags: [Calendar]
operationId: getCalendarFeedInfo
security:
- bearerAuth: []
summary: Feed tokens for the tenant and every staff member
description: >-
Returns the read-only ICS feed token for the whole tenant plus one per
staff member. Tokens are secrets — anyone holding a feed URL can read
that calendar, same model as private Google/Outlook calendar links.
responses:
"200":
description: Current feed tokens
content:
application/json:
schema:
$ref: "#/components/schemas/CalendarFeedInfo"
"404":
description: Tenant membership not found
/v1/calendar/feed/rotate:
post:
tags: [Calendar]
operationId: rotateCalendarFeedToken
security:
- bearerAuth: []
summary: Invalidate a feed URL and issue a fresh token
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [scope]
properties:
scope:
type: string
enum: [tenant, staff]
staffId:
type: string
format: uuid
description: Required when scope is staff; must belong to the tenant
responses:
"200":
description: Newly issued token and feed path
content:
application/json:
schema:
$ref: "#/components/schemas/CalendarFeedEntry"
"400":
description: Invalid scope or foreign staff member
"404":
description: Tenant membership not found
/v1/public/calendar/{token}:
get:
tags: [Calendar]
operationId: getCalendarFeed
summary: Read-only ICS calendar feed
description: >-
Token-as-secret calendar subscription endpoint. `{token}` may carry a
`.ics` suffix, which is ignored. The feed covers bookings from 30 days
back to 180 days ahead; cancelled bookings appear with
STATUS:CANCELLED and waitlisted entries as TENTATIVE/TRANSPARENT.
parameters:
- in: path
name: token
required: true
schema:
type: string
responses:
"200":
description: iCalendar document
content:
text/calendar:
schema:
type: string
"404":
description: Unknown or rotated feed token
# ---------------- Billing ----------------
/v1/billing/subscription:
get:
@@ -1822,6 +1902,36 @@ components:
waitlistPosition:
type: integer
description: 1-based queue spot for waitlisted class bookings; absent otherwise
CalendarFeedEntry:
type: object
properties:
token:
type: string
description: Feed secret — embed in the subscription URL
feedPath:
type: string
description: API-relative path ending in .ics; prefix with the API base URL
StaffCalendarFeedEntry:
type: object
properties:
staffId:
type: string
format: uuid
displayName:
type: string
token:
type: string
feedPath:
type: string
CalendarFeedInfo:
type: object
properties:
tenant:
$ref: "#/components/schemas/CalendarFeedEntry"
staff:
type: array
items:
$ref: "#/components/schemas/StaffCalendarFeedEntry"
DashboardBookingRequest:
type: object
required: [customerName, customerEmail, startsAt]