Per-client ICS calendar feed, replacing the shared ICS_TOKEN (#22)
Test backoffice (smb-crm) / test (push) Has been cancelled
Test backoffice (smb-crm) / test (push) Has been cancelled
Each client now gets their own clients.ics_token (lazily generated on first /owner/settings visit), which both authenticates and scopes /api/bookings.ics -- closing the gap where any shared-token holder could view another client's bookings by swapping the client_id query param. The owner settings page now surfaces a copyable subscribe URL. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+11
-12
@@ -38,9 +38,6 @@ app.register_blueprint(owner_settings_bp)
|
||||
app.secret_key = os.environ.get("SESSION_SECRET_KEY", "")
|
||||
|
||||
CRM_TOKEN = os.environ.get("CRM_API_TOKEN", "")
|
||||
# Separate read-only token for the public iCal feed (calendar apps can't send
|
||||
# the basic-auth header, so the feed is gated by this query token instead).
|
||||
ICS_TOKEN = os.environ.get("ICS_TOKEN", "")
|
||||
|
||||
# Static dashboard, with the CRM token injected so the (basic-auth-gated)
|
||||
# operator page can call the token-protected mutation endpoints.
|
||||
@@ -256,21 +253,23 @@ def _ics_esc(t):
|
||||
|
||||
@app.get("/api/bookings.ics")
|
||||
def bookings_ics():
|
||||
"""Read-only iCal feed for Apple/Google Calendar subscription. Gated by the
|
||||
ICS_TOKEN query param (no header auth, so calendar apps can fetch it)."""
|
||||
if not ICS_TOKEN or request.args.get("token") != ICS_TOKEN:
|
||||
"""Read-only iCal feed for Apple/Google Calendar subscription, scoped to
|
||||
one client via their own clients.ics_token (#22). No header auth, since
|
||||
calendar apps can't send one -- gated by the query token instead, but
|
||||
unlike the old shared ICS_TOKEN + client_id pair, the token itself
|
||||
resolves the client, so there's no separate client_id param that could
|
||||
be swapped to view another tenant's bookings."""
|
||||
client = bdb.get_client_by_ics_token(request.args.get("token"))
|
||||
if client is None:
|
||||
return Response("forbidden\n", status=403, mimetype="text/plain")
|
||||
cid = request.args.get("client_id")
|
||||
cid = client["client_id"]
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
if cid:
|
||||
cur.execute("SELECT * FROM bookings WHERE client_id = %s ORDER BY start_time", (cid,))
|
||||
else:
|
||||
cur.execute("SELECT * FROM bookings ORDER BY start_time")
|
||||
cur.execute("SELECT * FROM bookings WHERE client_id = %s ORDER BY start_time", (cid,))
|
||||
rows = cur.fetchall()
|
||||
now = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
out = ["BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//smb-crm//bookings//DE",
|
||||
"CALSCALE:GREGORIAN", "METHOD:PUBLISH",
|
||||
"X-WR-CALNAME:" + _ics_esc("Buchungen " + cid if cid else "Buchungen")]
|
||||
"X-WR-CALNAME:" + _ics_esc("Buchungen " + cid)]
|
||||
for r in rows:
|
||||
st = _ics_dt(r.get("start_time"))
|
||||
if not st:
|
||||
|
||||
Reference in New Issue
Block a user