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:
@@ -340,6 +340,46 @@ def get_client_by_slug(slug):
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def get_client_by_ics_token(token):
|
||||
"""Resolve a client for the per-client ICS feed (#22) -- like
|
||||
get_client_by_slug, this is the one lookup that goes straight from
|
||||
untrusted request input (the ?token= query param) to a client_id, so the
|
||||
feed can be scoped to exactly one tenant without a separate client_id
|
||||
param that could be swapped independently of the token."""
|
||||
if not token:
|
||||
return None
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT * FROM clients WHERE ics_token = %s", (token,))
|
||||
return cur.fetchone()
|
||||
|
||||
|
||||
def ensure_ics_token(client_id):
|
||||
"""Return the client's ics_token, generating and persisting one on first
|
||||
use (#22). Lazy rather than a one-off backfill migration, so clients
|
||||
onboarded before this ticket still get a working subscribe URL the first
|
||||
time their settings page loads. The UPDATE ... WHERE ics_token IS NULL
|
||||
guard means a losing concurrent call re-reads the winner's token instead
|
||||
of overwriting it -- same single-connection read-then-write shape as
|
||||
consume_password_reset_token."""
|
||||
with db.connect() as conn, conn.cursor() as cur:
|
||||
cur.execute("SELECT ics_token FROM clients WHERE client_id = %s", (client_id,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
if row["ics_token"]:
|
||||
return row["ics_token"]
|
||||
cur.execute(
|
||||
"UPDATE clients SET ics_token = %s WHERE client_id = %s AND ics_token IS NULL "
|
||||
"RETURNING ics_token",
|
||||
(secrets.token_urlsafe(24), client_id))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
cur.execute("SELECT ics_token FROM clients WHERE client_id = %s", (client_id,))
|
||||
row = cur.fetchone()
|
||||
conn.commit()
|
||||
return row["ics_token"]
|
||||
|
||||
|
||||
_CLIENT_UPDATABLE = {"auto_confirm", "notify_channel"}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user