Establish baseline for project documentation including BMAD specs, PRD, and system architecture notes.
13 KiB
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
- AC1 - Audit Collection Created: New Audit collection with fields for action, user, timestamp, collection, documentId, before/after data
- AC2 - Indexes Configured: Indexes on userId, action, timestamp for efficient querying
Part 2: Logging Hooks
- AC3 - Login/Logout Logging: Authentication events logged with user and timestamp
- AC4 - Content Changes Logging: create/update/delete operations logged with before/after values
- AC5 - Settings Logging: Global changes (Header/Footer) logged with user attribution
Part 3: Admin Interface
- AC6 - Audit Log Viewer: Admin-only view to browse and filter audit logs
- AC7 - 90-Day Retention: Auto-delete logs older than 90 days
Part 4: Testing
- AC8 - TypeScript Types: Running
pnpm buildregenerates payload-types.ts without errors - 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
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:
import { Audit } from './collections/Audit'
collections: [Pages, Posts, Media, Categories, Users, Audit],
Task 2: Create Logging Utility
File: apps/backend/src/utilities/auditLogger.ts
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
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
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
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
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:
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
// 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) |