---
phase: 06-backend-authentication
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - summercms/resources/db/migration/V6__backend_auth.sql
  - summercms/src/auth/AdminUser.scala
  - summercms/src/auth/Role.scala
  - summercms/src/auth/AdminSession.scala
  - summercms/src/auth/LoginAttempt.scala
  - summercms/src/auth/PasswordService.scala
  - summercms/src/auth/SessionService.scala
  - summercms/src/auth/LoginService.scala
  - summercms/src/auth/AuthError.scala
  - summercms/src/auth/middleware/AuthMiddleware.scala
  - summercms/src/auth/routes/LoginRoutes.scala
  - summercms/src/api/Routes.scala
  - build.mill
autonomous: true

must_haves:
  truths:
    - "Admin can log in with valid email/password and receive a session token"
    - "Admin receives generic error message on invalid credentials (no hints about which field is wrong)"
    - "Admin can log out and the session is revoked"
    - "Progressive lockout prevents brute force (10 min after 5 failures, etc.)"
    - "Sessions can be listed and revoked individually"
  artifacts:
    - path: "summercms/resources/db/migration/V6__backend_auth.sql"
      provides: "Database schema for admin_users, roles, admin_sessions, login_attempts"
      contains: "CREATE TABLE admin_users"
    - path: "summercms/src/auth/PasswordService.scala"
      provides: "Argon2id password hashing"
      exports: ["PasswordService", "hash", "verify"]
    - path: "summercms/src/auth/SessionService.scala"
      provides: "JWT + DB hybrid session management"
      exports: ["SessionService", "createSession", "validateToken", "revokeSession"]
    - path: "summercms/src/auth/LoginService.scala"
      provides: "Login logic with progressive lockout"
      exports: ["LoginService", "authenticate"]
    - path: "summercms/src/auth/routes/LoginRoutes.scala"
      provides: "POST /admin/auth/login, POST /admin/auth/logout, GET/DELETE /admin/auth/sessions"
      min_lines: 50
  key_links:
    - from: "LoginRoutes.scala"
      to: "LoginService"
      via: "ZIO service dependency"
      pattern: "ZIO\\.service\\[LoginService\\]"
    - from: "LoginService"
      to: "PasswordService"
      via: "password verification"
      pattern: "passwordService\\.verify"
    - from: "SessionService"
      to: "admin_sessions table"
      via: "Quill queries"
      pattern: "querySchema\\[AdminSession\\]"
---

<objective>
Create the foundation for admin backend authentication with login/logout functionality.

Purpose: Enable admin users to authenticate securely with JWT sessions backed by database for revocation support. This is the prerequisite for all protected admin functionality.

Output:
- Database migration with admin_users, roles, admin_sessions, login_attempts tables
- PasswordService with Argon2id hashing
- SessionService with JWT + DB hybrid for revocation support
- LoginService with progressive lockout protection
- Login/logout HTTP endpoints with auth middleware
</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/06-backend-authentication/06-CONTEXT.md
@.planning/phases/06-backend-authentication/06-RESEARCH.md

# Existing codebase patterns
@summercms/src/repository/UserRepository.scala
@summercms/src/repository/RepositoryError.scala
@summercms/src/db/QuillContext.scala
@summercms/src/api/Routes.scala
@build.mill
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create database migration and domain models</name>
  <files>
    summercms/resources/db/migration/V6__backend_auth.sql
    summercms/src/auth/AdminUser.scala
    summercms/src/auth/Role.scala
    summercms/src/auth/AdminSession.scala
    summercms/src/auth/LoginAttempt.scala
    summercms/src/auth/AuthError.scala
    build.mill
  </files>
  <action>
    1. Add new dependencies to build.mill:
       - mvn"com.password4j:password4j:1.8.4" (Argon2id hashing)
       - mvn"com.github.jwt-scala::jwt-zio-json:11.0.3" (JWT with ZIO JSON)

    2. Create V6__backend_auth.sql migration with tables:
       - roles (id, code, name, permissions TEXT[], is_system, created_at)
       - admin_users (id, email, password_hash, first_name, last_name, role_id FK, totp_secret, totp_enabled, recovery_codes TEXT[], created_at, updated_at, last_login_at)
       - admin_sessions (id VARCHAR(64) PK, user_id FK, created_at, expires_at, ip_address INET, user_agent, revoked, revoked_at)
       - login_attempts (id, email, ip_address INET, attempted_at, successful)
       - Insert default Super Admin role with permissions = '{"*"}'
       - Add indexes on admin_sessions(user_id), admin_sessions(expires_at), login_attempts(email, attempted_at)

    3. Create domain models in summercms/src/auth/:
       - AdminUser case class mapping to admin_users table (use Instant for timestamps)
       - Role case class mapping to roles table
       - AdminSession case class mapping to admin_sessions table
       - LoginAttempt case class mapping to login_attempts table

    4. Create AuthError sealed trait ADT:
       - InvalidCredentials (generic - no hints)
       - AccountLocked(until: Instant)
       - TotpRequired
       - InvalidTotp
       - SessionExpired
       - SessionRevoked
       - InvalidToken(reason: String)

    Follow existing patterns from RepositoryError.scala and model/User.scala.
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - Migration file exists at summercms/resources/db/migration/V6__backend_auth.sql
    - All case classes compile with proper Instant types
  </verify>
  <done>
    Database schema defined for auth tables, domain models created matching schema, build compiles with new dependencies
  </done>
</task>

<task type="auto">
  <name>Task 2: Create PasswordService, SessionService, and LoginService</name>
  <files>
    summercms/src/auth/PasswordService.scala
    summercms/src/auth/SessionService.scala
    summercms/src/auth/LoginService.scala
    summercms/src/auth/repository/AdminUserRepository.scala
    summercms/src/auth/repository/AdminSessionRepository.scala
    summercms/src/auth/repository/LoginAttemptRepository.scala
    summercms/src/config/AppConfig.scala
  </files>
  <action>
    1. Create PasswordService (ZIO service pattern):
       - trait PasswordService with hash(password: String): Task[String] and verify(password: String, hash: String): Task[Boolean]
       - Implementation using Password4j with Argon2id (default settings are OWASP compliant)
       - Use ZIO.attemptBlocking for Password4j calls

    2. Create repository traits following existing UserRepository pattern:
       - AdminUserRepository: findById, findByEmail, create, update
       - AdminSessionRepository: create, findById, findByUserId, revoke, revokeAllForUser
       - LoginAttemptRepository: recordAttempt, countRecentFailures(email, ip, since)
       All should use RepositoryError for errors.

    3. Add auth config to AppConfig.scala:
       - jwtSecret: String (env: JWT_SECRET, default: generate-a-secure-secret-in-production)
       - sessionDuration: Duration (default: 24 hours)
       - rememberMeDuration: Duration (default: 30 days)

    4. Create SessionService (ZIO service pattern):
       - createSession(userId, role, rememberMe, ip, userAgent): Task[String] - returns JWT token
       - validateToken(token: String): IO[AuthError, SessionClaims]
       - revokeSession(sessionId: String): Task[Unit]
       - listSessions(userId: Long): Task[List[AdminSession]]
       - revokeAllSessions(userId: Long): Task[Unit]
       - Use jwt-zio-json for JWT encoding/decoding with HS256
       - Store session in DB and include sessionId in JWT claims
       - On validate: decode JWT, check expiry, check DB for revocation

    5. Create LoginService (ZIO service pattern):
       - authenticate(email, password, totpCode: Option, ip): IO[AuthError, AdminUser]
       - Progressive lockout thresholds (per RESEARCH.md):
         - 5 failures: 10 min lockout
         - 10 failures: 20 min lockout
         - 15 failures: 1 hour lockout
         - 20 failures: 24 hour lockout
       - IMPORTANT: Always call passwordService.verify even for non-existent users (timing attack prevention)
       - Record all login attempts (success and failure)
       - Return generic InvalidCredentials error for both wrong email and wrong password
       - For now, skip TOTP validation (will be added in plan 06-03)

    Use ZLayer pattern like existing services. Follow _root_.config import pattern to avoid shadowing.
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - PasswordService.hash returns Argon2id hash (starts with $argon2)
    - SessionService.createSession returns a JWT string
    - LoginService has lockout thresholds defined
  </verify>
  <done>
    PasswordService hashes with Argon2id, SessionService creates/validates JWT tokens with DB backing, LoginService implements progressive lockout, all repositories created
  </done>
</task>

<task type="auto">
  <name>Task 3: Create auth routes and middleware</name>
  <files>
    summercms/src/auth/middleware/AuthMiddleware.scala
    summercms/src/auth/routes/LoginRoutes.scala
    summercms/src/api/Routes.scala
    summercms/src/Main.scala
  </files>
  <action>
    1. Create AuthMiddleware.scala:
       - Extract JWT from Authorization: Bearer header OR __Host-SID cookie
       - Call SessionService.validateToken
       - On success, make SessionClaims available to handlers
       - On failure, return 401 Unauthorized with JSON error

    2. Create LoginRoutes.scala with endpoints:
       - POST /admin/auth/login
         - Body: { email, password, totpCode?, rememberMe? }
         - On success: Return { token, user: { id, email, firstName, lastName } }
         - Set __Host-SID cookie (httpOnly, secure, sameSite=Strict, path=/admin)
         - On lockout: Return 429 with Retry-After header
         - On invalid: Return 401 with generic "Invalid email or password"

       - POST /admin/auth/logout (requires auth)
         - Revoke current session
         - Clear __Host-SID cookie
         - Return 200 OK

       - GET /admin/auth/sessions (requires auth)
         - List all sessions for current user
         - Return array with id, createdAt, expiresAt, ipAddress, userAgent, current: boolean

       - DELETE /admin/auth/sessions/:id (requires auth)
         - Revoke specific session by ID
         - Only allow revoking own sessions
         - Return 204 No Content

    3. Update Routes.scala to compose LoginRoutes (public) and protected routes (with AuthMiddleware)

    4. Update Main.scala to provide all new service layers:
       - PasswordService.live
       - SessionService.live (needs AdminSessionRepository, AuthConfig)
       - LoginService.live (needs AdminUserRepository, LoginAttemptRepository, PasswordService)
       - AdminUserRepository.live, AdminSessionRepository.live, LoginAttemptRepository.live

    Use zio.json for JSON encoding/decoding. Follow existing HealthRoutes.scala pattern for route structure.

    Cookie configuration (per RESEARCH.md):
    - name: __Host-SID (prefix for additional security)
    - httpOnly: true
    - secure: true (in production, allow http in dev)
    - sameSite: Strict
    - path: /admin
    - maxAge: 24 hours (or 30 days with rememberMe)
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - curl -X POST localhost:8080/admin/auth/login with valid credentials returns token
    - curl -X POST localhost:8080/admin/auth/logout with valid token returns 200
    - curl -X GET localhost:8080/admin/auth/sessions with valid token returns session list
  </verify>
  <done>
    Login returns JWT token and sets cookie, logout revokes session, session listing works, auth middleware protects routes
  </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 manually in DB (with hashed password from PasswordService)
4. Test login flow: POST /admin/auth/login with credentials
5. Test session listing: GET /admin/auth/sessions with token
6. Test logout: POST /admin/auth/logout
7. Verify token no longer works after logout
</verification>

<success_criteria>
- Admin can log in with valid email/password and receive JWT token
- Login returns generic error for invalid credentials (no hints)
- Logout revokes the session in database
- Revoked token returns 401 on subsequent requests
- Progressive lockout kicks in after 5 failed attempts
- Sessions can be listed and individually revoked
</success_criteria>

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