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:
460
_bmad-output/implementation-artifacts/1-2-rbac.story.md
Normal file
460
_bmad-output/implementation-artifacts/1-2-rbac.story.md
Normal 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) |
|
||||
Reference in New Issue
Block a user