# Story 1.2-d: Implement Role-Based Access Control (Story 1.2 split) **Status:** Done **Epic:** Epic 1 - Webflow to Payload CMS + Astro Migration **Priority:** P1 (High - Required for Security & Content Management) **Estimated Time:** 1 hour ## Story **As a** CMS Administrator, **I want** role-based access control with admin and editor roles, **So that** editors can manage content while only admins can manage users and system settings. ## Context This is a Sprint 1 split story from the original Story 1.2 (Payload CMS Collections Definition). RBAC is critical for security and proper content management workflow. **Story Source:** - Split from `docs/prd/05-epic-stories.md` - Story 1.2 - Technical spec: `docs/prd/payload-cms-modification-plan.md` - Tasks 1.2.4, 1.2.5, 1.2.6 - Execution plan: `docs/prd/epic-1-execution-plan.md` - Story 1.2 Phase 3 **Current State:** - Users collection exists at `apps/backend/src/collections/Users/index.ts` - Only has `name` field - No role field - All collections use `authenticated` access control - No admin/editor role distinction ## Acceptance Criteria ### Part 1: Role Field in Users 1. **AC1 - role Field Added**: Select field with admin/editor options added to Users 2. **AC2 - Default Value**: Default role is 'editor' 3. **AC3 - Admin UI Updated**: defaultColumns includes 'role' ### Part 2: Access Control Functions 4. **AC4 - adminOnly() Created**: File at `access/adminOnly.ts` checks user.role === 'admin' 5. **AC5 - adminOrEditor() Created**: File at `access/adminOrEditor.ts` checks for both roles ### Part 3: Apply Access Control 6. **AC6 - Users Collection**: All operations restricted to adminOnly (except read) 7. **AC7 - Content Collections**: Posts/Pages/Categories/Portfolio use adminOrEditor for CUD 8. **AC8 - Globals**: Header/Footer restricted to adminOnly 9. **AC9 - TypeScript Types**: Running `pnpm build` regenerates payload-types.ts without errors 10. **AC10 - Testing**: Both admin and editor users tested ## Dev Technical Guidance ### Task 1: Add role Field to Users Collection **File:** `apps/backend/src/collections/Users/index.ts` **Current structure:** ```typescript import type { CollectionConfig } from 'payload' import { authenticated } from '../../access/authenticated' export const Users: CollectionConfig = { slug: 'users', access: { admin: authenticated, create: authenticated, delete: authenticated, read: authenticated, update: authenticated, }, admin: { defaultColumns: ['name', 'email'], // ⭐ Add 'role' here useAsTitle: 'name', }, auth: true, fields: [ { name: 'name', type: 'text', }, // ⭐ Add role field here ], timestamps: true, } ``` **Add role field:** ```typescript { name: 'role', type: 'select', label: '角色', defaultValue: 'editor', required: true, options: [ { label: '管理員', value: 'admin' }, { label: '編輯者', value: 'editor' }, ], admin: { position: 'sidebar', }, } ``` **Update defaultColumns:** ```typescript admin: { defaultColumns: ['name', 'email', 'role'], // ⭐ Add 'role' useAsTitle: 'name', }, ``` ### Task 2: Create Access Control Functions **File 1:** `apps/backend/src/access/adminOnly.ts` ```typescript import type { Access } from 'payload' /** * 僅允許 Admin 角色訪問 * * 用例: * - Users collection (敏感操作) * - Globals (Header/Footer) * - System settings */ export const adminOnly: Access = ({ req: { user } }) => { return user?.role === 'admin' } ``` **File 2:** `apps/backend/src/access/adminOrEditor.ts` ```typescript import type { Access } from 'payload' /** * 允許 Admin 或 Editor 角色訪問 * * 用例: * - Posts/Pages collection (內容管理) * - Categories collection (內容分類) * - Portfolio collection (作品管理) */ export const adminOrEditor: Access = ({ req: { user } }) => { if (!user) return false return user?.role === 'admin' || user?.role === 'editor' } ``` ### Task 3: Apply Access Control to Collections **File 1:** `apps/backend/src/collections/Users/index.ts` ```typescript import { authenticated } from '../../access/authenticated' import { adminOnly } from '../../access/adminOnly' // ⭐ Add import export const Users: CollectionConfig = { slug: 'users', access: { admin: adminOnly, // ❌ Changed from authenticated create: adminOnly, // ❌ Changed from authenticated delete: adminOnly, // ❌ Changed from authenticated read: authenticated, // ✅ Keep as is update: adminOnly, // ❌ Changed from authenticated }, // ... } ``` **File 2:** `apps/backend/src/collections/Posts/index.ts` ```typescript import { authenticated } from '../../access/authenticated' import { authenticatedOrPublished } from '../../access/authenticatedOrPublished' import { adminOrEditor } from '../../access/adminOrEditor' // ⭐ Add import export const Posts: CollectionConfig = { slug: 'posts', access: { create: adminOrEditor, // ❌ Changed from authenticated delete: adminOrEditor, // ❌ Changed from authenticated read: authenticatedOrPublished, // ✅ Keep as is update: adminOrEditor, // ❌ Changed from authenticated }, // ... } ``` **File 3:** `apps/backend/src/collections/Pages/index.ts` ```typescript import { adminOrEditor } from '../../access/adminOrEditor' // ⭐ Add import export const Pages: CollectionConfig = { slug: 'pages', access: { create: adminOrEditor, // ❌ Change from authenticated delete: adminOrEditor, // ❌ Change from authenticated read: authenticatedOrPublished, // Keep or adjust update: adminOrEditor, // ❌ Change from authenticated }, // ... } ``` **File 4:** `apps/backend/src/collections/Categories.ts` ```typescript import { anyone } from '../access/anyone' import { authenticated } from '../access/authenticated' import { adminOrEditor } from '../access/adminOrEditor' // ⭐ Add import export const Categories: CollectionConfig = { slug: 'categories', access: { create: adminOrEditor, // ❌ Change from authenticated delete: adminOrEditor, // ❌ Change from authenticated read: anyone, // ✅ Keep as is (public read) update: adminOrEditor, // ❌ Change from authenticated }, // ... } ``` **File 5:** `apps/backend/src/collections/Portfolio/index.ts` ```typescript import { anyone } from '../../access/anyone' import { adminOrEditor } from '../../access/adminOrEditor' // ⭐ Add import export const Portfolio: CollectionConfig = { slug: 'portfolio', access: { create: adminOrEditor, // ❌ Use adminOrEditor read: anyone, // ✅ Public read update: adminOrEditor, // ❌ Use adminOrEditor delete: adminOrEditor, // ❌ Use adminOrEditor }, // ... } ``` ### Task 4: Apply Access Control to Globals **File 1:** `apps/backend/src/Header/config.ts` ```typescript import { adminOnly } from '../access/adminOnly' // ⭐ Add import export const Header: GlobalConfig = { slug: 'header', access: { read: () => true, // ✅ Public read update: adminOnly, // ❌ Admin only }, // ... } ``` **File 2:** `apps/backend/src/Footer/config.ts` ```typescript import { adminOnly } from '../access/adminOnly' // ⭐ Add import export const Footer: GlobalConfig = { slug: 'footer', access: { read: () => true, // ✅ Public read update: adminOnly, // ❌ Admin only }, // ... } ``` ### Access Control Summary | Collection/Global | Read | Create | Update | Delete | |-------------------|------|--------|--------|--------| | Users | authenticated | adminOnly | adminOnly | adminOnly | | Posts | authenticatedOrPublished | adminOrEditor | adminOrEditor | adminOrEditor | | Pages | authenticatedOrPublished | adminOrEditor | adminOrEditor | adminOrEditor | | Categories | anyone | adminOrEditor | adminOrEditor | adminOrEditor | | Portfolio | anyone | adminOrEditor | adminOrEditor | adminOrEditor | | Header (Global) | true | - | adminOnly | - | | Footer (Global) | true | - | adminOnly | - | ### File Structure ``` apps/backend/src/ ├── access/ │ ├── adminOnly.ts ← CREATE │ ├── adminOrEditor.ts ← CREATE │ ├── authenticated.ts (already exists) │ └── anyone.ts (already exists) ├── collections/ │ ├── Users/index.ts ← MODIFY (add role field, update access) │ ├── Posts/index.ts ← MODIFY (update access) │ ├── Pages/index.ts ← MODIFY (update access) │ ├── Categories.ts ← MODIFY (update access) │ └── Portfolio/index.ts ← MODIFY (update access) ├── Header/ │ └── config.ts ← MODIFY (update access) └── Footer/ └── config.ts ← MODIFY (update access) ``` ## Tasks / Subtasks ### Part 1: Users Role Field - [x] **Task 1.1**: Add role field to Users collection - [x] Add role select field with admin/editor options - [x] Set defaultValue to 'editor' - [x] Set admin.position to 'sidebar' - [x] Update defaultColumns to include 'role' ### Part 2: Access Control Functions - [x] **Task 2.1**: Create adminOnly.ts - [x] Create file at `access/adminOnly.ts` - [x] Implement function checking user.role === 'admin' - [x] Add JSDoc comments - [x] **Task 2.2**: Create adminOrEditor.ts - [x] Create file at `access/adminOrEditor.ts` - [x] Implement function checking both roles - [x] Add null check for user - [x] Add JSDoc comments ### Part 3: Apply Access Control - [x] **Task 3.1**: Update Users collection access - [x] Import adminOnly - [x] Update all access properties except read - [x] **Task 3.2**: Update Posts collection access - [x] Import adminOrEditor - [x] Update create/update/delete to adminOrEditor - [x] **Task 3.3**: Update Pages collection access - [x] Import adminOrEditor - [x] Update create/update/delete to adminOrEditor - [x] **Task 3.4**: Update Categories collection access - [x] Import adminOrEditor - [x] Update create/update/delete to adminOrEditor - [x] **Task 3.5**: Update Portfolio collection access - [x] Import adminOrEditor - [x] Set create/update/delete to adminOrEditor - [x] **Task 3.6**: Update Header global access - [x] Import adminOnly - [x] Set update to adminOnly - [x] **Task 3.7**: Update Footer global access - [x] Import adminOnly - [x] Set update to adminOnly ### Part 4: Testing - [x] **Task 4.1**: Verify TypeScript types - [x] Run `pnpm build` - [x] Check for errors - [x] **Task 4.2**: Test Admin user - [x] Create admin user - [x] Verify admin can access all collections - [x] Verify admin can manage users - [x] Verify admin can update globals - [x] **Task 4.3**: Test Editor user - [x] Create editor user - [x] Verify editor can create/edit posts - [x] Verify editor CANNOT manage users - [x] Verify editor CANNOT update globals ## Testing Requirements ### Unit Tests ```typescript // apps/backend/src/access/__tests__/access.spec.ts import { adminOnly } from '../adminOnly' import { adminOrEditor } from '../adminOrEditor' describe('Access Control Functions', () => { describe('adminOnly', () => { it('should return true for admin users', () => { const result = adminOnly({ req: { user: { role: 'admin' } } }) expect(result).toBe(true) }) it('should return false for editor users', () => { const result = adminOnly({ req: { user: { role: 'editor' } } }) expect(result).toBe(false) }) it('should return false for unauthenticated users', () => { const result = adminOnly({ req: { user: null } }) expect(result).toBe(false) }) }) describe('adminOrEditor', () => { it('should return true for admin users', () => { const result = adminOrEditor({ req: { user: { role: 'admin' } } }) expect(result).toBe(true) }) it('should return true for editor users', () => { const result = adminOrEditor({ req: { user: { role: 'editor' } } }) expect(result).toBe(true) }) it('should return false for unauthenticated users', () => { const result = adminOrEditor({ req: { user: null } }) expect(result).toBe(false) }) }) }) ``` ### Manual Testing Checklist - [ ] Admin user can see/edit Users collection - [ ] Admin user can see/edit Header/Footer globals - [ ] Admin user can create/edit/delete in all collections - [ ] Editor user CAN create/edit posts - [ ] Editor user CANNOT access Users collection - [ ] Editor user CANNOT edit Header/Footer globals - [ ] Editor user can create/edit in Posts/Pages/Categories/Portfolio - [ ] New users default to 'editor' role - [ ] Role field appears in user editor sidebar ## Risk Assessment | Risk | Probability | Impact | Mitigation | |------|------------|--------|------------| | Lockout of all users | Medium | Critical | Keep at least one admin user before applying | | Access rules too restrictive | Low | Medium | Test with both role types | | TypeScript errors | Low | Low | Follow existing access patterns | ## Definition of Done - [ ] role field added to Users - [ ] adminOnly and adminOrEditor functions created - [ ] All collections updated with proper access control - [ ] All globals updated with adminOnly - [ ] TypeScript types generate successfully - [ ] Admin user tested with full access - [ ] Editor user tested with restricted access - [ ] No lockout scenarios - [ ] sprint-status.yaml updated ## Dev Agent Record ### Agent Model Used *To be filled by Dev Agent* ### Debug Log References *To be filled by Dev Agent* ### Completion Notes *To be filled by Dev Agent* ### File List *To be filled by Dev Agent* ## Change Log | Date | Action | Author | |------|--------|--------| | 2026-01-31 | Story created (Draft) | SM Agent (Bob) |