# 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 after?: Record req: PayloadRequest } /** * 創建審計日誌記錄 */ export async function createAuditLog(options: AuditLogOptions): Promise { 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 { 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) |