From fe4e64a0adef365c91324901e97e7b96952b4451 Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 30 Jun 2026 12:04:20 +0200 Subject: [PATCH] =?UTF-8?q?docs(quick-260630-gbh):=20User=20Settings:=20Pa?= =?UTF-8?q?sswort=20=C3=A4ndern=20(nur=20non-LDAP)=20+=20Profilbild=20setz?= =?UTF-8?q?en?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .claude/settings.json | 3 +- .mcp.json | 8 ++ .planning/STATE.md | 16 ++- .../260630-gbh-SUMMARY.md | 116 ++++++++++++++++++ 4 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 .mcp.json create mode 100644 .planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/260630-gbh-SUMMARY.md diff --git a/.claude/settings.json b/.claude/settings.json index 3626577..6fe6354 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,5 +2,6 @@ "enabledPlugins": { "claude-mem@thedotmack": true, "claude-code-setup@claude-plugins-official": true - } + }, + "enabledMcpjsonServers": ["playwright"] } diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..259a959 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": ["@playwright/mcp@latest"] + } + } +} diff --git a/.planning/STATE.md b/.planning/STATE.md index bd7aa2c..d4439e3 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,8 +3,8 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: executing -stopped_at: context exhaustion at 76% (2026-06-28) -last_updated: "2026-06-28T21:05:27.533Z" +stopped_at: context exhaustion at 77% (2026-06-30) +last_updated: "2026-06-30T06:40:23.631Z" last_activity: 2026-06-27 -- Phase 07 execution started progress: total_phases: 7 @@ -28,7 +28,7 @@ See: .planning/PROJECT.md (updated 2026-06-18) Phase: 07 (dkv-fleet-module) — EXECUTING Plan: 2 of 6 Status: Ready to execute -Last activity: 2026-06-27 -- Phase 07 execution started +Last activity: 2026-06-30 - Completed quick task 260630-gbh: User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen Progress: [█████████░] 93% @@ -128,6 +128,12 @@ None yet. None yet. +### Quick Tasks Completed + +| # | Description | Date | Commit | Directory | +|---|-------------|------|--------|-----------| +| 260630-gbh | User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen | 2026-06-30 | merge | [260630-gbh-user-settings-passwort-ndern-nur-non-lda](.planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/) | + ## Deferred Items Items acknowledged and carried forward from previous milestone close: @@ -138,6 +144,6 @@ Items acknowledged and carried forward from previous milestone close: ## Session Continuity -Last session: 2026-06-28T21:05:27.525Z -Stopped at: context exhaustion at 76% (2026-06-28) +Last session: 2026-06-30T06:40:23.623Z +Stopped at: context exhaustion at 77% (2026-06-30) Resume file: .planning/phases/07-dkv-fleet-module/07-06-PLAN.md diff --git a/.planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/260630-gbh-SUMMARY.md b/.planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/260630-gbh-SUMMARY.md new file mode 100644 index 0000000..005178d --- /dev/null +++ b/.planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/260630-gbh-SUMMARY.md @@ -0,0 +1,116 @@ +--- +phase: quick-260630-gbh +plan: "01" +subsystem: user-settings +tags: [avatar, password-change, user-settings, auth, api] +status: complete +dependency_graph: + requires: [] + provides: [avatar-api, enriched-auth-me, account-settings-page, header-avatar] + affects: [auth-controller, user-controller, header, settings-sidebar] +tech_stack: + added: [] + patterns: [FileInterceptor-memory-storage, self-scoped-routes-no-roles, same-origin-img-proxy] +key_files: + created: + - apps/api/prisma/migrations/20260630095533_add_user_avatar/migration.sql + - apps/web/src/components/settings/account-settings-form.tsx + - apps/web/src/app/(portal)/settings/general/account/page.tsx + modified: + - apps/api/prisma/schema.prisma + - apps/api/src/user/user.controller.ts + - apps/api/src/auth/auth.service.ts + - apps/api/src/auth/auth.controller.ts + - apps/web/src/lib/auth-actions.ts + - apps/web/src/components/settings/settings-sidebar.tsx + - apps/web/src/lib/stores/auth-store.ts + - apps/web/src/components/layout/header.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json +decisions: + - "Avatar routes added to UserController (not AuthController) — RolesGuard returns true when no @Roles metadata, confirmed from guard source" + - "Used Prisma v6.19.3 (installed version), not v7 specified in CLAUDE.md — package.json pins ^6.0.0" + - "Plain tag in header (not next/image) — /api-proxy is same-origin rewrite, no remote config needed" + - "Web tsc --noEmit has pre-existing failures across all files (missing JSX/module type declarations); not introduced by this plan" +metrics: + duration: "~15 minutes" + completed: "2026-06-30" + tasks_completed: 3 + tasks_total: 3 + files_modified: 10 + files_created: 3 +--- + +# Phase quick-260630-gbh Plan 01: User Settings — Avatar + Password Change Summary + +**One-liner:** Profile avatar upload/serve with per-user file storage and conditional password-change form gated on local-vs-LDAP user status. + +## Tasks Completed + +| Task | Name | Commit | Files | +|------|------|--------|-------| +| 1 | Avatar persistence + API endpoints + enrich /auth/me | 0fba45d | schema.prisma, migration, user.controller.ts, auth.service.ts, auth.controller.ts | +| 2 | Settings Konto page — conditional password form + avatar upload | 4482a8c | auth-actions.ts, account-settings-form.tsx, settings-sidebar.tsx, account/page.tsx, de.json, en.json | +| 3 | Header avatar display with initial fallback | 5ae498f | auth-store.ts, header.tsx | + +## What Was Built + +**Backend:** +- `User.avatarPath String?` column added via Prisma migration `20260630095533_add_user_avatar` +- `POST /users/me/avatar`: FileInterceptor with 2 MB limit; image/png, image/jpeg, image/webp allowlist (T-gbh-01); writes `user-files/avatars/{userId}.{ext}` derived from MIME type (T-gbh-04); removes stale files with other extensions; userId from `@CurrentUser()` only (T-gbh-02); returns `{ success: true }` +- `GET /users/me/avatar`: looks up `avatarPath`, streams buffer with correct Content-Type and `Cache-Control: no-store` +- `AuthService.getMe(userId)`: loads user, returns public fields + `isLocalUser` (passwordHash set && ldapDn null) + `hasAvatar` (avatarPath not null); never serialises passwordHash or ldapDn (T-gbh-03) +- `GET /auth/me`: now calls `authService.getMe(user.id)` instead of returning raw JWT payload + +**Frontend:** +- `AuthUser` interface (both auth-actions.ts and auth-store.ts): added `isLocalUser?` and `hasAvatar?` +- `uploadAvatarAction`: server action POSTing multipart to `/users/me/avatar` with session cookie; returns `{ success, error? }` without redirect +- `AccountSettingsForm` (client component): avatar preview with initial fallback + file upload; password form gated on `isLocalUser === true`; LDAP managed notice when false +- `/settings/general/account` page: renders `AccountSettingsForm` +- `SettingsSidebar`: Konto link added above SMTP link +- i18n: `categoryAccount` + `account.*` keys in both de.json and en.json + +**Header:** +- Avatar button: shows `` when `hasAvatar=true` with `onError` fallback to initial letter + +## Deviations from Plan + +### Pre-existing condition (no action required) + +**[Pre-existing] Web tsc --noEmit fails across all source files** +- All JSX files fail with `TS7026: JSX element implicitly has type 'any'` and module resolution errors (`next/link`, `next-intl`, `zustand`, etc.) +- This affects the entire web package, not just files in this plan +- Out of scope per deviation rule scope boundary; logged here for awareness + +### Auto-decisions + +**[Rule 2 - Correctness] Auth.me enrichment placed in AuthService not inline** +- Kept DB access logic in AuthService (consistent with existing pattern) rather than inlining in controller + +**HEAD mismatch at startup** +- Expected base `04b37f54` but HEAD was `dcba4b9` (3 DKV commits landed on main after plan dispatch) +- Proceeded: worktree branch is `worktree-agent-a51974fb00fe6b744` (correct), DKV work is orthogonal to this plan's scope + +## Threat Surface Scan + +All T-gbh-01 through T-gbh-05 mitigations from the plan's threat model are implemented: +- T-gbh-01: 2 MB FileInterceptor limit + MIME allowlist in user.controller.ts +- T-gbh-02: `@CurrentUser()` only for userId — never from request body/params +- T-gbh-03: `getMe()` strips passwordHash/ldapDn/avatarPath before return +- T-gbh-04: filename = `{userId}.{ext}` from fixed MIME map, no user-controlled path component +- T-gbh-05: GET /users/me/avatar serves only the authenticated user's own file + +No new threat surface was introduced beyond what the plan's threat model already covers. + +## Self-Check: PASSED + +| Check | Result | +|-------|--------| +| migration.sql exists | FOUND | +| user.controller.ts | FOUND | +| auth.service.ts | FOUND | +| account-settings-form.tsx | FOUND | +| account/page.tsx | FOUND | +| commit 0fba45d (Task 1) | FOUND | +| commit 4482a8c (Task 2) | FOUND | +| commit 5ae498f (Task 3) | FOUND |