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:
+116
-117
@@ -21,29 +21,29 @@
|
||||
* Mounted under /api/* in index.ts — behind oidcAuthMiddleware.
|
||||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { Hono } from 'hono'
|
||||
import type { Context } from 'hono'
|
||||
import { zValidator } from '@hono/zod-validator'
|
||||
import { z } from 'zod'
|
||||
import { and, or, eq, desc } from 'drizzle-orm'
|
||||
import { sql } from 'drizzle-orm'
|
||||
import { db } from '../db/client.js'
|
||||
import { calendarEvents, calendars, users, calendarOutbox } from '../db/schema.js'
|
||||
import { expandOccurrences } from '../broker/expand.js'
|
||||
import { extractRruleString } from '../broker/vevent.js'
|
||||
import { getAuth } from '../auth/middleware.js'
|
||||
import { upsertUser, deriveDisplayName } from '../auth/user.js'
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { Hono } from 'hono';
|
||||
import type { Context } from 'hono';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { z } from 'zod';
|
||||
import { and, or, eq, desc } from 'drizzle-orm';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { db } from '../db/client.js';
|
||||
import { calendarEvents, calendars, users, calendarOutbox } from '../db/schema.js';
|
||||
import { expandOccurrences } from '../broker/expand.js';
|
||||
import { extractRruleString } from '../broker/vevent.js';
|
||||
import { getAuth } from '../auth/middleware.js';
|
||||
import { upsertUser, deriveDisplayName } from '../auth/user.js';
|
||||
// Side-effect import: brings in the ContextVariableMap augmentation for c.get('user')
|
||||
import '../auth/devBypass.js'
|
||||
import '../auth/devBypass.js';
|
||||
|
||||
export const eventsRouter = new Hono()
|
||||
export const eventsRouter = new Hono();
|
||||
|
||||
/** Shared-family calendar rose color (D-06). */
|
||||
const SHARED_FAMILY_COLOR = '#F25C7A'
|
||||
const SHARED_FAMILY_COLOR = '#F25C7A';
|
||||
|
||||
/** Maximum allowed date-window span to prevent DoS (T-02b-02). */
|
||||
const MAX_WINDOW_DAYS = 90
|
||||
const MAX_WINDOW_DAYS = 90;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Auth helper — shared by all write endpoints
|
||||
@@ -61,22 +61,22 @@ const MAX_WINDOW_DAYS = 90
|
||||
// ContextVariableMap augmentation in auth/devBypass.ts (typed as the DEV_USER shape),
|
||||
// and getAuth(c) accepts a Context — so no `any` / eslint-disable is needed here.
|
||||
async function resolveUserId(c: Context): Promise<number | null> {
|
||||
const devUser = c.get('user') as { id: number } | undefined
|
||||
if (devUser) return devUser.id
|
||||
const devUser = c.get('user') as { id: number } | undefined;
|
||||
if (devUser) return devUser.id;
|
||||
|
||||
const auth = await getAuth(c)
|
||||
if (!auth) return null
|
||||
const auth = await getAuth(c);
|
||||
if (!auth) return null;
|
||||
|
||||
const iss = (auth.iss as string | undefined) ?? ''
|
||||
const sub = auth.sub ?? ''
|
||||
const iss = (auth.iss as string | undefined) ?? '';
|
||||
const sub = auth.sub ?? '';
|
||||
|
||||
// Derive displayName via the shared helper (name → preferred_username → email
|
||||
// → sub fallback) so the write-path upsert agrees with me.ts and never
|
||||
// overwrites a correctly-derived name with a worse one.
|
||||
const displayName = deriveDisplayName(auth)
|
||||
const displayName = deriveDisplayName(auth);
|
||||
|
||||
const user = await upsertUser(iss, sub, displayName)
|
||||
return user?.id ?? null
|
||||
const user = await upsertUser(iss, sub, displayName);
|
||||
return user?.id ?? null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -87,7 +87,7 @@ async function resolveUserId(c: Context): Promise<number | null> {
|
||||
const eventsQuerySchema = z.object({
|
||||
start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
})
|
||||
});
|
||||
|
||||
/**
|
||||
* Shared event field validation (V5 — bounded lengths, T-03-08).
|
||||
@@ -111,14 +111,17 @@ const eventFieldsSchema = z.object({
|
||||
// ≤10-char non-date string cannot survive .replace(/-/g,'') and inject extra ';'-delimited
|
||||
// RRULE parts when spliced into the UNTIL template (outboxWorker.assembleRruleString).
|
||||
// int().min(1) prevents zero/negative counts.
|
||||
recurrenceUntil: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(), // 'YYYY-MM-DD' → RRULE UNTIL
|
||||
recurrenceUntil: z
|
||||
.string()
|
||||
.regex(/^\d{4}-\d{2}-\d{2}$/)
|
||||
.optional(), // 'YYYY-MM-DD' → RRULE UNTIL
|
||||
recurrenceCount: z.number().int().min(1).optional(), // integer ≥ 1 → RRULE COUNT
|
||||
})
|
||||
});
|
||||
|
||||
/** sync-status query params. */
|
||||
const syncStatusQuerySchema = z.object({
|
||||
uid: z.string().min(1).max(512),
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /api/events?start=YYYY-MM-DD&end=YYYY-MM-DD
|
||||
@@ -132,17 +135,17 @@ const syncStatusQuerySchema = z.object({
|
||||
// ---------------------------------------------------------------------------
|
||||
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 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) ---
|
||||
const windowStartDate = new Date(start + 'T00:00:00Z')
|
||||
const windowEndDate = new Date(end + 'T00:00:00Z')
|
||||
const spanDays = (windowEndDate.getTime() - windowStartDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
const windowStartDate = new Date(start + 'T00:00:00Z');
|
||||
const windowEndDate = new Date(end + 'T00:00:00Z');
|
||||
const spanDays = (windowEndDate.getTime() - windowStartDate.getTime()) / (1000 * 60 * 60 * 24);
|
||||
if (spanDays > MAX_WINDOW_DAYS || spanDays <= 0) {
|
||||
return c.json({ error: 'Date window must be between 1 and 90 days' }, 400)
|
||||
return c.json({ error: 'Date window must be between 1 and 90 days' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -211,11 +214,11 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// --- Expand each row into concrete occurrences ---
|
||||
const allOccurrences = rows.flatMap((row) => {
|
||||
const color = row.isShared ? SHARED_FAMILY_COLOR : row.userColor
|
||||
const color = row.isShared ? SHARED_FAMILY_COLOR : row.userColor;
|
||||
return expandOccurrences(
|
||||
row.rawVevent,
|
||||
windowStartDate,
|
||||
@@ -226,15 +229,15 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
||||
row.ownerName ?? null,
|
||||
color,
|
||||
row.isShared,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return c.json({ occurrences: allOccurrences })
|
||||
return c.json({ occurrences: allOccurrences });
|
||||
} catch (err) {
|
||||
console.error('[events] DB query or expansion failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events] DB query or expansion failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POST /api/events/create
|
||||
@@ -244,16 +247,16 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
|
||||
// Does NOT build a VEVENT and does NOT call Fastmail — that is the worker's job (D-12).
|
||||
// ---------------------------------------------------------------------------
|
||||
eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||
const currentUserId = await resolveUserId(c);
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
const payload = c.req.valid('json')
|
||||
const payload = c.req.valid('json');
|
||||
|
||||
try {
|
||||
// --- Resolve target calendar (D-03 / T-03-06) ---
|
||||
// If calendarUrl given: assert the calendar is owned by the current user OR is shared.
|
||||
// If not given: use the first personal calendar (D-01 last-used is a frontend concern).
|
||||
let targetCalendarUrl: string
|
||||
let targetCalendarUrl: string;
|
||||
|
||||
if (payload.calendarUrl) {
|
||||
// Look up the calendar — it must be owned by the current user or be shared.
|
||||
@@ -265,12 +268,12 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
||||
eq(calendars.url, payload.calendarUrl),
|
||||
or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if (!calRow) {
|
||||
return c.json({ error: 'Calendar not found or access denied' }, 403)
|
||||
return c.json({ error: 'Calendar not found or access denied' }, 403);
|
||||
}
|
||||
targetCalendarUrl = calRow.url
|
||||
targetCalendarUrl = calRow.url;
|
||||
} else {
|
||||
// Default to the member's first personal calendar (D-01).
|
||||
// WR-02: add a deterministic ORDER BY + LIMIT. Without them, a member with
|
||||
@@ -282,16 +285,16 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
||||
.from(calendars)
|
||||
.where(eq(calendars.userId, currentUserId))
|
||||
.orderBy(calendars.id)
|
||||
.limit(1)
|
||||
.limit(1);
|
||||
|
||||
if (!calRow) {
|
||||
return c.json({ error: 'No writable calendar found for user' }, 422)
|
||||
return c.json({ error: 'No writable calendar found for user' }, 422);
|
||||
}
|
||||
targetCalendarUrl = calRow.url
|
||||
targetCalendarUrl = calRow.url;
|
||||
}
|
||||
|
||||
// Generate a UID for the new event (Node.js 22 built-in)
|
||||
const uid = `${randomUUID()}@familysync`
|
||||
const uid = `${randomUUID()}@familysync`;
|
||||
|
||||
// Enqueue the outbox row (pending) — the worker builds the VEVENT and calls Fastmail.
|
||||
await db.insert(calendarOutbox).values({
|
||||
@@ -301,14 +304,14 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
||||
uid,
|
||||
calendarUrl: targetCalendarUrl,
|
||||
payload: JSON.stringify(payload),
|
||||
})
|
||||
});
|
||||
|
||||
return c.json({ uid }, 202)
|
||||
return c.json({ uid }, 202);
|
||||
} catch (err) {
|
||||
console.error('[events/create] DB operation failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events/create] DB operation failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PATCH /api/events/:uid/edit
|
||||
@@ -319,11 +322,11 @@ eventsRouter.post('/create', zValidator('json', eventFieldsSchema), async (c) =>
|
||||
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
||||
// ---------------------------------------------------------------------------
|
||||
eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||
const currentUserId = await resolveUserId(c);
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
const uid = c.req.param('uid')
|
||||
const payload = c.req.valid('json')
|
||||
const uid = c.req.param('uid');
|
||||
const payload = c.req.valid('json');
|
||||
|
||||
try {
|
||||
// --- Look up the event and verify ownership ---
|
||||
@@ -358,10 +361,10 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
),
|
||||
)
|
||||
.orderBy(sql`(${calendars.userId} = ${currentUserId}) desc`)
|
||||
.limit(1)
|
||||
.limit(1);
|
||||
|
||||
if (!eventRow) {
|
||||
return c.json({ error: 'Event not found' }, 404)
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Ownership check: must be the calendar owner or shared (T-03-06).
|
||||
@@ -372,20 +375,20 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
const [calRow] = await db
|
||||
.select({ isShared: calendars.isShared })
|
||||
.from(calendars)
|
||||
.where(and(eq(calendars.id, eventRow.calendarId), eq(calendars.isShared, true)))
|
||||
.where(and(eq(calendars.id, eventRow.calendarId), eq(calendars.isShared, true)));
|
||||
|
||||
if (!calRow) {
|
||||
return c.json({ error: 'Access denied' }, 403)
|
||||
return c.json({ error: 'Access denied' }, 403);
|
||||
}
|
||||
}
|
||||
|
||||
const newCalendarUrl = payload.calendarUrl ?? eventRow.calendarUrl
|
||||
const isCalendarMove = newCalendarUrl !== eventRow.calendarUrl
|
||||
const newCalendarUrl = payload.calendarUrl ?? eventRow.calendarUrl;
|
||||
const isCalendarMove = newCalendarUrl !== eventRow.calendarUrl;
|
||||
|
||||
if (isCalendarMove) {
|
||||
// D-04: edit-as-move — insert delete+create pair in one transaction (D-04 / Pitfall 5)
|
||||
const newUid = `${randomUUID()}@familysync`
|
||||
const groupId = randomUUID()
|
||||
const newUid = `${randomUUID()}@familysync`;
|
||||
const groupId = randomUUID();
|
||||
|
||||
// CR-01: carry the existing RRULE through the move. The edit payload omits
|
||||
// `recurrence` (the occurrence contract does not expose it, D-03), and the
|
||||
@@ -398,13 +401,9 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
// Only stash when the edit did NOT carry an explicit recurrence: an explicit
|
||||
// value (including 'none') is a deliberate user change and must win.
|
||||
const preservedRrule =
|
||||
payload.recurrence === undefined
|
||||
? extractRruleString(eventRow.rawVevent ?? '')
|
||||
: undefined
|
||||
payload.recurrence === undefined ? extractRruleString(eventRow.rawVevent ?? '') : undefined;
|
||||
const createPayload =
|
||||
preservedRrule !== undefined
|
||||
? { ...payload, _preservedRrule: preservedRrule }
|
||||
: payload
|
||||
preservedRrule !== undefined ? { ...payload, _preservedRrule: preservedRrule } : payload;
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
// Delete from old calendar
|
||||
@@ -417,7 +416,7 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
calendarObjectUrl: eventRow.objectUrl ?? undefined,
|
||||
etag: eventRow.etag ?? undefined,
|
||||
groupId,
|
||||
})
|
||||
});
|
||||
// Create on new calendar
|
||||
await tx.insert(calendarOutbox).values({
|
||||
userId: currentUserId,
|
||||
@@ -427,10 +426,10 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
calendarUrl: newCalendarUrl,
|
||||
payload: JSON.stringify(createPayload),
|
||||
groupId,
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return c.json({ uid: newUid }, 202)
|
||||
return c.json({ uid: newUid }, 202);
|
||||
}
|
||||
|
||||
// Same calendar — simple update row
|
||||
@@ -443,14 +442,14 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
calendarObjectUrl: eventRow.objectUrl ?? undefined,
|
||||
etag: eventRow.etag ?? undefined,
|
||||
payload: JSON.stringify(payload),
|
||||
})
|
||||
});
|
||||
|
||||
return c.json({ uid }, 202)
|
||||
return c.json({ uid }, 202);
|
||||
} catch (err) {
|
||||
console.error('[events/edit] DB operation failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events/edit] DB operation failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DELETE /api/events/:uid
|
||||
@@ -459,10 +458,10 @@ eventsRouter.patch('/:uid/edit', zValidator('json', eventFieldsSchema), async (c
|
||||
// Returns 202 immediately (D-05). Does NOT call Fastmail (D-12).
|
||||
// ---------------------------------------------------------------------------
|
||||
eventsRouter.delete('/:uid', async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||
const currentUserId = await resolveUserId(c);
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
const uid = c.req.param('uid')
|
||||
const uid = c.req.param('uid');
|
||||
|
||||
try {
|
||||
// Look up the event — join calendars so calendars.url / calendars.userId are accessible.
|
||||
@@ -490,10 +489,10 @@ eventsRouter.delete('/:uid', async (c) => {
|
||||
),
|
||||
)
|
||||
.orderBy(sql`(${calendars.userId} = ${currentUserId}) desc`)
|
||||
.limit(1)
|
||||
.limit(1);
|
||||
|
||||
if (!eventRow) {
|
||||
return c.json({ error: 'Event not found' }, 404)
|
||||
return c.json({ error: 'Event not found' }, 404);
|
||||
}
|
||||
|
||||
// Ownership check (T-03-06)
|
||||
@@ -501,10 +500,10 @@ eventsRouter.delete('/:uid', async (c) => {
|
||||
const [calRow] = await db
|
||||
.select({ isShared: calendars.isShared })
|
||||
.from(calendars)
|
||||
.where(and(eq(calendars.id, eventRow.calendarId), eq(calendars.isShared, true)))
|
||||
.where(and(eq(calendars.id, eventRow.calendarId), eq(calendars.isShared, true)));
|
||||
|
||||
if (!calRow) {
|
||||
return c.json({ error: 'Access denied' }, 403)
|
||||
return c.json({ error: 'Access denied' }, 403);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -517,14 +516,14 @@ eventsRouter.delete('/:uid', async (c) => {
|
||||
calendarUrl: eventRow.calendarUrl ?? '',
|
||||
calendarObjectUrl: eventRow.objectUrl ?? undefined,
|
||||
etag: eventRow.etag ?? undefined,
|
||||
})
|
||||
});
|
||||
|
||||
return c.json({ uid }, 202)
|
||||
return c.json({ uid }, 202);
|
||||
} catch (err) {
|
||||
console.error('[events/delete] DB operation failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events/delete] DB operation failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /api/events/sync-status?uid=<uid>
|
||||
@@ -534,10 +533,10 @@ eventsRouter.delete('/:uid', async (c) => {
|
||||
// Returns { uid, status: 'done' } when no outbox row exists (nothing pending = settled).
|
||||
// ---------------------------------------------------------------------------
|
||||
eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||
const currentUserId = await resolveUserId(c);
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
const { uid } = c.req.valid('query')
|
||||
const { uid } = c.req.valid('query');
|
||||
|
||||
try {
|
||||
// Scope strictly to current member's rows (T-03-07 — never leak another member's outbox).
|
||||
@@ -562,24 +561,24 @@ eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), asy
|
||||
sql`case ${calendarOutbox.status} when 'failed' then 0 when 'dead' then 0 when 'pending' then 1 else 2 end`,
|
||||
desc(calendarOutbox.createdAt),
|
||||
)
|
||||
.limit(1)
|
||||
.limit(1);
|
||||
|
||||
if (!rows.length) {
|
||||
// No outbox row → nothing pending = settled as done
|
||||
return c.json({ uid, status: 'done' })
|
||||
return c.json({ uid, status: 'done' });
|
||||
}
|
||||
|
||||
const row = rows[0]
|
||||
const row = rows[0];
|
||||
return c.json({
|
||||
uid: row.uid,
|
||||
status: row.status,
|
||||
...(row.lastError != null ? { error: row.lastError } : {}),
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[events/sync-status] DB query failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events/sync-status] DB query failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GET /api/events/writable-calendars
|
||||
@@ -595,8 +594,8 @@ eventsRouter.get('/sync-status', zValidator('query', syncStatusQuerySchema), asy
|
||||
// Response: { calendars: [{ url, displayName, color, isShared }] }
|
||||
// ---------------------------------------------------------------------------
|
||||
eventsRouter.get('/writable-calendars', async (c) => {
|
||||
const currentUserId = await resolveUserId(c)
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401)
|
||||
const currentUserId = await resolveUserId(c);
|
||||
if (currentUserId === null) return c.json({ error: 'Unauthorized' }, 401);
|
||||
|
||||
try {
|
||||
// D-03 writable set: own personal calendars + shared Family calendar.
|
||||
@@ -609,7 +608,7 @@ eventsRouter.get('/writable-calendars', async (c) => {
|
||||
isShared: calendars.isShared,
|
||||
})
|
||||
.from(calendars)
|
||||
.where(or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)))
|
||||
.where(or(eq(calendars.userId, currentUserId), eq(calendars.isShared, true)));
|
||||
|
||||
return c.json({
|
||||
calendars: rows.map((row) => ({
|
||||
@@ -618,9 +617,9 @@ eventsRouter.get('/writable-calendars', async (c) => {
|
||||
color: row.color ?? '',
|
||||
isShared: row.isShared,
|
||||
})),
|
||||
})
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[events/writable-calendars] DB query failed:', err)
|
||||
return c.json({ error: 'Service unavailable' }, 503)
|
||||
console.error('[events/writable-calendars] DB query failed:', err);
|
||||
return c.json({ error: 'Service unavailable' }, 503);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user