Files
schalli 694bcff4a4 docs(05-03): complete calendar backend plan
- 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
2026-06-24 15:19:27 +02:00

157 lines
7.8 KiB
Markdown

---
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=<generated-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*