fix(260607-l6l): scope GET /api/events to current user + shared calendars

BUG 3: GET / had no ownership predicate — it returned all users' events.
Second household member would see other member's private events.

- Resolve currentUserId at top of GET handler (same resolveUserId helper
  as write endpoints); return 401 if unauthenticated.
- Add ownership predicate to WHERE: AND (calendars.userId = currentUserId
  OR calendars.isShared = true). Combined with and() around the existing
  date-window or() block. Mirrors the /writable-calendars idiom (D-03).
This commit is contained in:
Lucas Berger
2026-06-07 15:28:54 -04:00
parent 23c8bb3402
commit 00a0454514
+11
View File
@@ -126,6 +126,10 @@ const syncStatusQuerySchema = z.object({
// Response shape: { occurrences: CalendarOccurrence[] }
// ---------------------------------------------------------------------------
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
// Resolve the current user first — only return events for owned + shared calendars (T-03-06).
const currentUserId = await resolveUserId(c)
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
const { start, end } = c.req.valid('query')
// --- Window span guard (T-02b-02) ---
@@ -163,6 +167,12 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.innerJoin(users, eq(calendars.userId, users.id))
.where(
and(
// Ownership predicate (BUG 3 fix): restrict to calendars owned by the current user
// OR shared-family calendars (isShared=true). Mirrors the /writable-calendars idiom
// (~line 509) so both endpoints agree on the authoritative writable set (D-03).
or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)),
// Date-window pre-filter (RESEARCH.md §Open Questions 3 / Pitfall 5):
or(
// Recurring masters: may have occurrences inside the window even if dtstartUtc is old.
// Two sub-cases:
@@ -190,6 +200,7 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
sql`${calendarEvents.dtstartDate} < ${end}`,
),
),
),
)
// --- Expand each row into concrete occurrences ---