From f91c4492464fba7596295951fbd87a3dc581a44c Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Sat, 21 Mar 2026 16:56:20 -0400 Subject: [PATCH] fix: add missing ip_address/user_agent columns to audit_log for pre-3.7 databases Pre-3.7 SQLite databases may lack ip_address and user_agent columns on audit_log. Migration 012 added username but missed these two, causing "table audit_log has no column named user_agent" errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/db/migrations.test.ts | 12 +-- src/db/migrations.ts | 17 ++- .../013_add_audit_log_missing_columns.ts | 101 ++++++++++++++++++ 3 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 src/server/migrations/013_add_audit_log_missing_columns.ts diff --git a/src/db/migrations.test.ts b/src/db/migrations.test.ts index 2bd14303b..22d21913a 100644 --- a/src/db/migrations.test.ts +++ b/src/db/migrations.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { registry } from './migrations.js'; describe('migrations registry', () => { - it('has all 12 migrations registered', () => { - expect(registry.count()).toBe(12); + it('has all 13 migrations registered', () => { + expect(registry.count()).toBe(13); }); it('first migration is v37 baseline', () => { @@ -12,11 +12,11 @@ describe('migrations registry', () => { expect(all[0].name).toContain('v37_baseline'); }); - it('last migration is the auth schema alignment', () => { + it('last migration is the audit_log missing columns fix', () => { const all = registry.getAll(); const last = all[all.length - 1]; - expect(last.number).toBe(12); - expect(last.name).toContain('auth'); + expect(last.number).toBe(13); + expect(last.name).toContain('audit_log'); }); it('migrations are sequentially numbered from 1 to 12', () => { @@ -46,7 +46,7 @@ describe('migrations registry', () => { } }); - it('migrations 002-012 all have settingsKey', () => { + it('migrations 002-013 all have settingsKey', () => { const all = registry.getAll(); for (let i = 1; i < all.length; i++) { expect(all[i].settingsKey, `Migration ${all[i].number} should have settingsKey`).toBeTruthy(); diff --git a/src/db/migrations.ts b/src/db/migrations.ts index 6be4ef272..5360785e0 100644 --- a/src/db/migrations.ts +++ b/src/db/migrations.ts @@ -1,7 +1,7 @@ /** * Migration Registry Barrel File * - * Registers all 12 migrations in sequential order for use by the migration runner. + * Registers all 13 migrations in sequential order for use by the migration runner. * Migration 001 is the v3.7 baseline (selfIdempotent — handles its own detection). * Migrations 002-011 were originally 078-087 and retain their original settingsKeys * for upgrade compatibility. @@ -24,6 +24,7 @@ import { migration as fixCustomThemesColumnsMigration, runMigration085Postgres, import { runMigration086Sqlite, runMigration086Postgres, runMigration086Mysql } from '../server/migrations/010_add_auto_distance_delete_log.js'; import { migration as fixMessageNodeNumBigintMigration, runMigration087Postgres, runMigration087Mysql } from '../server/migrations/011_fix_message_nodenum_bigint.js'; import { migration as authAlignMigration, runMigration012Postgres, runMigration012Mysql } from '../server/migrations/012_align_sqlite_auth_schema.js'; +import { migration as auditLogColumnsMigration, runMigration013Postgres, runMigration013Mysql } from '../server/migrations/013_add_audit_log_missing_columns.js'; // ============================================================================ // Registry @@ -154,3 +155,17 @@ registry.register({ postgres: (client) => runMigration012Postgres(client), mysql: (pool) => runMigration012Mysql(pool), }); + +// --------------------------------------------------------------------------- +// Migration 013: Add missing ip_address/user_agent columns to audit_log +// Pre-3.7 SQLite databases may lack these columns. +// --------------------------------------------------------------------------- + +registry.register({ + number: 13, + name: 'add_audit_log_missing_columns', + settingsKey: 'migration_013_add_audit_log_missing_columns', + sqlite: (db) => auditLogColumnsMigration.up(db), + postgres: (client) => runMigration013Postgres(client), + mysql: (pool) => runMigration013Mysql(pool), +}); diff --git a/src/server/migrations/013_add_audit_log_missing_columns.ts b/src/server/migrations/013_add_audit_log_missing_columns.ts new file mode 100644 index 000000000..7f4dafed6 --- /dev/null +++ b/src/server/migrations/013_add_audit_log_missing_columns.ts @@ -0,0 +1,101 @@ +/** + * Migration 013: Add missing columns to audit_log for pre-3.7 SQLite databases + * + * The Drizzle schema expects ip_address and user_agent columns on audit_log, + * but databases created before the v3.7 baseline may not have them. + * Migration 012 added username but missed these two. + * + * PostgreSQL/MySQL baselines already include these columns, so those are no-ops. + */ +import type { Database } from 'better-sqlite3'; +import { logger } from '../../utils/logger.js'; + +// ============ SQLite ============ + +export const migration = { + up: (db: Database): void => { + logger.info('Running migration 013 (SQLite): Adding missing audit_log columns...'); + + // 1. Add ip_address to audit_log + try { + db.exec('ALTER TABLE audit_log ADD COLUMN ip_address TEXT'); + logger.debug('Added ip_address column to audit_log'); + } catch (e: any) { + if (e.message?.includes('duplicate column')) { + logger.debug('audit_log.ip_address already exists, skipping'); + } else { + logger.warn('Could not add ip_address to audit_log:', e.message); + } + } + + // 2. Add user_agent to audit_log + try { + db.exec('ALTER TABLE audit_log ADD COLUMN user_agent TEXT'); + logger.debug('Added user_agent column to audit_log'); + } catch (e: any) { + if (e.message?.includes('duplicate column')) { + logger.debug('audit_log.user_agent already exists, skipping'); + } else { + logger.warn('Could not add user_agent to audit_log:', e.message); + } + } + + logger.info('Migration 013 complete (SQLite): audit_log columns aligned'); + }, + + down: (_db: Database): void => { + logger.debug('Migration 013 down: Not implemented (destructive column drops)'); + } +}; + +// ============ PostgreSQL ============ + +export async function runMigration013Postgres(client: import('pg').PoolClient): Promise { + logger.info('Running migration 013 (PostgreSQL): Ensuring audit_log columns exist...'); + + try { + await client.query('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS "ipAddress" TEXT'); + await client.query('ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS "userAgent" TEXT'); + logger.debug('Ensured ipAddress/userAgent exist on audit_log'); + } catch (error: any) { + logger.error('Migration 013 (PostgreSQL) failed:', error.message); + throw error; + } + + logger.info('Migration 013 complete (PostgreSQL): audit_log columns aligned'); +} + +// ============ MySQL ============ + +export async function runMigration013Mysql(pool: import('mysql2/promise').Pool): Promise { + logger.info('Running migration 013 (MySQL): Ensuring audit_log columns exist...'); + + try { + const [ipRows] = await pool.query(` + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_log' AND COLUMN_NAME = 'ipAddress' + `); + if (!Array.isArray(ipRows) || ipRows.length === 0) { + await pool.query('ALTER TABLE audit_log ADD COLUMN ipAddress TEXT'); + logger.debug('Added ipAddress to audit_log'); + } else { + logger.debug('audit_log.ipAddress already exists, skipping'); + } + + const [uaRows] = await pool.query(` + SELECT COLUMN_NAME FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'audit_log' AND COLUMN_NAME = 'userAgent' + `); + if (!Array.isArray(uaRows) || uaRows.length === 0) { + await pool.query('ALTER TABLE audit_log ADD COLUMN userAgent TEXT'); + logger.debug('Added userAgent to audit_log'); + } else { + logger.debug('audit_log.userAgent already exists, skipping'); + } + } catch (error: any) { + logger.error('Migration 013 (MySQL) failed:', error.message); + throw error; + } + + logger.info('Migration 013 complete (MySQL): audit_log columns aligned'); +}