Establish baseline for project documentation including BMAD specs, PRD, and system architecture notes.
491 lines
13 KiB
Markdown
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) |
|