---
phase: 06-backend-authentication
plan: 02
type: execute
wave: 2
depends_on: ["06-01"]
files_modified:
  - summercms/resources/db/migration/V7__password_reset.sql
  - summercms/src/auth/PasswordResetToken.scala
  - summercms/src/auth/PasswordResetService.scala
  - summercms/src/auth/repository/PasswordResetRepository.scala
  - summercms/src/auth/EmailService.scala
  - summercms/src/auth/routes/PasswordResetRoutes.scala
  - summercms/src/api/Routes.scala
  - summercms/src/config/AppConfig.scala
  - build.mill
autonomous: true

must_haves:
  truths:
    - "User can request password reset and receives email (if account exists)"
    - "Request always returns success regardless of email existence (timing attack prevention)"
    - "Reset link expires after 30 minutes"
    - "After successful reset, redirect to login (no auto-login)"
    - "All existing sessions are terminated when password changes"
  artifacts:
    - path: "summercms/src/auth/PasswordResetService.scala"
      provides: "Password reset token generation and validation"
      exports: ["PasswordResetService", "requestReset", "validateToken", "resetPassword"]
    - path: "summercms/src/auth/EmailService.scala"
      provides: "Email sending capability"
      exports: ["EmailService", "sendPasswordReset"]
    - path: "summercms/src/auth/routes/PasswordResetRoutes.scala"
      provides: "POST /admin/auth/forgot-password, POST /admin/auth/reset-password"
      min_lines: 40
  key_links:
    - from: "PasswordResetService"
      to: "EmailService"
      via: "sending reset email"
      pattern: "emailService\\.sendPasswordReset"
    - from: "PasswordResetService.resetPassword"
      to: "SessionService.revokeAllSessions"
      via: "terminate sessions on password change"
      pattern: "sessionService\\.revokeAllSessions"
---

<objective>
Implement password reset flow with secure token generation and email delivery.

Purpose: Allow admin users to recover account access when they forget their password, while maintaining security through time-limited tokens and session invalidation.

Output:
- Password reset token generation with SHA-256 hashing for storage
- Email sending service (wrapped JavaMail in ZIO)
- Password reset HTTP endpoints
- Automatic session termination on password change
</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/phases/06-backend-authentication/06-CONTEXT.md
@.planning/phases/06-backend-authentication/06-RESEARCH.md
@.planning/phases/06-backend-authentication/06-01-SUMMARY.md

# Dependencies from 06-01
@summercms/src/auth/AdminUser.scala
@summercms/src/auth/PasswordService.scala
@summercms/src/auth/SessionService.scala
@summercms/src/auth/repository/AdminUserRepository.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create password reset token infrastructure</name>
  <files>
    summercms/resources/db/migration/V7__password_reset.sql
    summercms/src/auth/PasswordResetToken.scala
    summercms/src/auth/repository/PasswordResetRepository.scala
    summercms/src/config/AppConfig.scala
  </files>
  <action>
    1. Create V7__password_reset.sql migration:
       - CREATE TABLE password_reset_tokens (
           id BIGSERIAL PRIMARY KEY,
           token_hash VARCHAR(64) NOT NULL UNIQUE,  -- SHA-256 hex
           user_id BIGINT NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
           created_at TIMESTAMPTZ NOT NULL,
           expires_at TIMESTAMPTZ NOT NULL,
           ip_address INET,
           used BOOLEAN DEFAULT false,
           used_at TIMESTAMPTZ
         )
       - CREATE INDEX idx_password_reset_tokens_user_id ON password_reset_tokens(user_id)
       - CREATE INDEX idx_password_reset_tokens_expires ON password_reset_tokens(expires_at)

    2. Create PasswordResetToken case class:
       - id: Long
       - tokenHash: String
       - userId: Long
       - createdAt: Instant
       - expiresAt: Instant
       - ipAddress: Option[String]
       - used: Boolean
       - usedAt: Option[Instant]

    3. Create PasswordResetRepository (following existing patterns):
       - create(token: PasswordResetToken): IO[RepositoryError, PasswordResetToken]
       - findByTokenHash(hash: String): IO[RepositoryError, Option[PasswordResetToken]]
       - markUsed(id: Long): IO[RepositoryError, Unit]
       - invalidateAllForUser(userId: Long): IO[RepositoryError, Unit]
       Use Quill with querySchema mapping.

    4. Add email config to AppConfig.scala:
       - smtp.host: String (env: SMTP_HOST, default: localhost)
       - smtp.port: Int (env: SMTP_PORT, default: 587)
       - smtp.username: Option[String] (env: SMTP_USERNAME)
       - smtp.password: Option[String] (env: SMTP_PASSWORD)
       - smtp.from: String (env: SMTP_FROM, default: noreply@summercms.local)
       - smtp.startTls: Boolean (env: SMTP_STARTTLS, default: true)
       - passwordReset.tokenExpiry: Duration (default: 30 minutes)
       - passwordReset.baseUrl: String (env: APP_BASE_URL, default: http://localhost:8080)
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - Migration file exists at summercms/resources/db/migration/V7__password_reset.sql
    - PasswordResetRepository compiles with proper Quill queries
  </verify>
  <done>
    Database table for password reset tokens created, domain model defined, repository with CRUD operations implemented
  </done>
</task>

<task type="auto">
  <name>Task 2: Create EmailService and PasswordResetService</name>
  <files>
    summercms/src/auth/EmailService.scala
    summercms/src/auth/PasswordResetService.scala
    build.mill
  </files>
  <action>
    1. Add JavaMail dependency to build.mill:
       - mvn"com.sun.mail:jakarta.mail:2.0.1"

    2. Create EmailService (ZIO service pattern):
       - trait EmailService:
           def sendPasswordReset(to: String, token: String, ipAddress: String, requestedAt: Instant): Task[Unit]
       - Implementation wraps JavaMail in ZIO.attemptBlocking
       - Email content (per CONTEXT.md - includes context about the request):
         - Subject: "SummerCMS Password Reset Request"
         - Body includes:
           - Reset link: {baseUrl}/admin/auth/reset-password?token={token}
           - IP address that requested reset
           - Request timestamp
           - "If you didn't request this, ignore this email"
           - "This link expires in 30 minutes"
       - Use HTML email format with simple styling

    3. Create PasswordResetService (ZIO service pattern):
       - requestReset(email: String, ipAddress: String): Task[Unit]
         - ALWAYS return success (timing attack prevention)
         - If user exists:
           - Generate 256-bit random token using SecureRandom (64 chars URL-safe base64)
           - Hash with SHA-256 before storing
           - Invalidate any existing tokens for this user
           - Store hashed token with 30-minute expiry
           - Send email with plain token
         - If user doesn't exist:
           - Do nothing but take same amount of time (simulate DB write + email send)

       - validateToken(token: String): IO[AuthError, Long]
         - Hash the provided token
         - Look up in database by hash
         - Check expiry
         - Check not already used
         - Return userId if valid

       - resetPassword(token: String, newPassword: String): Task[Unit]
         - Validate token (get userId)
         - Hash new password with PasswordService
         - Update user's password_hash
         - Mark token as used
         - CRITICAL: Call SessionService.revokeAllSessions(userId) to terminate all sessions
         - Return success

    Token generation (per RESEARCH.md):
    - Use java.security.SecureRandom for entropy
    - 48 bytes = 64 URL-safe base64 characters
    - Hash with SHA-256 before storage
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - EmailService.sendPasswordReset can be called (logs in dev mode if SMTP not configured)
    - PasswordResetService.requestReset completes for both existing and non-existing emails
  </verify>
  <done>
    EmailService sends password reset emails, PasswordResetService generates secure tokens and handles full reset flow including session termination
  </done>
</task>

<task type="auto">
  <name>Task 3: Create password reset routes</name>
  <files>
    summercms/src/auth/routes/PasswordResetRoutes.scala
    summercms/src/api/Routes.scala
    summercms/src/Main.scala
  </files>
  <action>
    1. Create PasswordResetRoutes.scala:
       - POST /admin/auth/forgot-password (public)
         - Body: { email: String }
         - Always return 200 with { success: true, message: "If an account exists, a reset email has been sent" }
         - Call PasswordResetService.requestReset in background (don't block response)
         - Extract IP from request for logging

       - GET /admin/auth/reset-password?token=xxx (public)
         - Validate token (don't consume it)
         - If valid: return 200 with { valid: true }
         - If invalid/expired: return 400 with { valid: false, error: "Token invalid or expired" }

       - POST /admin/auth/reset-password (public)
         - Body: { token: String, password: String }
         - Call PasswordResetService.resetPassword
         - On success: return 200 with { success: true, message: "Password has been reset. Please log in." }
         - On failure: return 400 with appropriate error
         - Per CONTEXT.md: NO auto-login, redirect to login page (client-side)

    2. Update Routes.scala to include PasswordResetRoutes

    3. Update Main.scala to provide:
       - EmailService.live (needs SmtpConfig)
       - PasswordResetService.live (needs PasswordResetRepository, AdminUserRepository, PasswordService, SessionService, EmailService)
       - PasswordResetRepository.live (needs Quill context)

    Response format: Use zio.json for all JSON encoding.
    All endpoints return JSON with consistent structure.
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - curl -X POST localhost:8080/admin/auth/forgot-password -d '{"email":"test@test.com"}' returns 200
    - curl -X POST localhost:8080/admin/auth/reset-password with valid token returns 200
    - After password reset, old sessions are revoked (test by trying to use old token)
  </verify>
  <done>
    Password reset flow complete: request generates email, token validation works, password update terminates all sessions
  </done>
</task>

</tasks>

<verification>
After all tasks complete:
1. Run ./mill summercms.compile - should succeed
2. Run migrations against database
3. Create a test admin user with known email
4. Test forgot password: POST /admin/auth/forgot-password
5. Check that email would be sent (verify in logs or mailhog)
6. Test token validation: GET /admin/auth/reset-password?token=xxx
7. Test password reset: POST /admin/auth/reset-password
8. Verify old session token no longer works after password change
</verification>

<success_criteria>
- POST /admin/auth/forgot-password always returns 200 (no email existence leak)
- Reset email contains IP address and timestamp of request
- Reset token expires after 30 minutes
- Password reset terminates all existing sessions
- After reset, user must log in again (no auto-login)
</success_criteria>

<output>
After completion, create `.planning/phases/06-backend-authentication/06-02-SUMMARY.md`
</output>
