--- phase: 05-dashboard-calendar plan: 03 subsystem: api tags: [calendar, caldav, ics, exchange, aes-256-gcm, tsdav, node-ical, ews, microsoft-graph, prisma, nestjs] requires: - phase: 05-dashboard-calendar/01 provides: Dashboard grid + widget registry + Prisma models (DashboardLayout, WidgetInstance) - phase: 05-dashboard-calendar/02 provides: Widget settings panel pattern + SearchProvider CRUD pattern provides: - CalendarSource Prisma model with AES-256-GCM encrypted credentials - CalendarModule with source CRUD, multi-protocol providers, event aggregation - REST API endpoints for calendar source management and event fetching - CalendarCryptoService for credential encryption/decryption - ICS/CalDAV/Exchange provider implementations with graceful degradation affects: [05-dashboard-calendar/04, 05-dashboard-calendar/05] tech-stack: added: [tsdav@2.2.2, node-ical@0.26.1, ews-javascript-api@0.15.3, @microsoft/microsoft-graph-client@3.0.7] patterns: [provider-dispatch-by-type, encrypted-credentials-at-rest, ssrf-url-validation, promise-allsettled-aggregation, in-memory-ttl-cache] key-files: created: - apps/api/src/calendar/calendar.module.ts - apps/api/src/calendar/calendar.controller.ts - apps/api/src/calendar/calendar.service.ts - apps/api/src/calendar/crypto.service.ts - apps/api/src/calendar/providers/ics.provider.ts - apps/api/src/calendar/providers/caldav.provider.ts - apps/api/src/calendar/providers/exchange.provider.ts - apps/api/src/calendar/dto/create-calendar-source.dto.ts - apps/api/src/calendar/dto/update-calendar-source.dto.ts - apps/api/src/calendar/dto/calendar-events-query.dto.ts modified: - apps/api/prisma/schema.prisma - apps/api/src/app.module.ts - apps/api/package.json - docker-compose.yml key-decisions: - "DAVClient class constructor + login() instead of createDAVClient factory (type safety with tsdav v2)" - "Dynamic imports for ews-javascript-api and @microsoft/microsoft-graph-client (lazy-load, avoid unused SDK weight)" - "In-memory Map cache with 5-min TTL for events (Redis not needed at current scale; Pitfall 4)" - "ews-javascript-api imported as any due to no TypeScript definitions available" patterns-established: - "Provider dispatch pattern: getProvider(type) returns CalendarProvider implementation by source.type" - "Credential encryption: AES-256-GCM with iv:authTag:ciphertext hex format" - "SSRF validation: https-only DTO + private IP range blocking in service" - "Safe select pattern: SOURCE_SAFE_SELECT excludes encryptedPassword, derives hasCredentials boolean" requirements-completed: [CAL-01, CAL-02, CAL-03, DASH-05] duration: 11min completed: 2026-06-24 --- # Phase 05 Plan 03: Calendar Backend Summary **Multi-protocol calendar backend with CalDAV/ICS/Exchange providers, AES-256-GCM credential encryption, SSRF-protected source CRUD, and in-memory cached event aggregation** ## Performance - **Duration:** 11 min - **Started:** 2026-06-24T13:05:25Z - **Completed:** 2026-06-24T13:16:25Z - **Tasks:** 4 - **Files modified:** 14 ## Accomplishments - CalendarSource Prisma model with encrypted credentials pushed to live PostgreSQL - Full CalendarModule: source CRUD (6 endpoints), ownership checks, SSRF validation - Three calendar providers: ICS (node-ical + RRULE expansion), CalDAV (tsdav), Exchange (Graph API + EWS) - Event aggregation across visible sources with Promise.allSettled, per-user cache with 5-min TTL - Credential encryption via AES-256-GCM; passwords never returned in API responses ## Task Commits Each task was committed atomically: 1. **Task 1: Calendar backend -- model, crypto, source CRUD module** - `9ec6313` (feat) 2. **Task 2: Calendar providers (CalDAV, ICS, Exchange)** - `389ac9b` (feat) 3. **Task 3: Event aggregation + caching backend** - `0cd8efe` (feat) 4. **Task 4: Prisma schema push + encryption key check** - `bb8990c` (chore) ## Files Created/Modified - `apps/api/prisma/schema.prisma` - Added CalendarSource model (16 columns) - `apps/api/src/calendar/calendar.module.ts` - NestJS module with all providers registered - `apps/api/src/calendar/calendar.controller.ts` - REST endpoints for sources + events - `apps/api/src/calendar/calendar.service.ts` - Source CRUD + event aggregation with cache - `apps/api/src/calendar/crypto.service.ts` - AES-256-GCM encrypt/decrypt service - `apps/api/src/calendar/providers/ics.provider.ts` - ICS fetch + parse + RRULE expansion - `apps/api/src/calendar/providers/caldav.provider.ts` - CalDAV via tsdav with time-range filter - `apps/api/src/calendar/providers/exchange.provider.ts` - Exchange Graph API + EWS dispatch - `apps/api/src/calendar/dto/create-calendar-source.dto.ts` - Create DTO with https-only validation - `apps/api/src/calendar/dto/update-calendar-source.dto.ts` - Update DTO with isVisible toggle - `apps/api/src/calendar/dto/calendar-events-query.dto.ts` - Events query DTO (from/to dates) - `apps/api/src/app.module.ts` - Registered CalendarModule - `apps/api/package.json` - Added tsdav, node-ical, ews-javascript-api, @microsoft/microsoft-graph-client - `docker-compose.yml` - Added CALENDAR_ENCRYPTION_KEY env var ## Decisions Made - Used `DAVClient` class constructor + `login()` instead of `createDAVClient` factory function because the factory returns a plain object without `login()` method in tsdav v2 types - Dynamic imports for EWS and Graph SDKs to avoid loading unused SDK code at startup - In-memory Map cache with 5-minute TTL for events (Redis overkill at current scale; easy to swap later) - `ews-javascript-api` imported as `any` since the library ships no TypeScript definitions - ICS provider uses native `fetch()` + `ical.sync.parseICS()` instead of `ical.async.fromURL()` for better timeout control (AbortController) ## Deviations from Plan None - plan executed exactly as written. ## Issues Encountered - **tsdav `createDAVClient` type mismatch:** The factory function returns a utility object missing the `login()` method in its type. Resolved by using the `DAVClient` class constructor directly. - **ews-javascript-api no TypeScript definitions:** Library is JS-only with no `.d.ts` files. The `DateTime` type expected by `CalendarView` constructor is an EWS-internal type. Resolved by importing as `any` and using JS Date objects which the runtime accepts. - **node-ical `async.fromURL` overload ambiguity:** Passing options to the async overload causes TypeScript to select the void-returning callback signature. Resolved by using native `fetch()` + `sync.parseICS()` instead. ## User Setup Required **CALENDAR_ENCRYPTION_KEY** must be configured before the CalendarModule can start: 1. Generate a 32-byte hex key: `openssl rand -hex 32` 2. Add to `apps/api/.env`: `CALENDAR_ENCRYPTION_KEY=` 3. Add to Docker environment or `.env` root for production deployment 4. The key has been added to the development `.env` and `docker-compose.yml` ## Threat Mitigations Implemented | Threat ID | Mitigation | |-----------|------------| | T-05-09 | Prisma `select` excludes encryptedPassword; returns `hasCredentials` boolean | | T-05-10 | AES-256-GCM encryption via CalendarCryptoService | | T-05-11 | DTO https-only + private IP range blocking + localhost/internal blocking | | T-05-12 | All endpoints scoped to userId; ownership check before update/delete | | T-05-13 | Generic error messages; no credential details in error responses | ## Next Phase Readiness - Calendar backend API complete and ready for frontend integration (Plan 04/05) - CalendarSource table live in PostgreSQL - Event aggregation endpoint functional — frontend can fetch via `GET /calendar/events` - Source CRUD ready for Settings UI — `GET/POST/PATCH/DELETE /calendar/sources` ## Self-Check: PASSED - All 10 created files verified present - All 4 task commits verified in git log (9ec6313, 389ac9b, 0cd8efe, bb8990c) --- *Phase: 05-dashboard-calendar* *Completed: 2026-06-24*