---
phase: 06-backend-authentication
plan: 03
type: execute
wave: 2
depends_on: ["06-01"]
files_modified:
  - summercms/src/auth/Permission.scala
  - summercms/src/auth/PermissionRegistry.scala
  - summercms/src/auth/PermissionChecker.scala
  - summercms/src/auth/TotpService.scala
  - summercms/src/auth/middleware/PermissionMiddleware.scala
  - summercms/src/auth/routes/TotpRoutes.scala
  - summercms/src/auth/routes/RoleRoutes.scala
  - summercms/src/auth/repository/RoleRepository.scala
  - summercms/src/auth/LoginService.scala
  - summercms/src/api/Routes.scala
  - build.mill
autonomous: true

must_haves:
  truths:
    - "Roles can be created with specific permissions attached"
    - "Controllers and views check permissions before rendering protected content"
    - "Wildcard permissions work (e.g., 'blog.*' matches 'blog.posts.create')"
    - "Super Admin role has '*' permission that matches everything"
    - "2FA can be enabled optionally by admin users"
  artifacts:
    - path: "summercms/src/auth/PermissionRegistry.scala"
      provides: "Plugin permission registration"
      exports: ["PermissionRegistry", "register", "getAllPermissions"]
    - path: "summercms/src/auth/PermissionChecker.scala"
      provides: "Permission checking with wildcard support"
      exports: ["PermissionChecker", "hasPermission", "hasAnyPermission"]
    - path: "summercms/src/auth/middleware/PermissionMiddleware.scala"
      provides: "Route-level permission enforcement"
      exports: ["PermissionMiddleware", "require"]
    - path: "summercms/src/auth/TotpService.scala"
      provides: "TOTP 2FA setup and verification"
      exports: ["TotpService", "generateSecret", "verifyCode"]
  key_links:
    - from: "PermissionMiddleware"
      to: "PermissionChecker"
      via: "permission validation"
      pattern: "permissionChecker\\.hasPermission"
    - from: "PermissionChecker"
      to: "RoleRepository"
      via: "role lookup"
      pattern: "roleRepository\\.findById"
    - from: "LoginService"
      to: "TotpService"
      via: "2FA verification"
      pattern: "totpService\\.verifyCode"
---

<objective>
Implement role-based access control (RBAC) with plugin-registered permissions and optional TOTP 2FA.

Purpose: Enable fine-grained access control where plugins can register their own permissions, roles aggregate permissions, and middleware enforces access rules. Optional 2FA adds an extra security layer for admin accounts.

Output:
- Permission registry for plugin-registered permissions
- Permission checker with wildcard matching
- Permission middleware for route protection
- Role CRUD endpoints
- TOTP 2FA setup and verification
</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
@.planning/phases/02-plugin-system/02-RESEARCH.md

# Dependencies from 06-01
@summercms/src/auth/Role.scala
@summercms/src/auth/AdminUser.scala
@summercms/src/auth/LoginService.scala
@summercms/src/auth/middleware/AuthMiddleware.scala
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create permission system</name>
  <files>
    summercms/src/auth/Permission.scala
    summercms/src/auth/PermissionRegistry.scala
    summercms/src/auth/PermissionChecker.scala
    summercms/src/auth/repository/RoleRepository.scala
  </files>
  <action>
    1. Create Permission.scala:
       - case class PermissionDef(
           code: String,      // e.g., "golem15.blog.access_posts"
           label: String,     // Human-readable: "Access blog posts"
           tab: String,       // Grouping in UI: "Blog"
           order: Int = 0
         )
       - companion object with helper methods

    2. Create PermissionRegistry (ZIO service with Ref):
       - trait PermissionRegistry:
           def register(pluginId: String, permissions: List[PermissionDef]): UIO[Unit]
           def getAllPermissions: UIO[List[PermissionDef]]
           def getPermissionsForPlugin(pluginId: String): UIO[List[PermissionDef]]
           def getPermissionsByTab: UIO[Map[String, List[PermissionDef]]]
       - Implementation uses ZIO Ref[Map[String, List[PermissionDef]]]
       - Register some core permissions on initialization:
         - "backend.access" - "Access admin backend"
         - "backend.manage_users" - "Manage admin users"
         - "backend.manage_roles" - "Manage roles"

    3. Create RoleRepository (following existing patterns):
       - findById(id: Long): IO[RepositoryError, Option[Role]]
       - findByCode(code: String): IO[RepositoryError, Option[Role]]
       - findAll: IO[RepositoryError, List[Role]]
       - create(role: Role): IO[RepositoryError, Role]
       - update(role: Role): IO[RepositoryError, Role]
       - delete(id: Long): IO[RepositoryError, Unit]
       Use Quill with querySchema mapping.

    4. Create PermissionChecker (ZIO service):
       - trait PermissionChecker:
           def hasPermission(userId: Long, permission: String): Task[Boolean]
           def hasAnyPermission(userId: Long, permissions: List[String]): Task[Boolean]
           def hasAllPermissions(userId: Long, permissions: List[String]): Task[Boolean]

       - Implementation:
         - Get user's role from AdminUserRepository
         - Get role's permissions
         - Match using wildcard logic:
           - "*" matches everything (Super Admin)
           - "blog.*" matches "blog.posts.create", "blog.categories.edit", etc.
           - Exact match: "blog.posts.create" matches only "blog.posts.create"

       - Wildcard matching algorithm:
         ```scala
         def matchesPermission(granted: String, required: String): Boolean =
           if granted == "*" then true
           else if granted.endsWith(".*") then
             required.startsWith(granted.dropRight(1))  // "blog.*" -> "blog."
           else
             granted == required
         ```
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - PermissionChecker.hasPermission returns true for user with matching permission
    - Wildcard "*" grants all permissions
    - Wildcard "blog.*" grants "blog.posts.create" but not "users.list"
  </verify>
  <done>
    Permission registry stores plugin permissions, permission checker validates with wildcard support, role repository provides CRUD
  </done>
</task>

<task type="auto">
  <name>Task 2: Create permission middleware and role management routes</name>
  <files>
    summercms/src/auth/middleware/PermissionMiddleware.scala
    summercms/src/auth/routes/RoleRoutes.scala
    summercms/src/api/Routes.scala
  </files>
  <action>
    1. Create PermissionMiddleware.scala:
       - object PermissionMiddleware:
           def require(permission: String): Middleware[...] - requires exactly this permission
           def requireAny(permissions: String*): Middleware[...] - requires any of these
           def requireAll(permissions: String*): Middleware[...] - requires all of these

       - Implementation pattern (composing with AuthMiddleware):
         - Get SessionClaims from context (set by AuthMiddleware)
         - Call PermissionChecker.hasPermission(claims.userId, permission)
         - If false, return 403 Forbidden with JSON: { error: "Insufficient permissions" }
         - If true, continue to handler

       - Example usage in routes:
         ```scala
         val protectedRoute = AuthMiddleware.apply @@
           PermissionMiddleware.require("blog.posts.create") @@
           handler { ... }
         ```

    2. Create RoleRoutes.scala (protected, requires "backend.manage_roles"):
       - GET /admin/roles
         - List all roles
         - Return: [{ id, code, name, permissions: [...], isSystem }]

       - GET /admin/roles/:id
         - Get single role
         - Return: { id, code, name, permissions: [...], isSystem }

       - POST /admin/roles (requires "backend.manage_roles")
         - Body: { code, name, permissions: [...] }
         - Validate code is unique
         - Return created role

       - PUT /admin/roles/:id (requires "backend.manage_roles")
         - Body: { name, permissions: [...] }
         - Cannot modify code or isSystem roles
         - Return updated role

       - DELETE /admin/roles/:id (requires "backend.manage_roles")
         - Cannot delete system roles (is_system = true)
         - Return 204 No Content

       - GET /admin/permissions
         - List all registered permissions (for role editor UI)
         - Return: { tabs: [{ name, permissions: [{code, label}] }] }

    3. Update Routes.scala to include RoleRoutes with proper middleware chain
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - GET /admin/roles returns role list (with Super Admin having "*")
    - POST /admin/roles creates new role
    - User without permission gets 403 Forbidden
    - User with permission gets 200 OK
  </verify>
  <done>
    PermissionMiddleware enforces route-level permissions, role CRUD endpoints available, permission listing available for UI
  </done>
</task>

<task type="auto">
  <name>Task 3: Add optional TOTP 2FA</name>
  <files>
    summercms/src/auth/TotpService.scala
    summercms/src/auth/routes/TotpRoutes.scala
    summercms/src/auth/LoginService.scala
    summercms/src/api/Routes.scala
    summercms/src/Main.scala
    build.mill
  </files>
  <action>
    1. Add java-totp dependency to build.mill:
       - mvn"dev.samstevens.totp:totp:1.7.1"

    2. Create TotpService.scala (ZIO service):
       - trait TotpService:
           def generateSecret: Task[String]
           def generateQrCodeDataUri(secret: String, email: String): Task[String]
           def verifyCode(secret: String, code: String): Task[Boolean]
           def generateRecoveryCodes: Task[List[String]]

       - Implementation using dev.samstevens.totp:
         - DefaultSecretGenerator for secret generation
         - QrData.Builder with issuer "SummerCMS Admin"
         - ZxingPngQrGenerator for QR code
         - DefaultCodeVerifier for verification
         - RecoveryCodeGenerator for backup codes (8 codes)

       - Recovery codes:
         - Generate 8 random codes
         - Hash each with SHA-256 before storing (same pattern as password reset tokens)
         - When verifying, hash input and compare to stored hashes
         - Each recovery code is single-use

    3. Create TotpRoutes.scala (all require auth):
       - POST /admin/auth/2fa/setup
         - Generate new secret
         - Return: { secret, qrCodeDataUri }
         - Does NOT enable 2FA yet (user must verify first)

       - POST /admin/auth/2fa/verify-setup
         - Body: { secret, code }
         - Verify the code matches the secret
         - If valid: update user with totp_secret, totp_enabled=true, generate and store recovery codes
         - Return: { enabled: true, recoveryCodes: [...] } (only time codes are shown!)

       - POST /admin/auth/2fa/disable
         - Body: { code } (require current TOTP code to disable)
         - Clear totp_secret, set totp_enabled=false, clear recovery_codes
         - Return: { enabled: false }

       - GET /admin/auth/2fa/status
         - Return: { enabled: boolean }

    4. Update LoginService.authenticate:
       - After password verification, check if user has totp_enabled=true
       - If yes and totpCode is None: return AuthError.TotpRequired
       - If yes and totpCode provided:
         - First try TotpService.verifyCode
         - If fails, try matching against recovery codes (consume if match)
       - If no 2FA, proceed normally

    5. Update Main.scala to provide TotpService.live
  </action>
  <verify>
    - ./mill summercms.compile succeeds
    - POST /admin/auth/2fa/setup returns secret and QR code data URI
    - After enabling 2FA, login requires TOTP code
    - Recovery code can be used once in place of TOTP
    - POST /admin/auth/2fa/disable removes 2FA requirement
  </verify>
  <done>
    2FA can be enabled/disabled by users, login enforces 2FA when enabled, recovery codes work as backup
  </done>
</task>

</tasks>

<verification>
After all tasks complete:
1. Run ./mill summercms.compile - should succeed
2. Test role creation: POST /admin/roles with new role
3. Test permission check: Access route without permission (should get 403)
4. Test wildcard: Give role "blog.*", verify "blog.posts.create" works
5. Test 2FA setup: POST /admin/auth/2fa/setup, scan QR, verify setup
6. Test 2FA login: Login without code (should fail), login with code (should succeed)
7. Test recovery code: Use recovery code instead of TOTP (should work once)
</verification>

<success_criteria>
- Roles can be created, updated, deleted (except system roles)
- Permission middleware returns 403 for unauthorized access
- Wildcard permissions work correctly ("*" and "prefix.*")
- 2FA can be optionally enabled by users
- 2FA login requires valid TOTP code when enabled
- Recovery codes work as 2FA backup (single-use)
- Super Admin role with "*" can access everything
</success_criteria>

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