--- phase: 07-user-plugin-and-authentication plan: 03 subsystem: auth tags: [user, password-reset, activation, avatar, mail, console] requires: - phase: 07-user-plugin-and-authentication provides: login, register, MailTemplate, ResolveMailLocale, postcard Mailer provides: - forgot-password, reset-password, activate, activate-by-code - update, change-password, marketing-consent - avatar upload and remove through system_files - mail.activate, mail.restore, mail.reactivate (pl and -en) - user:require-password-change affects: [07-04, 07-05, 07-06] tech-stack: added: [] patterns: [constant-time code compare, two-phase avatar blob delete, postcard memory driver for mail assertions] key-files: created: - ../fonoteka.go/plugins/golem15/user/classes/codes.go - ../fonoteka.go/plugins/golem15/user/console/require_password_change.go - ../fonoteka.go/plugins/golem15/user/views/mail/activate.htm modified: - ../fonoteka.go/plugins/golem15/user/controllers/api_controller.go - ../fonoteka.go/plugins/golem15/user/plugin.go - ../fonoteka.go/plugins/golem15/user/routes.go key-decisions: - "Forgot-password always returns the same 200 body" - "Authenticated activate ignores a failed code and still returns the user" - "Avatar blobs are deleted only after the system_files transaction commits" patterns-established: - "Pattern: reset and activation links are {id}!{code} query values" - "Pattern: mail vars expose both lowercase and Go-template capital keys" requirements-completed: [] duration: 95min completed: 2026-09-22 --- # Phase 7 Plan 03: Account management Summary **Password reset, activation, profile update, avatar upload, the six user mail templates, and `user:require-password-change`.** ## Performance - **Duration:** 95 min - **Started:** 2026-09-22T13:15:00Z - **Completed:** 2026-09-22T14:50:00Z - **Tasks:** 3 - **Files modified:** 28 ## Accomplishments - Forgot-password always answers `{"message":"If that email exists, a reset link has been sent."}`. Reset consumes `{id}!{code}`, stores the new hash, and sets `tokens_valid_after` so older tokens fail. - Public activate-by-code restores a soft-deleted user and returns a token. The authenticated activate route returns the user payload even when the code does not match. - Profile update, change-password, and marketing consent write the signed-in row directly. A password change keeps the presenting token and invalidates older ones. - Avatar upload sniffs the first bytes, stores an `attach.File` with morph `Golem15\User\Models\User`, and fills `has_avatar` plus a 128px `avatar_url`. - Polish and English mail templates render through the postcard memory driver. `user:require-password-change` sets `must_change_password`. ## Task Commits 1. **Task 1: Password reset and activation** — `9b80aa7` in `fonoteka.go` 2. **Task 2: Profile, change-password, marketing consent** — `aba832a` in `fonoteka.go` 3. **Task 3: Avatar, mail templates, require-password-change** — `5c6a735` in `fonoteka.go` ## Files Created/Modified - `plugins/golem15/user/classes/codes.go` — issue and verify reset and activation codes - `plugins/golem15/user/controllers/api_controller.go` — account, profile, and avatar handlers - `plugins/golem15/user/views/mail/` — activate, restore, reactivate, and the user layout - `plugins/golem15/user/console/require_password_change.go` — console command - `plugins/golem15/user/plugin.go` — `HasMailTemplates` and `HasCommands` ## Decisions Made NULL `reset_password_code_issued_at` is treated as `time.Now()` at check time, so a cutover code does not expire through the TTL until it is consumed. `has_self_set_password` is set true before `Create` on register. GORM would otherwise insert the zero value and override the column default, and change-password would then fail closed. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] `required` rejects JSON false** - **Found during:** Task 2 - **Issue:** go-playground's `required` treats `false` as empty, so marketing consent `false` returned 422. - **Fix:** The handler checks that the key is present and that the value is a bool. - **Files modified:** `controllers/api_controller.go` - **Committed in:** `aba832a` **2. [Rule 2 - Missing] User-mode register did not issue an activation code** - **Found during:** Task 3 - **Issue:** The activation mail had to carry `link` and `code`. The user-mode branch only named the template. - **Fix:** `IssueActivationCode` runs before the send. Vars include `name`/`Name`, `link`/`Link`, and `code`/`Code` so the Go templates render and the test can read `Vars["link"]`. - **Files modified:** `controllers/api_controller.go` - **Committed in:** `5c6a735` --- **Total deviations:** 2 auto-fixed **Impact on plan:** Consent false is a successful update. Activation mail now contains the code the public activate route consumes. ## Issues Encountered None ## User Setup Required None - no external service configuration required. ## Next Phase Readiness Ready for 07-04 (personal API tokens and me/locale). AUTH-01 stays open until the phase requirement is signed off. AUTH-02's payload seam is unchanged. ## Self-Check: PASSED - Handlers for reset, activation, profile, avatar, and the console command are on `fonoteka.go` master (`9b80aa7`, `aba832a`, `5c6a735`). - `go test ./plugins/golem15/user/...` passed, including `TestUploadAvatar`, `TestRemoveAvatar`, `TestMail`, and `TestRequirePasswordChange`.