- Plan 10-02: Added explicit UserMailService wiring in Account.scala onRegister handler for activation emails when activateMode == User - Plan 10-04: Split into backend-only (models, services, admin controller) reducing from 16 to 15 files, estimated context ~50% - Plan 10-05: New plan for frontend components (Posts, Post, Categories, RelatedPosts), Wave 3 depends on 10-04 - Updated ROADMAP.md to reflect 5 plans for Phase 10
280 lines
11 KiB
Markdown
280 lines
11 KiB
Markdown
---
|
|
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/components/Account.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"
|
|
---
|
|
|
|
<objective>
|
|
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
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jin/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.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
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Profile management with avatar upload</name>
|
|
<files>
|
|
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
|
|
</files>
|
|
<action>
|
|
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
|
|
</action>
|
|
<verify>
|
|
./mill summercms.compile succeeds
|
|
Account component has onUpdate and onUploadAvatar handlers
|
|
Templates have valid Pebble syntax
|
|
</verify>
|
|
<done>
|
|
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.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Password reset flow and email service</name>
|
|
<files>
|
|
plugins/golem15/user/components/ResetPassword.scala
|
|
plugins/golem15/user/components/Account.scala (modify)
|
|
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
|
|
</files>
|
|
<action>
|
|
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
|
|
|
|
**Account.scala modification (CRITICAL - wire activation email):**
|
|
- Modify the onRegister handler created in Plan 01 to integrate UserMailService:
|
|
- Inject UserMailService dependency into Account component
|
|
- After successful userService.register call, check UserSettings.activateMode
|
|
- If activateMode == ActivateMode.User, call mailService.sendActivationEmail(user, activationCode)
|
|
- This completes the key_link from Account.scala to UserMailService
|
|
|
|
**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**
|
|
</action>
|
|
<verify>
|
|
./mill summercms.compile succeeds
|
|
ResetPassword component registered in Plugin.scala
|
|
Account.scala onRegister calls mailService.sendActivationEmail when activateMode == User
|
|
Email templates have valid Pebble syntax
|
|
Reset code generation uses SecureRandom
|
|
</verify>
|
|
<done>
|
|
Password reset request sends email (or stubs if no SMTP).
|
|
Reset link with token allows setting new password.
|
|
Activation email sent during registration when activateMode is User.
|
|
No email enumeration possible (generic success messages).
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
After all tasks complete:
|
|
1. User plugin compiles: `./mill summercms.compile`
|
|
2. Account component has all handlers: onSignin, onRegister, onActivate, onUpdate, onUploadAvatar
|
|
3. Account.scala onRegister integrates UserMailService for activation emails
|
|
4. ResetPassword component handles: onRequest, onReset
|
|
5. Email service can send activation and reset emails
|
|
6. Templates render valid forms with HTMX
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- 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
|
|
- Registration with activateMode=User triggers activation email via UserMailService
|
|
- Password reset request always shows success (no enumeration)
|
|
- Reset link with valid code allows new password entry
|
|
- User automatically logged in after successful reset
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/10-core-plugins/10-02-SUMMARY.md`
|
|
</output>
|