# 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) |
