- Create 05-03-SUMMARY.md with full execution documentation - Update STATE.md: advance to plan 4/5, add decisions, record metrics - Update ROADMAP.md: mark 05-03 complete, 3/5 plans done - Update REQUIREMENTS.md: CAL-01/02/03 + DASH-05 backend complete
7.8 KiB
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:
- Task 1: Calendar backend -- model, crypto, source CRUD module -
9ec6313(feat) - Task 2: Calendar providers (CalDAV, ICS, Exchange) -
389ac9b(feat) - Task 3: Event aggregation + caching backend -
0cd8efe(feat) - 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 registeredapps/api/src/calendar/calendar.controller.ts- REST endpoints for sources + eventsapps/api/src/calendar/calendar.service.ts- Source CRUD + event aggregation with cacheapps/api/src/calendar/crypto.service.ts- AES-256-GCM encrypt/decrypt serviceapps/api/src/calendar/providers/ics.provider.ts- ICS fetch + parse + RRULE expansionapps/api/src/calendar/providers/caldav.provider.ts- CalDAV via tsdav with time-range filterapps/api/src/calendar/providers/exchange.provider.ts- Exchange Graph API + EWS dispatchapps/api/src/calendar/dto/create-calendar-source.dto.ts- Create DTO with https-only validationapps/api/src/calendar/dto/update-calendar-source.dto.ts- Update DTO with isVisible toggleapps/api/src/calendar/dto/calendar-events-query.dto.ts- Events query DTO (from/to dates)apps/api/src/app.module.ts- Registered CalendarModuleapps/api/package.json- Added tsdav, node-ical, ews-javascript-api, @microsoft/microsoft-graph-clientdocker-compose.yml- Added CALENDAR_ENCRYPTION_KEY env var
Decisions Made
- Used
DAVClientclass constructor +login()instead ofcreateDAVClientfactory function because the factory returns a plain object withoutlogin()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-apiimported asanysince the library ships no TypeScript definitions- ICS provider uses native
fetch()+ical.sync.parseICS()instead ofical.async.fromURL()for better timeout control (AbortController)
Deviations from Plan
None - plan executed exactly as written.
Issues Encountered
- tsdav
createDAVClienttype mismatch: The factory function returns a utility object missing thelogin()method in its type. Resolved by using theDAVClientclass constructor directly. - ews-javascript-api no TypeScript definitions: Library is JS-only with no
.d.tsfiles. TheDateTimetype expected byCalendarViewconstructor is an EWS-internal type. Resolved by importing asanyand using JS Date objects which the runtime accepts. - node-ical
async.fromURLoverload ambiguity: Passing options to the async overload causes TypeScript to select the void-returning callback signature. Resolved by using nativefetch()+sync.parseICS()instead.
User Setup Required
CALENDAR_ENCRYPTION_KEY must be configured before the CalendarModule can start:
- Generate a 32-byte hex key:
openssl rand -hex 32 - Add to
apps/api/.env:CALENDAR_ENCRYPTION_KEY=<generated-key> - Add to Docker environment or
.envroot for production deployment - The key has been added to the development
.envanddocker-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