Files
website-enchun-mgr/_bmad-output/implementation-artifacts/1-12-audit-logging.story.md
pkupuk e9897388dc docs: separate documentation and specs into initial commit
Establish baseline for project documentation including BMAD specs, PRD, and system architecture notes.
2026-02-11 11:49:49 +08:00

491 lines
13 KiB
Markdown

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