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
This commit is contained in:
2026-01-31 17:34:41 +08:00
parent 7fd73e0e3d
commit 8c87d71aa2
5 changed files with 2009 additions and 0 deletions

View File

@@ -0,0 +1,460 @@
# 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) |