-- ============================================================================ -- Audit Logs Migration -- Add audit logging table to track user actions and system changes -- ============================================================================ -- ============================================================================ -- ENUM TYPE: audit_action -- ============================================================================ CREATE TYPE audit_action AS ENUM ( 'CREATE', 'UPDATE', 'DELETE', 'LOGIN', 'LOGOUT', 'READ', 'EXPORT', 'BULK_UPLOAD', 'STATUS_CHANGE', 'PERMISSION_CHANGE' ); -- ============================================================================ -- TABLE: audit_logs -- ============================================================================ CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- User information user_id UUID REFERENCES users(id) ON DELETE SET NULL, user_email VARCHAR(255) NOT NULL, user_name VARCHAR(255) NOT NULL, -- Action details action audit_action NOT NULL, table_name VARCHAR(100) NOT NULL, record_id UUID, -- Change tracking old_values JSONB, new_values JSONB, description TEXT, -- Request metadata ip_address INET, user_agent TEXT, -- Status success BOOLEAN NOT NULL DEFAULT TRUE, error_message TEXT, -- Timestamp created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- ============================================================================ -- INDEXES -- ============================================================================ CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id); CREATE INDEX idx_audit_logs_action ON audit_logs(action); CREATE INDEX idx_audit_logs_table_name ON audit_logs(table_name); CREATE INDEX idx_audit_logs_record_id ON audit_logs(record_id); CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at DESC); CREATE INDEX idx_audit_logs_user_id_created_at ON audit_logs(user_id, created_at DESC); CREATE INDEX idx_audit_logs_table_name_record_id ON audit_logs(table_name, record_id); -- Index for JSON queries on old_values and new_values CREATE INDEX idx_audit_logs_old_values ON audit_logs USING GIN (old_values); CREATE INDEX idx_audit_logs_new_values ON audit_logs USING GIN (new_values); -- ============================================================================ -- COMMENTS -- ============================================================================ COMMENT ON TABLE audit_logs IS 'System audit log tracking all user actions and data changes'; COMMENT ON COLUMN audit_logs.user_id IS 'Reference to user who performed the action (nullable if user deleted)'; COMMENT ON COLUMN audit_logs.user_email IS 'Email snapshot at time of action'; COMMENT ON COLUMN audit_logs.user_name IS 'Name snapshot at time of action'; COMMENT ON COLUMN audit_logs.action IS 'Type of action performed'; COMMENT ON COLUMN audit_logs.table_name IS 'Database table affected by the action'; COMMENT ON COLUMN audit_logs.record_id IS 'ID of the specific record affected'; COMMENT ON COLUMN audit_logs.old_values IS 'JSON snapshot of values before change'; COMMENT ON COLUMN audit_logs.new_values IS 'JSON snapshot of values after change'; COMMENT ON COLUMN audit_logs.description IS 'Human-readable description of the action'; COMMENT ON COLUMN audit_logs.ip_address IS 'IP address of the user'; COMMENT ON COLUMN audit_logs.user_agent IS 'Browser/client user agent string'; COMMENT ON COLUMN audit_logs.success IS 'Whether the action completed successfully'; COMMENT ON COLUMN audit_logs.error_message IS 'Error message if action failed'; -- ============================================================================ -- HELPER FUNCTION: Get current user info from request context -- ============================================================================ CREATE OR REPLACE FUNCTION get_current_user_info() RETURNS TABLE ( user_id UUID, user_email VARCHAR(255), user_name VARCHAR(255) ) AS $$ BEGIN -- This will be called from application code with current_setting RETURN QUERY SELECT NULLIF(current_setting('app.current_user_id', true), '')::UUID, NULLIF(current_setting('app.current_user_email', true), ''), NULLIF(current_setting('app.current_user_name', true), ''); END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- ============================================================================ -- HELPER FUNCTION: Log audit entry -- ============================================================================ CREATE OR REPLACE FUNCTION log_audit( p_user_id UUID, p_user_email VARCHAR(255), p_user_name VARCHAR(255), p_action audit_action, p_table_name VARCHAR(100), p_record_id UUID DEFAULT NULL, p_old_values JSONB DEFAULT NULL, p_new_values JSONB DEFAULT NULL, p_description TEXT DEFAULT NULL, p_ip_address INET DEFAULT NULL, p_user_agent TEXT DEFAULT NULL, p_success BOOLEAN DEFAULT TRUE, p_error_message TEXT DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_log_id UUID; BEGIN INSERT INTO audit_logs ( user_id, user_email, user_name, action, table_name, record_id, old_values, new_values, description, ip_address, user_agent, success, error_message ) VALUES ( p_user_id, p_user_email, p_user_name, p_action, p_table_name, p_record_id, p_old_values, p_new_values, p_description, p_ip_address, p_user_agent, p_success, p_error_message ) RETURNING id INTO v_log_id; RETURN v_log_id; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -- ============================================================================ -- VIEW: audit_logs_summary -- ============================================================================ CREATE OR REPLACE VIEW audit_logs_summary AS SELECT al.id, al.user_email, al.user_name, al.action, al.table_name, al.record_id, al.description, al.success, al.created_at, al.ip_address, -- User reference (may be null if user deleted) u.id AS current_user_id, u.is_active AS user_is_active FROM audit_logs al LEFT JOIN users u ON al.user_id = u.id ORDER BY al.created_at DESC; COMMENT ON VIEW audit_logs_summary IS 'Audit logs with user status information'; -- ============================================================================ -- VIEW: audit_statistics -- ============================================================================ CREATE OR REPLACE VIEW audit_statistics AS SELECT DATE(created_at) AS date, action, table_name, COUNT(*) AS action_count, COUNT(DISTINCT user_id) AS unique_users, SUM(CASE WHEN success THEN 1 ELSE 0 END) AS successful_actions, SUM(CASE WHEN NOT success THEN 1 ELSE 0 END) AS failed_actions FROM audit_logs GROUP BY DATE(created_at), action, table_name ORDER BY date DESC, action_count DESC; COMMENT ON VIEW audit_statistics IS 'Daily statistics of audit log actions'; -- ============================================================================ -- END OF MIGRATION -- ============================================================================