style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125 insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc (singleQuote:true, semi:true, tabWidth:2, trailingComma:all, printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
+22
-12
@@ -62,22 +62,29 @@ Additional findings: an all-day end-date inclusivity inconsistency that compound
|
||||
|
||||
**File:** `apps/api/src/routes/events.ts:311-322` (edit) and `:411-422` (delete)
|
||||
**Issue:** Both handlers look up the event with `.where(eq(calendarEvents.uid, uid))` and destructure `const [eventRow]`. The unique key is `(calendarId, uid)` (`schema.ts:121`), and with a shared Fastmail account (D-16) the SAME uid is cached once per member's calendar — so this query returns 2+ rows and `[0]` is whichever the DB returns first (lowest id = typically the OTHER member). Consequences:
|
||||
|
||||
- The ownership check `eventRow.userId !== currentUserId` can compare against the wrong member's calendar row, then fall through to the `isShared` branch and either wrongly 403 a legitimate owner or wrongly authorize against a different calendar.
|
||||
- The enqueued outbox row carries `eventRow.calendarUrl / objectUrl / etag` from the arbitrary row, so the write can target the wrong member's object URL / etag.
|
||||
|
||||
The `GET /` handler correctly scopes by `or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true))`; the write lookups do not. This is the exact bug class schema.ts "BUG B" warns about, un-propagated to the event lookups.
|
||||
**Fix:** Scope the lookup to the current user's writable set and disambiguate deterministically:
|
||||
|
||||
```ts
|
||||
const [eventRow] = await db
|
||||
.select({ /* …same cols… */ })
|
||||
.select({
|
||||
/* …same cols… */
|
||||
})
|
||||
.from(calendarEvents)
|
||||
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||
.where(and(
|
||||
eq(calendarEvents.uid, uid),
|
||||
or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)),
|
||||
))
|
||||
.limit(1)
|
||||
.where(
|
||||
and(
|
||||
eq(calendarEvents.uid, uid),
|
||||
or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
```
|
||||
|
||||
Prefer the current user's own row over a shared/other row if both match (e.g. order so `calendars.userId = currentUserId` ranks first), so the etag/objectUrl chosen belongs to the acting member.
|
||||
|
||||
### CR-02: Outbox WR-02 "freshest etag" re-read also queries uid-only — can pick the wrong member's etag
|
||||
@@ -85,17 +92,20 @@ Prefer the current user's own row over a shared/other row if both match (e.g. or
|
||||
**File:** `apps/api/src/broker/outboxWorker.ts:205-211`
|
||||
**Issue:** Before a PUT, the worker re-reads the freshest etag with `db.select({ etag }).from(calendarEvents).where(eq(calendarEvents.uid, row.uid))` and takes `freshEtagRows[0].etag`. Same uid-collision problem as CR-01: for a shared-account uid this returns multiple rows and `[0]` may be the OTHER member's etag. Using a foreign etag in `If-Match` will either spuriously 412 (false conflict → the edit is marked `failed` with no retry, D-08, user sees the conflict toast and the edit is dropped) or, worse, match by coincidence and overwrite. The intended WR-02 behavior (avoid stale-etag 412 on rapid edits) is undermined.
|
||||
**Fix:** Scope the re-read to the row's own calendar. The outbox row knows `calendarUrl` and `userId`; join through `calendars`:
|
||||
|
||||
```ts
|
||||
const freshEtagRows = await db
|
||||
.select({ etag: calendarEvents.etag })
|
||||
.from(calendarEvents)
|
||||
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
|
||||
.where(and(
|
||||
eq(calendarEvents.uid, row.uid),
|
||||
eq(calendars.userId, row.userId),
|
||||
eq(calendars.url, row.calendarUrl),
|
||||
))
|
||||
.limit(1)
|
||||
.where(
|
||||
and(
|
||||
eq(calendarEvents.uid, row.uid),
|
||||
eq(calendars.userId, row.userId),
|
||||
eq(calendars.url, row.calendarUrl),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
```
|
||||
|
||||
### CR-03: All-day end date is exclusive on write but inclusive on edit pre-fill — span grows one day per re-edit
|
||||
|
||||
Reference in New Issue
Block a user