Files
website-enchun-mgr/_bmad-output/implementation-artifacts/1-2-rbac.story.md
pkupuk 8c87d71aa2 docs: update sprint-status and story files after Sprint 1 completion
- Mark Story 1-2-collections-definition as DONE (100%)
- Update all Sprint 1 split stories to Done status
- Epic 1 completed_stories: 0 → 2
- NFR4 (load testing): planned → done
- NFR9 (audit logging): planned → done

Implementation already committed in 7fd73e0
2026-01-31 17:34:41 +08:00

14 KiB

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

  1. AC4 - adminOnly() Created: File at access/adminOnly.ts checks user.role === 'admin'
  2. AC5 - adminOrEditor() Created: File at access/adminOrEditor.ts checks for both roles

Part 3: Apply Access Control

  1. AC6 - Users Collection: All operations restricted to adminOnly (except read)
  2. AC7 - Content Collections: Posts/Pages/Categories/Portfolio use adminOrEditor for CUD
  3. AC8 - Globals: Header/Footer restricted to adminOnly
  4. AC9 - TypeScript Types: Running pnpm build regenerates payload-types.ts without errors
  5. 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:

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:

{
  name: 'role',
  type: 'select',
  label: '角色',
  defaultValue: 'editor',
  required: true,
  options: [
    { label: '管理員', value: 'admin' },
    { label: '編輯者', value: 'editor' },
  ],
  admin: {
    position: 'sidebar',
  },
}

Update defaultColumns:

admin: {
  defaultColumns: ['name', 'email', 'role'],  // ⭐ Add 'role'
  useAsTitle: 'name',
},

Task 2: Create Access Control Functions

File 1: apps/backend/src/access/adminOnly.ts

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

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

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

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

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

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

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

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

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

  • Task 1.1: Add role field to Users collection
    • Add role select field with admin/editor options
    • Set defaultValue to 'editor'
    • Set admin.position to 'sidebar'
    • Update defaultColumns to include 'role'

Part 2: Access Control Functions

  • Task 2.1: Create adminOnly.ts

    • Create file at access/adminOnly.ts
    • Implement function checking user.role === 'admin'
    • Add JSDoc comments
  • Task 2.2: Create adminOrEditor.ts

    • Create file at access/adminOrEditor.ts
    • Implement function checking both roles
    • Add null check for user
    • Add JSDoc comments

Part 3: Apply Access Control

  • Task 3.1: Update Users collection access

    • Import adminOnly
    • Update all access properties except read
  • Task 3.2: Update Posts collection access

    • Import adminOrEditor
    • Update create/update/delete to adminOrEditor
  • Task 3.3: Update Pages collection access

    • Import adminOrEditor
    • Update create/update/delete to adminOrEditor
  • Task 3.4: Update Categories collection access

    • Import adminOrEditor
    • Update create/update/delete to adminOrEditor
  • Task 3.5: Update Portfolio collection access

    • Import adminOrEditor
    • Set create/update/delete to adminOrEditor
  • Task 3.6: Update Header global access

    • Import adminOnly
    • Set update to adminOnly
  • Task 3.7: Update Footer global access

    • Import adminOnly
    • Set update to adminOnly

Part 4: Testing

  • Task 4.1: Verify TypeScript types

    • Run pnpm build
    • Check for errors
  • Task 4.2: Test Admin user

    • Create admin user
    • Verify admin can access all collections
    • Verify admin can manage users
    • Verify admin can update globals
  • Task 4.3: Test Editor user

    • Create editor user
    • Verify editor can create/edit posts
    • Verify editor CANNOT manage users
    • Verify editor CANNOT update globals

Testing Requirements

Unit Tests

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