mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +00:00
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:
1 parent
796c06ae66
commit
6c3b2bf6bf
14 files changed
+1354
-3
No files matched your search
@@ -239,6 +239,43 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/calendar/feed": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get: operations["getCalendarFeedInfo"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/calendar/feed/rotate": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Invalidate a feed URL and issue a fresh token */
|
||||
post: operations["rotateCalendarFeedToken"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/catalog/blocked-days": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -719,6 +756,26 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/public/calendar/{token}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
get: operations["getCalendarFeed"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/v1/public/contact": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -1003,6 +1060,16 @@ export interface components {
|
||||
primaryColor?: string;
|
||||
siteUrl?: string;
|
||||
};
|
||||
CalendarFeedEntry: {
|
||||
/** @description API-relative path ending in .ics; prefix with the API base URL */
|
||||
feedPath?: string;
|
||||
/** @description Feed secret — embed in the subscription URL */
|
||||
token?: string;
|
||||
};
|
||||
CalendarFeedInfo: {
|
||||
staff?: components["schemas"]["StaffCalendarFeedEntry"][];
|
||||
tenant?: components["schemas"]["CalendarFeedEntry"];
|
||||
};
|
||||
/** @description Stripe checkout launch payload — redirect the user to checkoutUrl. */
|
||||
CheckoutLaunchResponse: {
|
||||
/**
|
||||
@@ -1332,6 +1399,13 @@ export interface components {
|
||||
status: string;
|
||||
waitlistPosition?: number;
|
||||
};
|
||||
StaffCalendarFeedEntry: {
|
||||
displayName?: string;
|
||||
feedPath?: string;
|
||||
/** Format: uuid */
|
||||
staffId?: string;
|
||||
token?: string;
|
||||
};
|
||||
StaffMember: {
|
||||
active: boolean;
|
||||
/** Format: date-time */
|
||||
@@ -2000,6 +2074,79 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
getCalendarFeedInfo: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Current feed tokens */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CalendarFeedInfo"];
|
||||
};
|
||||
};
|
||||
/** @description Tenant membership not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
rotateCalendarFeedToken: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @enum {string} */
|
||||
scope: "tenant" | "staff";
|
||||
/**
|
||||
* Format: uuid
|
||||
* @description Required when scope is staff; must belong to the tenant
|
||||
*/
|
||||
staffId?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Newly issued token and feed path */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CalendarFeedEntry"];
|
||||
};
|
||||
};
|
||||
/** @description Invalid scope or foreign staff member */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Tenant membership not found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
listBlockedDays: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -3319,6 +3466,35 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
getCalendarFeed: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
token: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description iCalendar document */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"text/calendar": string;
|
||||
};
|
||||
};
|
||||
/** @description Unknown or rotated feed token */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
submitContactForm: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
Reference in new issue
Block a user