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:
@@ -126,6 +126,10 @@ const syncStatusQuerySchema = z.object({
|
|||||||
// Response shape: { occurrences: CalendarOccurrence[] }
|
// Response shape: { occurrences: CalendarOccurrence[] }
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
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')
|
const { start, end } = c.req.valid('query')
|
||||||
|
|
||||||
// --- Window span guard (T-02b-02) ---
|
// --- 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(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||||
.innerJoin(users, eq(calendars.userId, users.id))
|
.innerJoin(users, eq(calendars.userId, users.id))
|
||||||
.where(
|
.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(
|
or(
|
||||||
// Recurring masters: may have occurrences inside the window even if dtstartUtc is old.
|
// Recurring masters: may have occurrences inside the window even if dtstartUtc is old.
|
||||||
// Two sub-cases:
|
// Two sub-cases:
|
||||||
@@ -190,6 +200,7 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
|||||||
sql`${calendarEvents.dtstartDate} < ${end}`,
|
sql`${calendarEvents.dtstartDate} < ${end}`,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Expand each row into concrete occurrences ---
|
// --- Expand each row into concrete occurrences ---
|
||||||
|
|||||||
Reference in New Issue
Block a user