/** * Migration: Add missing user audit and tracking fields * * Fixes: Missing important user fields (last_login_at, timezone, email_verified_at) * * @type {import('node-pg-migrate').ColumnDefinitions | undefined} */ export const shorthands = undefined; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const up = (pgm) => { console.log('Adding user audit and tracking fields...'); // Add fields to user_profiles pgm.addColumns('user_profiles', { last_login_at: { type: 'timestamptz', notNull: false, comment: 'Timestamp of last successful login', }, timezone: { type: 'varchar(50)', notNull: false, default: 'UTC', comment: 'User timezone preference (e.g., Asia/Shanghai)', }, locale: { type: 'varchar(10)', notNull: false, default: 'zh-CN', comment: 'User locale preference (e.g., en-US, zh-CN)', }, login_count: { type: 'integer', notNull: true, default: 0, comment: 'Total number of logins', }, }); // Add fields to user_identities pgm.addColumns('user_identities', { email_verified_at: { type: 'timestamptz', notNull: false, comment: 'Timestamp when email was verified', }, last_auth_at: { type: 'timestamptz', notNull: false, comment: 'Timestamp of last authentication attempt', }, auth_failure_count: { type: 'integer', notNull: true, default: 0, comment: 'Count of consecutive authentication failures', }, locked_until: { type: 'timestamptz', notNull: false, comment: 'Account locked until this timestamp (for brute-force protection)', }, password_changed_at: { type: 'timestamptz', notNull: false, comment: 'Timestamp when password was last changed', }, }); // Add CHECK constraint for auth_failure_count pgm.addConstraint('user_identities', 'chk_user_identities_auth_failure_count', { check: 'auth_failure_count >= 0', }); // Add CHECK constraint for login_count pgm.addConstraint('user_profiles', 'chk_user_profiles_login_count', { check: 'login_count >= 0', }); // Add index for locked accounts query pgm.createIndex('user_identities', 'locked_until', { name: 'idx_user_identities_locked_until', where: 'locked_until IS NOT NULL', }); // Add index for unverified emails pgm.createIndex('user_identities', 'email_verified_at', { name: 'idx_user_identities_email_unverified', where: "email IS NOT NULL AND email_verified_at IS NULL AND provider = 'email'", }); console.log('User audit and tracking fields added successfully.'); }; /** * @param pgm {import('node-pg-migrate').MigrationBuilder} * @param run {() => void | undefined} * @returns {Promise | void} */ export const down = (pgm) => { console.log('Removing user audit and tracking fields...'); // Drop indexes pgm.dropIndex('user_identities', 'locked_until', { name: 'idx_user_identities_locked_until', ifExists: true, }); pgm.dropIndex('user_identities', 'email_verified_at', { name: 'idx_user_identities_email_unverified', ifExists: true, }); // Drop constraints pgm.dropConstraint('user_identities', 'chk_user_identities_auth_failure_count', { ifExists: true, }); pgm.dropConstraint('user_profiles', 'chk_user_profiles_login_count', { ifExists: true, }); // Drop columns from user_identities pgm.dropColumns('user_identities', [ 'email_verified_at', 'last_auth_at', 'auth_failure_count', 'locked_until', 'password_changed_at', ]); // Drop columns from user_profiles pgm.dropColumns('user_profiles', ['last_login_at', 'timezone', 'locale', 'login_count']); console.log('User audit and tracking fields removed.'); };