---
phase: 10-core-plugins
plan: 02
type: execute
wave: 2
depends_on: ["10-01"]
files_modified:
- plugins/golem15/user/components/ResetPassword.scala
- plugins/golem15/user/services/FrontendUserService.scala
- plugins/golem15/user/services/UserMailService.scala
- plugins/golem15/user/resources/views/account/profile.peb
- plugins/golem15/user/resources/views/account/avatar.peb
- plugins/golem15/user/resources/views/resetpassword/request.peb
- plugins/golem15/user/resources/views/resetpassword/reset.peb
- plugins/golem15/user/resources/views/mail/activation.peb
- plugins/golem15/user/resources/views/mail/reset.peb
autonomous: true
must_haves:
truths:
- "Logged-in user can view and edit their profile"
- "User can change password with current password verification"
- "User can upload and change avatar"
- "Visitor can request password reset via email"
- "User can reset password using email link with token"
- "Activation email sent when activation mode is user"
artifacts:
- path: "plugins/golem15/user/components/ResetPassword.scala"
provides: "Password reset flow component"
exports: ["ResetPasswordComponent", "onRequest", "onReset"]
- path: "plugins/golem15/user/services/UserMailService.scala"
provides: "Email sending for activation and reset"
exports: ["UserMailService", "sendActivationEmail", "sendResetEmail"]
- path: "plugins/golem15/user/resources/views/account/profile.peb"
provides: "Profile edit form template"
contains: "hx-post"
key_links:
- from: "plugins/golem15/user/components/Account.scala"
to: "UserMailService"
via: "activation email on register"
pattern: "mailService\\.sendActivationEmail"
- from: "plugins/golem15/user/components/ResetPassword.scala"
to: "FrontendUserService"
via: "password reset"
pattern: "userService\\.resetPassword"
- from: "plugins/golem15/user/services/FrontendUserService.scala"
to: "MediaLibrary"
via: "avatar upload"
pattern: "mediaLibrary\\.upload"
---
Complete the User plugin with profile management and password reset functionality.
Purpose: Enable users to manage their accounts and recover access, completing the frontend user experience.
Output: Working profile and reset features:
- Profile edit form with name/surname/password change
- Avatar upload using media library
- Password reset request and completion flow
- Email templates for activation and reset
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
@/home/jin/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/10-core-plugins/10-CONTEXT.md
@.planning/phases/10-core-plugins/10-RESEARCH.md
# Prior plan in this phase
@.planning/phases/10-core-plugins/10-01-SUMMARY.md
# Reference: Phase 9 media library for avatar upload
Task 1: Profile management with avatar upload
plugins/golem15/user/components/Account.scala (modify)
plugins/golem15/user/services/FrontendUserService.scala (modify)
plugins/golem15/user/resources/views/account/profile.peb
plugins/golem15/user/resources/views/account/avatar.peb
Add profile management to Account component:
**Account.scala additions:**
- Add onUpdate HTMX handler (research Pattern 4):
- Get current user from authService, fail if not authenticated
- If requirePassword property set, validate current password
- Parse ProfileUpdate from form (name, surname, newPassword optional)
- Validate new password length if provided (min from settings)
- Call userService.updateProfile
- If password changed, re-authenticate user (new JWT)
- Return success partial with "Profile updated" message
- Add onUploadAvatar HTMX handler:
- Get current user, fail if not authenticated
- Get uploaded file from multipart form
- Validate file type (image/jpeg, image/png, image/gif, image/webp)
- Validate file size (max 2MB from settings or default)
- Call userService.updateAvatar
- Return updated avatar partial with new image
**FrontendUserService.scala additions:**
- Add updateAvatar(userId, file: UploadedFile): FrontendUser
- Use MediaLibrary from Phase 9 to store file
- Generate path: users/{userId}/avatar.{ext}
- Update user.avatarPath
- Delete old avatar if exists
- Modify updateProfile to handle password change:
- If newPassword provided, hash with Argon2id
- Update passwordHash field
- Rotate persistCode to invalidate other sessions
**profile.peb template:**
- Form with hx-post to onUpdate handler, hx-target for response
- Name input (pre-filled from user)
- Surname input (pre-filled from user)
- Current password input (if requirePassword property)
- New password input (optional)
- Confirm new password input
- Submit button
- Error/success display area
- CSRF token
**avatar.peb template:**
- Current avatar display (or placeholder)
- Form with hx-post to onUploadAvatar, hx-encoding="multipart/form-data"
- File input with accept="image/*"
- Upload button
- Preview area that updates on success
./mill summercms.compile succeeds
Account component has onUpdate and onUploadAvatar handlers
Templates have valid Pebble syntax
Users can edit profile (name, surname, password).
Password change requires current password if configured.
Avatar upload stores image via media library.
Templates render forms with proper HTMX attributes.
Task 2: Password reset flow and email service
plugins/golem15/user/components/ResetPassword.scala
plugins/golem15/user/services/UserMailService.scala
plugins/golem15/user/services/FrontendUserService.scala (modify)
plugins/golem15/user/resources/views/resetpassword/request.peb
plugins/golem15/user/resources/views/resetpassword/reset.peb
plugins/golem15/user/resources/views/mail/activation.peb
plugins/golem15/user/resources/views/mail/reset.peb
Create password reset component and email service:
**UserMailService.scala:**
- Trait with ZIO effects:
- sendActivationEmail(user: FrontendUser, code: String): IO[MailError, Unit]
- sendResetEmail(user: FrontendUser, code: String): IO[MailError, Unit]
- Live implementation using ZIO email service (or stub for now):
- Load Pebble template for email body
- Include activation/reset URL with code
- Send via configured SMTP or email service
- MailError ADT: SendFailed, TemplateError, ConfigurationError
**FrontendUserService.scala additions:**
- requestPasswordReset(email: String): Unit
- Find user by email (don't reveal if not found - timing attack)
- Generate secure random reset code (32 bytes hex)
- Set resetPasswordCode on user with expiry tracking
- Call mailService.sendResetEmail
- Always succeed (don't reveal user existence)
- resetPassword(code: String, newPassword: String): FrontendUser
- Find user by resetPasswordCode
- Validate code not expired (24 hours)
- Hash new password with Argon2id
- Clear resetPasswordCode
- Rotate persistCode (invalidate other sessions)
- Return updated user
**ResetPassword.scala component:**
- Extend SummerComponent trait
- ComponentDetails: name "ResetPassword", description "Password recovery flow"
- Property schema:
- paramCode: String, URL parameter for reset code, default "code"
- redirect: Dropdown, page after successful reset
- HTMX handlers:
- onRequest: Parse email from form, call userService.requestPasswordReset,
always show success message (no email enumeration)
- onReset: Get code from URL param, parse new password + confirmation,
validate password length and match, call userService.resetPassword,
auto-login user, redirect to configured page
**Templates:**
- resetpassword/request.peb:
- Form with hx-post to onRequest
- Email input
- Submit button "Send Reset Link"
- Success message: "If an account exists, you will receive an email"
- resetpassword/reset.peb:
- Form with hx-post to onReset
- Hidden input with reset code from URL
- New password input
- Confirm password input
- Submit button "Reset Password"
- Error display for invalid/expired code
- mail/activation.peb (Pebble email template):
- Subject: "Activate your account"
- Body: Welcome message, activation link with code
- Plain text fallback
- mail/reset.peb (Pebble email template):
- Subject: "Reset your password"
- Body: Reset link with code, expiry notice (24 hours)
- Plain text fallback
**Register ResetPassword component in Plugin.scala**
./mill summercms.compile succeeds
ResetPassword component registered in Plugin.scala
Email templates have valid Pebble syntax
Reset code generation uses SecureRandom
Password reset request sends email (or stubs if no SMTP).
Reset link with token allows setting new password.
Activation email template ready for registration flow.
No email enumeration possible (generic success messages).
After all tasks complete:
1. User plugin compiles: `./mill summercms.compile`
2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar
3. ResetPassword component handles: onRequest, onReset
4. Email service can send activation and reset emails
5. Templates render valid forms with HTMX
- Logged-in user can view profile form pre-filled with their data
- Profile update saves name/surname changes
- Password change requires current password confirmation
- Avatar upload stores image and updates user record
- Password reset request always shows success (no enumeration)
- Reset link with valid code allows new password entry
- User automatically logged in after successful reset