docs: separate documentation and specs into initial commit
Establish baseline for project documentation including BMAD specs, PRD, and system architecture notes.
This commit is contained in:
@@ -0,0 +1,490 @@
|
||||
# Story 1.12-a: Add Audit Logging System (NFR9)
|
||||
|
||||
**Status:** Draft
|
||||
**Epic:** Epic 1 - Webflow to Payload CMS + Astro Migration
|
||||
**Priority:** P1 (High - NFR9 Compliance Requirement)
|
||||
**Estimated Time:** 2 hours
|
||||
|
||||
## Story
|
||||
|
||||
**As a** System Administrator,
|
||||
**I want** an audit logging system that records all critical operations,
|
||||
**So that** I can track user actions for compliance and security auditing (NFR9 requirement).
|
||||
|
||||
## Context
|
||||
|
||||
This is a Sprint 1 addition story. NFR9 requires logging all critical operations (login, content changes, settings modifications) for audit purposes. This was identified as missing from the original plan.
|
||||
|
||||
**Story Source:**
|
||||
- NFR9 from `docs/prd/02-requirements.md`
|
||||
- Sprint 1 adjustments in `sprint-status.yaml`
|
||||
|
||||
**NFR9 Requirement:**
|
||||
> "The system must log all critical operations (login, content changes, settings modifications) for audit purposes."
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Part 1: Audit Collection
|
||||
1. **AC1 - Audit Collection Created**: New Audit collection with fields for action, user, timestamp, collection, documentId, before/after data
|
||||
2. **AC2 - Indexes Configured**: Indexes on userId, action, timestamp for efficient querying
|
||||
|
||||
### Part 2: Logging Hooks
|
||||
3. **AC3 - Login/Logout Logging**: Authentication events logged with user and timestamp
|
||||
4. **AC4 - Content Changes Logging**: create/update/delete operations logged with before/after values
|
||||
5. **AC5 - Settings Logging**: Global changes (Header/Footer) logged with user attribution
|
||||
|
||||
### Part 3: Admin Interface
|
||||
6. **AC6 - Audit Log Viewer**: Admin-only view to browse and filter audit logs
|
||||
7. **AC7 - 90-Day Retention**: Auto-delete logs older than 90 days
|
||||
|
||||
### Part 4: Testing
|
||||
8. **AC8 - TypeScript Types**: Running `pnpm build` regenerates payload-types.ts without errors
|
||||
9. **AC9 - Functionality Tested**: All logging scenarios tested and working
|
||||
|
||||
## Dev Technical Guidance
|
||||
|
||||
### Task 1: Create Audit Collection
|
||||
|
||||
**File:** `apps/backend/src/collections/Audit/index.ts`
|
||||
|
||||
```typescript
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { adminOnly } from '../../access/adminOnly'
|
||||
|
||||
export const Audit: CollectionConfig = {
|
||||
slug: 'audit',
|
||||
access: {
|
||||
create: () => false, // Only system can create
|
||||
delete: adminOnly, // Only admins can delete
|
||||
read: adminOnly, // Only admins can read
|
||||
update: () => false, // Logs are immutable
|
||||
},
|
||||
admin: {
|
||||
defaultColumns: ['timestamp', 'action', 'user', 'collection'],
|
||||
useAsTitle: 'action',
|
||||
description: '審計日誌 - 記錄所有系統關鍵操作',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'action',
|
||||
type: 'select',
|
||||
required: true,
|
||||
options: [
|
||||
{ label: '登入', value: 'login' },
|
||||
{ label: '登出', value: 'logout' },
|
||||
{ label: '創建', value: 'create' },
|
||||
{ label: '更新', value: 'update' },
|
||||
{ label: '刪除', value: 'delete' },
|
||||
{ label: '設定修改', value: 'settings' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'collection',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '操作的 collection 名稱',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'documentId',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '受影響文件的 ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'before',
|
||||
type: 'json',
|
||||
admin: {
|
||||
description: '操作前的數據',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'after',
|
||||
type: 'json',
|
||||
admin: {
|
||||
description: '操作後的數據',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ipAddress',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '使用者 IP 地址',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'userAgent',
|
||||
type: 'text',
|
||||
admin: {
|
||||
description: '瀏覽器 User Agent',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'timestamp',
|
||||
type: 'date',
|
||||
required: true,
|
||||
defaultValue: () => new Date(),
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: false,
|
||||
}
|
||||
```
|
||||
|
||||
**Register in payload.config.ts:**
|
||||
```typescript
|
||||
import { Audit } from './collections/Audit'
|
||||
|
||||
collections: [Pages, Posts, Media, Categories, Users, Audit],
|
||||
```
|
||||
|
||||
### Task 2: Create Logging Utility
|
||||
|
||||
**File:** `apps/backend/src/utilities/auditLogger.ts`
|
||||
|
||||
```typescript
|
||||
import type { PayloadRequest } from 'payload'
|
||||
import { payload } from '@/payload'
|
||||
|
||||
export interface AuditLogOptions {
|
||||
action: 'login' | 'logout' | 'create' | 'update' | 'delete' | 'settings'
|
||||
collection?: string
|
||||
documentId?: string
|
||||
before?: Record<string, unknown>
|
||||
after?: Record<string, unknown>
|
||||
req: PayloadRequest
|
||||
}
|
||||
|
||||
/**
|
||||
* 創建審計日誌記錄
|
||||
*/
|
||||
export async function createAuditLog(options: AuditLogOptions): Promise<void> {
|
||||
const { action, collection, documentId, before, after, req } = options
|
||||
|
||||
try {
|
||||
await payload.create({
|
||||
collection: 'audit',
|
||||
data: {
|
||||
action,
|
||||
user: req.user?.id || null,
|
||||
collection,
|
||||
documentId,
|
||||
before,
|
||||
after,
|
||||
ipAddress: req.headers.get('x-forwarded-for') || req.headers.get('cf-connecting-ip') || 'unknown',
|
||||
userAgent: req.headers.get('user-agent') || 'unknown',
|
||||
timestamp: new Date(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to create audit log:', error)
|
||||
// Don't throw - audit logging failure shouldn't break the main operation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3: Add Login/Logout Hooks
|
||||
|
||||
**File:** `apps/backend/src/collections/Users/index.ts`
|
||||
|
||||
```typescript
|
||||
import { createAuditLog } from '../../utilities/auditLogger'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
// ... existing config ...
|
||||
hooks: {
|
||||
afterLogin: [
|
||||
async ({ req }) => {
|
||||
await createAuditLog({
|
||||
action: 'login',
|
||||
req,
|
||||
})
|
||||
},
|
||||
],
|
||||
afterLogout: [
|
||||
async ({ req }) => {
|
||||
await createAuditLog({
|
||||
action: 'logout',
|
||||
req,
|
||||
})
|
||||
},
|
||||
],
|
||||
// ... existing hooks ...
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Task 4: Add Content Change Hooks
|
||||
|
||||
**For each collection (Posts, Pages, Categories, Portfolio):**
|
||||
|
||||
**File:** `apps/backend/src/collections/Posts/index.ts`
|
||||
|
||||
```typescript
|
||||
import { createAuditLog } from '../../utilities/auditLogger'
|
||||
|
||||
export const Posts: CollectionConfig = {
|
||||
// ... existing config ...
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, previousDoc, req, operation }) => {
|
||||
// Only log for authenticated users, not system operations
|
||||
if (!req.user) return
|
||||
|
||||
const action = operation === 'create' ? 'create' : 'update'
|
||||
|
||||
await createAuditLog({
|
||||
action,
|
||||
collection: 'posts',
|
||||
documentId: doc.id,
|
||||
before: operation === 'update' ? previousDoc : undefined,
|
||||
after: doc,
|
||||
req,
|
||||
})
|
||||
|
||||
// ... existing revalidatePost hook ...
|
||||
},
|
||||
],
|
||||
afterDelete: [
|
||||
async ({ doc, req }) => {
|
||||
if (!req.user) return
|
||||
|
||||
await createAuditLog({
|
||||
action: 'delete',
|
||||
collection: 'posts',
|
||||
documentId: doc.id,
|
||||
before: doc,
|
||||
req,
|
||||
})
|
||||
|
||||
// ... existing revalidateDelete hook ...
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Task 5: Add Settings Change Hooks
|
||||
|
||||
**File:** `apps/backend/src/Header/config.ts` and `Footer/config.ts`
|
||||
|
||||
```typescript
|
||||
import { createAuditLog } from '../utilities/auditLogger'
|
||||
|
||||
export const Header: GlobalConfig = {
|
||||
slug: 'header',
|
||||
hooks: {
|
||||
afterChange: [
|
||||
async ({ doc, previousDoc, req }) => {
|
||||
if (!req.user) return
|
||||
|
||||
await createAuditLog({
|
||||
action: 'settings',
|
||||
collection: 'header',
|
||||
documentId: doc.id,
|
||||
before: previousDoc,
|
||||
after: doc,
|
||||
req,
|
||||
})
|
||||
},
|
||||
],
|
||||
},
|
||||
// ... rest of config ...
|
||||
}
|
||||
```
|
||||
|
||||
### Task 6: Implement Log Retention (90 Days)
|
||||
|
||||
**File:** `apps/backend/src/cron/cleanupAuditLogs.ts`
|
||||
|
||||
```typescript
|
||||
import { payload } from '@/payload'
|
||||
|
||||
/**
|
||||
* 定期清理 90 天前的審計日誌
|
||||
* 應該通過 cron job 每天執行
|
||||
*/
|
||||
export async function cleanupOldAuditLogs(): Promise<void> {
|
||||
const ninetyDaysAgo = new Date()
|
||||
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)
|
||||
|
||||
try {
|
||||
const result = await payload.delete({
|
||||
collection: 'audit',
|
||||
where: {
|
||||
timestamp: {
|
||||
less_than: ninetyDaysAgo,
|
||||
},
|
||||
},
|
||||
})
|
||||
console.log(`Cleaned up ${result.deleted} old audit logs`)
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup audit logs:', error)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Configure in payload.config.ts jobs:**
|
||||
|
||||
```typescript
|
||||
jobs: {
|
||||
tasks: [
|
||||
{
|
||||
cron: '0 2 * * *', // Run daily at 2 AM
|
||||
handler: async () => {
|
||||
const { cleanupOldAuditLogs } = await import('./src/cron/cleanupAuditLogs')
|
||||
await cleanupOldAuditLogs()
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### File Structure
|
||||
|
||||
```
|
||||
apps/backend/src/
|
||||
├── collections/
|
||||
│ └── Audit/
|
||||
│ └── index.ts ← CREATE
|
||||
├── utilities/
|
||||
│ └── auditLogger.ts ← CREATE
|
||||
├── cron/
|
||||
│ └── cleanupAuditLogs.ts ← CREATE
|
||||
├── collections/
|
||||
│ ├── Users/index.ts ← MODIFY (add login/logout hooks)
|
||||
│ ├── Posts/index.ts ← MODIFY (add audit hooks)
|
||||
│ ├── Pages/index.ts ← MODIFY (add audit hooks)
|
||||
│ ├── Categories.ts ← MODIFY (add audit hooks)
|
||||
│ └── Portfolio/index.ts ← MODIFY (add audit hooks)
|
||||
├── Header/
|
||||
│ └── config.ts ← MODIFY (add audit hooks)
|
||||
├── Footer/
|
||||
│ └── config.ts ← MODIFY (add audit hooks)
|
||||
└── payload.config.ts ← MODIFY (register Audit, add cron)
|
||||
```
|
||||
|
||||
## Tasks / Subtasks
|
||||
|
||||
### Part 1: Audit Collection
|
||||
- [ ] **Task 1.1**: Create Audit collection
|
||||
- [ ] Create Audit/index.ts with all fields
|
||||
- [ ] Configure access control (admin only)
|
||||
- [ ] Register in payload.config.ts
|
||||
|
||||
### Part 2: Logging Utility
|
||||
- [ ] **Task 2.1**: Create auditLogger utility
|
||||
- [ ] Create auditLogger.ts
|
||||
- [ ] Implement createAuditLog function
|
||||
- [ ] Add error handling
|
||||
|
||||
### Part 3: Authentication Logging
|
||||
- [ ] **Task 3.1**: Add login hook to Users
|
||||
- [ ] **Task 3.2**: Add logout hook to Users
|
||||
|
||||
### Part 4: Content Change Logging
|
||||
- [ ] **Task 4.1**: Add audit hooks to Posts
|
||||
- [ ] **Task 4.2**: Add audit hooks to Pages
|
||||
- [ ] **Task 4.3**: Add audit hooks to Categories
|
||||
- [ ] **Task 4.4**: Add audit hooks to Portfolio
|
||||
|
||||
### Part 5: Settings Logging
|
||||
- [ ] **Task 5.1**: Add audit hooks to Header
|
||||
- [ ] **Task 5.2**: Add audit hooks to Footer
|
||||
|
||||
### Part 6: Log Retention
|
||||
- [ ] **Task 6.1**: Create cleanupAuditLogs function
|
||||
- [ ] **Task 6.2**: Configure cron job in payload.config.ts
|
||||
|
||||
### Part 7: Testing
|
||||
- [ ] **Task 7.1**: Verify TypeScript types
|
||||
- [ ] **Task 7.2**: Test login/logout logging
|
||||
- [ ] **Task 7.3**: Test content change logging
|
||||
- [ ] **Task 7.4**: Test settings change logging
|
||||
- [ ] **Task 7.5**: Test admin-only access
|
||||
- [ ] **Task 7.6**: Test 90-day cleanup
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
```typescript
|
||||
// apps/backend/src/utilities/__tests__/auditLogger.spec.ts
|
||||
import { createAuditLog } from '../auditLogger'
|
||||
|
||||
describe('Audit Logger', () => {
|
||||
it('should create audit log entry', async () => {
|
||||
// Test implementation
|
||||
})
|
||||
|
||||
it('should handle errors gracefully', async () => {
|
||||
// Test implementation
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] Login creates audit log
|
||||
- [ ] Logout creates audit log
|
||||
- [ ] Creating post creates audit log
|
||||
- [ ] Updating post creates audit log with before/after
|
||||
- [ ] Deleting post creates audit log with before data
|
||||
- [ ] Updating Header creates audit log
|
||||
- [ ] Non-admin users cannot access Audit collection
|
||||
- [ ] Admin users can view Audit collection
|
||||
- [ ] Audit logs show correct user attribution
|
||||
- [ ] IP addresses are captured
|
||||
- [ ] User agents are captured
|
||||
- [ ] Old logs are cleaned up (manual test of cleanup function)
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
| Risk | Probability | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
| Performance impact | Medium | Medium | Async logging, don't block main operations |
|
||||
| Data growth | High | Low | 90-day retention policy |
|
||||
| Missing events | Low | Medium | Comprehensive hook coverage |
|
||||
| Privacy concerns | Low | Medium | Admin-only access, no sensitive data in logs |
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Audit collection created and registered
|
||||
- [ ] Audit logger utility created
|
||||
- [ ] Login/logout logging implemented
|
||||
- [ ] Content change logging implemented
|
||||
- [ ] Settings change logging implemented
|
||||
- [ ] 90-day retention cron job configured
|
||||
- [ ] TypeScript types generate successfully
|
||||
- [ ] All logging scenarios tested
|
||||
- [ ] Admin-only access verified
|
||||
- [ ] 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