Files
tessera-ctl/.planning/quick/260630-gbh-user-settings-passwort-ndern-nur-non-lda/260630-gbh-SUMMARY.md
T
schalli fe4e64a0ad
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 42s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m42s
docs(quick-260630-gbh): User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 12:04:20 +02:00

5.8 KiB

phase, plan, subsystem, tags, status, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags status dependency_graph tech_stack key_files decisions metrics
quick-260630-gbh 01 user-settings
avatar
password-change
user-settings
auth
api
complete
requires provides affects
avatar-api
enriched-auth-me
account-settings-page
header-avatar
auth-controller
user-controller
header
settings-sidebar
added patterns
FileInterceptor-memory-storage
self-scoped-routes-no-roles
same-origin-img-proxy
created modified
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
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
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 <img> 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
duration completed tasks_completed tasks_total files_modified files_created
~15 minutes 2026-06-30 3 3 10 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 <img src="/api-proxy/users/me/avatar"> 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