docs(05): create dashboard & calendar phase plan
4 plans across 4 waves covering DASH-01..07 + CAL-01..03. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
---
|
||||
phase: 05-dashboard-calendar
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["05-01", "05-02"]
|
||||
files_modified:
|
||||
- apps/api/prisma/schema.prisma
|
||||
- apps/api/src/app.module.ts
|
||||
- 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/caldav.provider.ts
|
||||
- apps/api/src/calendar/providers/ics.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
|
||||
- apps/api/package.json
|
||||
- apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx
|
||||
- apps/web/src/components/settings/calendar-source-form.tsx
|
||||
- apps/web/src/components/settings/calendar-settings-panel.tsx
|
||||
- apps/web/src/components/dashboard/widgets/calendar-widget.tsx
|
||||
- apps/web/src/components/dashboard/widget-registry.ts
|
||||
- apps/web/src/lib/calendar-api.ts
|
||||
- apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx
|
||||
- apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx
|
||||
autonomous: true
|
||||
user_setup:
|
||||
- service: calendar-encryption
|
||||
why: "AES-256-GCM key required to encrypt calendar credentials at rest"
|
||||
env_vars:
|
||||
- name: CALENDAR_ENCRYPTION_KEY
|
||||
source: "Generate a 32-byte hex key: openssl rand -hex 32 — add to apps/api .env and docker-compose api environment"
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can configure CalDAV, Exchange, and ICS calendar sources in Settings > Dashboard > Kalender"
|
||||
- "User can toggle which calendar sources are visible in the widget"
|
||||
- "Calendar widget shows upcoming events aggregated from selected sources"
|
||||
- "Calendar credentials are encrypted at rest and never returned in GET responses"
|
||||
artifacts:
|
||||
- path: "apps/api/prisma/schema.prisma"
|
||||
provides: "CalendarSource model"
|
||||
contains: "model CalendarSource"
|
||||
- path: "apps/api/src/calendar/calendar.service.ts"
|
||||
provides: "Event aggregation across provider types"
|
||||
exports: ["CalendarService"]
|
||||
- path: "apps/api/src/calendar/providers/ics.provider.ts"
|
||||
provides: "ICS event fetch + parse"
|
||||
- path: "apps/web/src/components/dashboard/widgets/calendar-widget.tsx"
|
||||
provides: "Upcoming-events list widget"
|
||||
- path: "apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx"
|
||||
provides: "Calendar source management UI"
|
||||
key_links:
|
||||
- from: "apps/web/src/components/dashboard/widgets/calendar-widget.tsx"
|
||||
to: "/api/calendar/events"
|
||||
via: "fetch in effect"
|
||||
pattern: "calendar/events"
|
||||
- from: "apps/api/src/calendar/calendar.service.ts"
|
||||
to: "prisma.calendarSource"
|
||||
via: "user-scoped query with credential decryption"
|
||||
pattern: "prisma\\.calendarSource"
|
||||
- from: "apps/api/src/calendar/calendar.service.ts"
|
||||
to: "ICSProvider/CalDAVProvider/ExchangeProvider"
|
||||
via: "provider dispatch by source.type"
|
||||
pattern: "type === 'ics'"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deliver the calendar vertical slice end-to-end: a user configures one or more calendar sources (CalDAV via tsdav, ICS via node-ical, Exchange via ews/Graph) in Settings > Dashboard > Kalender, toggles which sources are visible, and the Calendar widget on the dashboard shows upcoming events aggregated and normalized across all visible sources. Credentials are encrypted at rest (AES-256-GCM) and never leave the backend.
|
||||
|
||||
Purpose: Implements CAL-01 (source integration), CAL-02 (visibility selection), CAL-03 (event previews for selected sources), and DASH-05 (calendar widget). This is the heaviest slice — calendar protocols carry the highest hidden complexity per RESEARCH.
|
||||
|
||||
Output: Working calendar widget backed by a multi-protocol aggregation service and a source-management settings page.
|
||||
</objective>
|
||||
|
||||
<artifacts_this_phase_produces>
|
||||
Symbols created by THIS plan (exclude from drift verification — they are new):
|
||||
|
||||
**Prisma models:** `CalendarSource`
|
||||
**NestJS:** `CalendarModule`, `CalendarController`, `CalendarService`, `CalendarCryptoService`, `CalDAVProvider`, `ICSProvider`, `ExchangeProvider`, `CreateCalendarSourceDto`, `UpdateCalendarSourceDto`, `CalendarEventsQueryDto`
|
||||
**Interfaces:** `CalendarEvent`, `CalendarProvider`
|
||||
**API endpoints:** `GET /api/calendar/sources`, `POST /api/calendar/sources`, `PATCH /api/calendar/sources/:id`, `DELETE /api/calendar/sources/:id`, `POST /api/calendar/sources/:id/test`, `GET /api/calendar/events`
|
||||
**Frontend components:** `CalendarWidget`, `CalendarSettingsPanel`, `CalendarSourceForm`
|
||||
**Frontend modules:** `calendar-api.ts` (fetchSources/addSource/updateSource/deleteSource/testSource/fetchEvents); registry wiring of real `CalendarWidget`
|
||||
**npm:** `tsdav`, `node-ical`, `ews-javascript-api`, `@microsoft/microsoft-graph-client`
|
||||
**env var:** `CALENDAR_ENCRYPTION_KEY`
|
||||
</artifacts_this_phase_produces>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||||
@$HOME/.claude/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/05-dashboard-calendar/05-CONTEXT.md
|
||||
@.planning/phases/05-dashboard-calendar/05-RESEARCH.md
|
||||
@.planning/phases/05-dashboard-calendar/05-PATTERNS.md
|
||||
@.planning/phases/05-dashboard-calendar/05-UI-SPEC.md
|
||||
@.planning/phases/05-dashboard-calendar/05-01-SUMMARY.md
|
||||
@.planning/phases/05-dashboard-calendar/05-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Calendar backend — model, crypto, source CRUD module</name>
|
||||
<files>apps/api/prisma/schema.prisma, apps/api/src/app.module.ts, 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/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, apps/api/package.json</files>
|
||||
<read_first>
|
||||
- apps/api/prisma/schema.prisma (CalendarSource model follows existing conventions; see CalendarSource example in RESEARCH lines 562-581)
|
||||
- apps/api/src/module-registry/module-registry.module.ts (module pattern)
|
||||
- apps/api/src/module-registry/module-registry.controller.ts (userId/tenant extraction)
|
||||
- apps/api/src/module-registry/module-registry.service.ts (PrismaService injection + CRUD)
|
||||
- apps/api/src/domaincheck/dto/check-domain.dto.ts (DTO pattern)
|
||||
- apps/api/src/app.module.ts (register CalendarModule)
|
||||
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 364-412 (anti-patterns: never fetch calendar in browser, encrypt credentials, never return passwords), lines 673-681 (encryption recommendation), lines 743-761 (Security Domain: SSRF, credential exposure)
|
||||
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 161-166 (source list + form fields)
|
||||
</read_first>
|
||||
<action>
|
||||
Install calendar libs: `cd apps/api && pnpm add tsdav@2.2.2 node-ical@0.26.1 ews-javascript-api@0.15.3 @microsoft/microsoft-graph-client@3.0.7` (all Approved in RESEARCH Legitimacy Audit — no checkpoint).
|
||||
|
||||
Add Prisma model `CalendarSource`: `id String @id @default(uuid())`, `userId String`, `tenantId String`, `name String`, `type String` ('caldav'|'ics'|'exchange'), `exchangeMode String?` ('ews'|'graph' — only for exchange, per RESEARCH open question 2), `url String`, `username String?`, `encryptedPassword String?` (AES-256-GCM ciphertext), `color String? @default("#3B82F6")`, `isVisible Boolean @default(true)`, `syncIntervalMin Int @default(15)`, `lastSyncAt DateTime?`, `lastSyncError String?`, `createdAt DateTime @default(now())`, `updatedAt DateTime @updatedAt`, `@@index([userId])`, `@@index([tenantId])`. Source config is per-user (D-09), not per-tenant.
|
||||
|
||||
Create `crypto.service.ts` (`CalendarCryptoService`, injectable): AES-256-GCM using `CALENDAR_ENCRYPTION_KEY` (32-byte hex) from ConfigService. `encrypt(plaintext)` returns `iv:authTag:ciphertext` (hex-joined); `decrypt(stored)` reverses it. Throw a clear error at startup if the key is missing/wrong length.
|
||||
|
||||
Create CalendarModule (controllers: [CalendarController], providers: [CalendarService, CalendarCryptoService, CalDAVProvider, ICSProvider, ExchangeProvider]); register in app.module.ts imports. Providers added in Task 2 — declare them in providers array now (stub provider files created in Task 2, but module references them; create minimal stubs here if needed so the module compiles, or sequence so Task 2 fills bodies).
|
||||
|
||||
CalendarController `@Controller('calendar')`: `@Get('sources')` (returns sources WITHOUT password fields — use Prisma `select` excluding encryptedPassword, Pitfall 3), `@Post('sources')`, `@Patch('sources/:id')`, `@Delete('sources/:id')`, `@Post('sources/:id/test')` (testConnection), `@Get('events')` (aggregated events — implemented in Task 3). Every handler extracts userId + tenantId; all source mutations verify userId ownership.
|
||||
|
||||
CalendarService (this task: source CRUD only): inject PrismaService + CalendarCryptoService. `getSources(userId)` → findMany with `select` that NEVER includes encryptedPassword (return a boolean `hasCredentials` instead). `addSource(userId, tenantId, dto)` → encrypt dto.password into encryptedPassword before create. `updateSource(id, userId, dto)` → ownership check, re-encrypt if password provided. `deleteSource(id, userId)` → ownership check + delete.
|
||||
|
||||
DTOs: CreateCalendarSourceDto — `@IsString() @IsNotEmpty() name`, `@IsIn(['caldav','ics','exchange']) type`, `@IsUrl({ protocols: ['https'], require_protocol: true }) url` (https-only — SSRF mitigation, Security Domain), `@IsOptional() @IsString() username`, `@IsOptional() @IsString() password`, `@IsOptional() @IsIn(['ews','graph']) exchangeMode`, `@IsOptional() @IsHexColor() color`. UpdateCalendarSourceDto — all optional incl. `@IsOptional() @IsBoolean() isVisible` (CAL-02 toggle). CalendarEventsQueryDto — `@IsOptional() @IsDateString() from`, `@IsOptional() @IsDateString() to`.
|
||||
|
||||
SSRF guard: in addSource/test, reject URLs resolving to private IP ranges (10.x, 192.168.x, 127.x, 169.254.x) in addition to https-only scheme.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && npx prisma validate && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/api/package.json contains tsdav, node-ical, ews-javascript-api, @microsoft/microsoft-graph-client
|
||||
- schema.prisma contains `model CalendarSource` with `encryptedPassword` and `isVisible`
|
||||
- calendar.controller.ts GET /sources uses Prisma `select` that excludes encryptedPassword (grep: no `encryptedPassword: true` in the sources select)
|
||||
- crypto.service.ts uses `aes-256-gcm`
|
||||
- create-calendar-source.dto.ts restricts url to https (`protocols: ['https']`)
|
||||
- app.module.ts imports CalendarModule
|
||||
- `npx prisma validate` and `npx tsc --noEmit` exit 0
|
||||
</acceptance_criteria>
|
||||
<done>Calendar source CRUD with encrypted credentials, https-only URLs, no password leakage in GET responses.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Calendar providers (CalDAV, ICS, Exchange)</name>
|
||||
<files>apps/api/src/calendar/providers/caldav.provider.ts, apps/api/src/calendar/providers/ics.provider.ts, apps/api/src/calendar/providers/exchange.provider.ts</files>
|
||||
<read_first>
|
||||
- apps/api/src/calendar/calendar.service.ts (from Task 1 — providers are injected here; match the CalendarProvider interface)
|
||||
- apps/api/src/calendar/crypto.service.ts (decrypt credentials before use)
|
||||
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 287-310 (CalendarEvent + CalendarProvider interface), lines 96-101 (library purposes), Assumptions A2/A3/A6 (date-range filter, exchange OAuth, recurring expansion)
|
||||
- .planning/phases/05-dashboard-calendar/05-PATTERNS.md lines 353-361 (no-analog — use RESEARCH library patterns)
|
||||
</read_first>
|
||||
<action>
|
||||
Define a shared `CalendarProvider` interface (in calendar.service.ts or a types file): `fetchEvents(source, from, to): Promise<CalendarEvent[]>` and `testConnection(source): Promise<boolean>`. `CalendarEvent { id, sourceId, title, start: Date, end: Date, allDay: boolean, location?, description? }`.
|
||||
|
||||
`ics.provider.ts` (ICSProvider, injectable) — the simplest, implement fully: HTTPS GET the .ics URL (timeout ~8s, abort on timeout — Pitfall 4), parse with node-ical `async.fromURL` or `sync.parseICS`. Filter VEVENTs to the [from,to] window. Expand recurring events via node-ical RRULE support (Assumption A6 — if node-ical's expansion is insufficient, note the ical.js fallback but do not block). Map to CalendarEvent. testConnection: fetch + parse, return true if at least the request succeeds.
|
||||
|
||||
`caldav.provider.ts` (CalDAVProvider, injectable): use tsdav `createDAVClient` with Basic auth (username + decrypted password). Login, fetch calendars, `fetchCalendarObjects` with the time-range filter (Assumption A2 — pass start/end so only the window is fetched). Parse returned iCal data with node-ical, map to CalendarEvent. testConnection: attempt login + list calendars.
|
||||
|
||||
`exchange.provider.ts` (ExchangeProvider, injectable): dispatch on `source.exchangeMode`. For 'graph' use @microsoft/microsoft-graph-client to query `/me/calendarView` with start/end (Exchange Online, RESEARCH State of the Art prefers Graph). For 'ews' use ews-javascript-api FindAppointments over a CalendarView (on-premise). Map to CalendarEvent. Wrap both in try/catch; on auth failure surface a generic error (no credential details — Security V7). If Exchange integration cannot be fully realized for a given mode, return an empty array and set lastSyncError rather than throwing (graceful degradation; D-08 requires the source TYPE to be configurable and attempted — the widget must not crash).
|
||||
|
||||
All providers: never log decrypted credentials; respect per-request timeouts.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- ics.provider.ts imports node-ical and filters events by date window
|
||||
- caldav.provider.ts imports tsdav and passes a time-range to fetchCalendarObjects
|
||||
- exchange.provider.ts branches on exchangeMode ('graph' vs 'ews')
|
||||
- no provider logs decrypted passwords (grep: no `console.log` of password/credential vars)
|
||||
- all three implement fetchEvents + testConnection returning the CalendarEvent shape
|
||||
- `npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Three providers fetch + normalize events; Exchange degrades gracefully; no credential leakage.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Event aggregation + caching + calendar widget</name>
|
||||
<files>apps/api/src/calendar/calendar.service.ts, apps/api/src/calendar/calendar.controller.ts, apps/web/src/components/dashboard/widgets/calendar-widget.tsx, apps/web/src/components/dashboard/widget-registry.ts, apps/web/src/lib/calendar-api.ts, apps/web/src/components/dashboard/widgets/calendar-widget.test.tsx</files>
|
||||
<read_first>
|
||||
- apps/api/src/calendar/calendar.service.ts (from Task 1/2 — add aggregateEvents)
|
||||
- apps/api/src/calendar/providers/*.provider.ts (from Task 2 — dispatch by type)
|
||||
- apps/web/src/components/dashboard/widget-registry.ts (replace calendar placeholder with real CalendarWidget)
|
||||
- apps/web/src/components/dashboard/widgets/clock-widget.tsx (WidgetProps pattern)
|
||||
- .planning/phases/05-dashboard-calendar/05-RESEARCH.md lines 408-413 (caching with TTL Pitfall 4)
|
||||
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 110, 206-209 (calendar widget spec: time/title/source color dot; empty-state copy)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test (calendar-widget.test.tsx): given a mocked `/api/calendar/events` response with two events, the widget renders both titles, their times, and a source color dot per event; given an empty response renders t('widgets.calendarEmptyNoEvents'); given a no-sources response renders t('widgets.calendarEmptyNoSources')
|
||||
</behavior>
|
||||
<action>
|
||||
Backend — add `aggregateEvents(userId, from, to)` to CalendarService: load the user's `isVisible: true` sources (CAL-02/CAL-03), dispatch each to its provider by `source.type` (ics/caldav/exchange), decrypt credentials per source, fetch in parallel (`Promise.allSettled` so one failing source doesn't break others — set lastSyncError on failures), merge + sort by start ascending, return normalized CalendarEvent[] including each event's source color. Cache results per user in an in-memory Map with a 5-minute TTL (Pitfall 4) — serve cached immediately, refresh in background. Implement `GET /calendar/events` in the controller using CalendarEventsQueryDto (default window: now → now+30 days). Implement `POST /calendar/sources/:id/test` → provider.testConnection, update lastSyncAt/lastSyncError.
|
||||
|
||||
Frontend — `calendar-api.ts`: fetchSources, addSource, updateSource (incl. isVisible toggle), deleteSource, testSource, fetchEvents — all `credentials:'include'`.
|
||||
|
||||
`calendar-widget.tsx` (DASH-05, D-10 read-only): `'use client'`. On mount fetch /api/calendar/events. Render an upcoming-events list: each row shows time (locale-formatted), title, and an 8px source color dot (UI-SPEC color palette). Three states per UI-SPEC copy: no sources configured → t('widgets.calendarEmptyNoSources'); sources but no events → t('widgets.calendarEmptyNoEvents'); events → list. Loading state while fetching. Refresh periodically (e.g. every 5 min) to match backend cache. Never fetch external calendars directly from the browser (RESEARCH anti-pattern — always via /api/calendar/events).
|
||||
|
||||
Update `widget-registry.ts`: replace the calendar placeholder component with the real CalendarWidget. Keep WIDGET_CONSTRAINTS.
|
||||
|
||||
Write calendar-widget.test.tsx per <behavior> with mocked fetch.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx && cd apps/api && npx tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- calendar.service.ts aggregateEvents uses `Promise.allSettled` and filters `isVisible`
|
||||
- calendar.service.ts caches events with a TTL (grep: a Map + timestamp/expiry check)
|
||||
- calendar.controller.ts contains `@Get('events')` and `@Post('sources/:id/test')`
|
||||
- calendar-widget.tsx fetches `/api/calendar/events` and renders three distinct empty/list states
|
||||
- widget-registry.ts references the real `CalendarWidget`
|
||||
- calendar-widget.test.tsx exits 0; apps/api `tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Calendar widget shows aggregated upcoming events from visible sources, cached, read-only, with correct empty states.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 4: Calendar settings page (source management + visibility)</name>
|
||||
<files>apps/web/src/app/(portal)/settings/dashboard/calendar/page.tsx, apps/web/src/components/settings/calendar-settings-panel.tsx, apps/web/src/components/settings/calendar-source-form.tsx, apps/web/src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx</files>
|
||||
<read_first>
|
||||
- apps/web/src/app/(portal)/settings/layout.tsx (from 05-01 — nested settings layout)
|
||||
- apps/web/src/components/settings/settings-sidebar.tsx (from 05-01 — this page is the "Kalender" sub-item target)
|
||||
- apps/web/src/lib/calendar-api.ts (from 05-03 Task 3)
|
||||
- .planning/phases/05-dashboard-calendar/05-UI-SPEC.md lines 120, 161-166, 204-209, 216-220, 256-261 (calendar settings panel spec, source form fields, connection test, delete confirmation, copy)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- Test (calendar-settings.test.tsx): rendering CalendarSettingsPanel with two mocked sources lists both with name + type badge + visibility toggle; toggling a source calls updateSource with the new isVisible; the add form requires name+type+url before enabling submit
|
||||
</behavior>
|
||||
<action>
|
||||
Create `settings/dashboard/calendar/page.tsx` (D-11): `'use client'`, renders CalendarSettingsPanel. Title t('settings.categoryCalendar').
|
||||
|
||||
Create `calendar-settings-panel.tsx`: fetch sources (calendar-api.fetchSources). Render a source list — each row: color dot, name, type badge (CalDAV/Exchange/ICS), visibility toggle switch (CAL-02 — calls updateSource({isVisible})), connection-status indicator (green check / orange warning from lastSyncError per UI-SPEC), edit + delete actions. Delete uses a confirmation dialog (UI-SPEC destructive: heading + body + "Quelle loeschen"/"Abbrechen"). "Quelle hinzufuegen" button reveals CalendarSourceForm. Empty state: t('settings.calendarEmpty') (UI-SPEC copy).
|
||||
|
||||
Create `calendar-source-form.tsx`: fields Name (required), Type (select CalDAV/Exchange/ICS, required), when Exchange show an Exchange-mode select (Exchange Online=graph / Exchange Server=ews, RESEARCH open question 2), URL (required, https validation client-side), Username (optional, hidden for ICS), Password (password input, optional, hidden for ICS), Color (from the 8-color palette in UI-SPEC). On save call addSource (or updateSource when editing); then auto-run testSource and show connection-success/error toast (UI-SPEC copy). Validate URL is https before submit.
|
||||
|
||||
Write calendar-settings.test.tsx per <behavior> with mocked calendar-api.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/web && pnpm vitest run "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx" && pnpm exec tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- settings/dashboard/calendar/page.tsx renders CalendarSettingsPanel
|
||||
- calendar-settings-panel.tsx has a visibility toggle calling updateSource with isVisible
|
||||
- calendar-source-form.tsx shows an Exchange-mode select only when type is exchange, and hides username/password for ICS
|
||||
- calendar-source-form.tsx validates https before submit
|
||||
- delete uses a confirmation dialog (contains the "Quelle loeschen"/"Delete source" CTA)
|
||||
- calendar-settings.test.tsx exits 0; `pnpm exec tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Users manage CalDAV/Exchange/ICS sources, toggle widget visibility, test connections, and delete with confirmation.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 5: [BLOCKING] Prisma schema push + encryption key check</name>
|
||||
<files>apps/api/prisma/schema.prisma</files>
|
||||
<read_first>
|
||||
- apps/api/prisma/schema.prisma (CalendarSource model from Task 1 must exist)
|
||||
- apps/api/src/calendar/crypto.service.ts (CALENDAR_ENCRYPTION_KEY consumer)
|
||||
</read_first>
|
||||
<action>
|
||||
Ensure `CALENDAR_ENCRYPTION_KEY` (32-byte hex, generate via `openssl rand -hex 32`) is present in apps/api `.env` and the docker-compose api service environment so the crypto service can start. Then push the schema to the running PostgreSQL container: `npx prisma db push` from apps/api, then `npx prisma generate`. MANDATORY — type checks pass without the push (false-positive). Only the new CalendarSource table is added; if data loss is reported, STOP and flag for manual review rather than passing `--accept-data-loss`.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd apps/api && test -n "$(grep -s CALENDAR_ENCRYPTION_KEY .env)" && npx prisma db push --skip-generate && npx prisma generate</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- apps/api/.env contains CALENDAR_ENCRYPTION_KEY
|
||||
- `npx prisma db push` exits 0 and reports in sync on a second run
|
||||
- live DB contains the CalendarSource table
|
||||
- `npx prisma generate` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Live schema includes CalendarSource; encryption key configured; client regenerated.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Browser → Calendar API | User submits source config incl. credentials |
|
||||
| API → External calendar servers | Backend fetches events from user-supplied URLs |
|
||||
| API → PostgreSQL | Encrypted credentials stored at rest |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-05-09 | Information Disclosure | GET /calendar/sources | mitigate | Prisma `select` excludes encryptedPassword; return `hasCredentials` boolean only (Pitfall 3) |
|
||||
| T-05-10 | Information Disclosure | CalendarSource at rest | mitigate | AES-256-GCM encryption via CalendarCryptoService + CALENDAR_ENCRYPTION_KEY |
|
||||
| T-05-11 | Tampering / SSRF | source URL fetch (ics/caldav) | mitigate | DTO https-only (`protocols:['https']`) + block private IP ranges (10/192.168/127/169.254) + request timeouts |
|
||||
| T-05-12 | Elevation of Privilege | source mutations + /events | mitigate | All endpoints scoped to userId from JWT; ownership check before update/delete; events per-user not per-tenant (D-09) |
|
||||
| T-05-13 | Information Disclosure | connection error responses | mitigate | Generic error messages; no credential details in errors (ASVS V7) |
|
||||
| T-05-SC | Tampering | npm installs (tsdav/node-ical/ews/graph) | mitigate | All Approved in RESEARCH Legitimacy Audit; no [ASSUMED]/[SUS] → no blocking checkpoint |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `cd apps/api && npx prisma validate` exits 0
|
||||
- `cd apps/api && npx tsc --noEmit` exits 0
|
||||
- `cd apps/web && pnpm exec tsc --noEmit` exits 0
|
||||
- `cd apps/web && pnpm vitest run src/components/dashboard/widgets/calendar-widget.test.tsx "src/app/(portal)/settings/dashboard/calendar/calendar-settings.test.tsx"` green
|
||||
- `npx prisma db push` reports in sync; CALENDAR_ENCRYPTION_KEY present
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- User configures CalDAV, ICS, and Exchange sources in Settings > Dashboard > Kalender
|
||||
- User toggles source visibility; only visible sources feed the widget (CAL-02/CAL-03)
|
||||
- Calendar widget shows aggregated upcoming events with source color dots, read-only
|
||||
- Credentials encrypted at rest, never returned in GET, https-only URLs enforced
|
||||
- One failing source does not break the others (Promise.allSettled)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/05-dashboard-calendar/05-03-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user