-- ============================================================= -- ARCHIVO: 10-audit.sql -- DESCRIPCION: Sistema de audit trail, cambios de entidades, logs -- VERSION: 1.0.0 -- PROYECTO: ERP-Core V2 -- FECHA: 2026-01-10 -- EPIC: SAAS-AUDIT (EPIC-SAAS-004) -- HISTORIAS: US-050, US-051, US-052 -- ============================================================= -- ===================== -- SCHEMA: audit -- ===================== CREATE SCHEMA IF NOT EXISTS audit; -- ===================== -- TABLA: audit.audit_logs -- Log de auditoría general -- ===================== CREATE TABLE IF NOT EXISTS audit.audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Actor user_id UUID REFERENCES auth.users(id), user_email VARCHAR(255), user_name VARCHAR(200), session_id UUID, impersonator_id UUID REFERENCES auth.users(id), -- Si está siendo impersonado -- Acción action VARCHAR(50) NOT NULL, -- create, read, update, delete, login, logout, export, etc. action_category VARCHAR(50), -- data, auth, system, config, billing -- Recurso resource_type VARCHAR(100) NOT NULL, -- user, product, sale, branch, etc. resource_id UUID, resource_name VARCHAR(255), -- Cambios old_values JSONB, new_values JSONB, changed_fields TEXT[], -- Contexto ip_address INET, user_agent TEXT, device_info JSONB DEFAULT '{}', location JSONB DEFAULT '{}', -- {country, city, lat, lng} -- Request info request_id VARCHAR(100), request_method VARCHAR(10), request_path TEXT, request_params JSONB DEFAULT '{}', -- Resultado status VARCHAR(20) DEFAULT 'success', -- success, failure, partial error_message TEXT, duration_ms INTEGER, -- Metadata metadata JSONB DEFAULT '{}', tags TEXT[] DEFAULT '{}', -- Timestamp created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Particionar por fecha para mejor rendimiento (recomendado en producción) -- CREATE TABLE audit.audit_logs_y2026m01 PARTITION OF audit.audit_logs -- FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'); -- ===================== -- TABLA: audit.entity_changes -- Historial detallado de cambios por entidad -- ===================== CREATE TABLE IF NOT EXISTS audit.entity_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Entidad entity_type VARCHAR(100) NOT NULL, entity_id UUID NOT NULL, entity_name VARCHAR(255), -- Versión version INTEGER NOT NULL DEFAULT 1, previous_version INTEGER, -- Snapshot completo de la entidad data_snapshot JSONB NOT NULL, -- Cambios específicos changes JSONB DEFAULT '[]', -- Ejemplo: [{"field": "price", "old": 100, "new": 150, "type": "update"}] -- Actor changed_by UUID REFERENCES auth.users(id), change_reason TEXT, -- Tipo de cambio change_type VARCHAR(20) NOT NULL, -- create, update, delete, restore -- Timestamp changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Índice único para versiones CREATE UNIQUE INDEX IF NOT EXISTS idx_entity_version ON audit.entity_changes(entity_type, entity_id, version); -- ===================== -- TABLA: audit.sensitive_data_access -- Acceso a datos sensibles -- ===================== CREATE TABLE IF NOT EXISTS audit.sensitive_data_access ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Actor user_id UUID NOT NULL REFERENCES auth.users(id), session_id UUID, -- Datos accedidos data_type VARCHAR(100) NOT NULL, -- pii, financial, medical, credentials data_category VARCHAR(100), -- customer_data, employee_data, payment_info entity_type VARCHAR(100), entity_id UUID, -- Acción access_type VARCHAR(30) NOT NULL, -- view, export, modify, decrypt access_reason TEXT, -- Contexto ip_address INET, user_agent TEXT, -- Resultado was_authorized BOOLEAN DEFAULT TRUE, denial_reason TEXT, -- Timestamp accessed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: audit.data_exports -- Log de exportaciones de datos -- ===================== CREATE TABLE IF NOT EXISTS audit.data_exports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Actor user_id UUID NOT NULL REFERENCES auth.users(id), -- Exportación export_type VARCHAR(50) NOT NULL, -- report, backup, gdpr_request, bulk_export export_format VARCHAR(20), -- csv, xlsx, pdf, json entity_types TEXT[] NOT NULL, -- Filtros aplicados filters JSONB DEFAULT '{}', date_range_start TIMESTAMPTZ, date_range_end TIMESTAMPTZ, -- Resultado record_count INTEGER, file_size_bytes BIGINT, file_hash VARCHAR(64), -- SHA-256 del archivo -- Estado status VARCHAR(20) DEFAULT 'pending', -- pending, processing, completed, failed, expired -- Archivos download_url TEXT, download_expires_at TIMESTAMPTZ, download_count INTEGER DEFAULT 0, -- Contexto ip_address INET, user_agent TEXT, -- Timestamps requested_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMPTZ, expires_at TIMESTAMPTZ ); -- ===================== -- TABLA: audit.login_history -- Historial de inicios de sesión -- ===================== CREATE TABLE IF NOT EXISTS audit.login_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE, -- Identificación del usuario email VARCHAR(255), username VARCHAR(100), -- Resultado status VARCHAR(20) NOT NULL, -- success, failed, blocked, mfa_required, mfa_failed -- Método de autenticación auth_method VARCHAR(30), -- password, sso, oauth, mfa, magic_link, biometric oauth_provider VARCHAR(30), -- MFA mfa_method VARCHAR(20), -- totp, sms, email, push mfa_verified BOOLEAN, -- Dispositivo device_id UUID REFERENCES auth.devices(id), device_fingerprint VARCHAR(255), device_type VARCHAR(30), -- desktop, mobile, tablet device_os VARCHAR(50), device_browser VARCHAR(50), -- Ubicación ip_address INET, user_agent TEXT, country_code VARCHAR(2), city VARCHAR(100), latitude DECIMAL(10, 8), longitude DECIMAL(11, 8), -- Riesgo risk_score INTEGER, -- 0-100 risk_factors JSONB DEFAULT '[]', is_suspicious BOOLEAN DEFAULT FALSE, is_new_device BOOLEAN DEFAULT FALSE, is_new_location BOOLEAN DEFAULT FALSE, -- Error info failure_reason VARCHAR(100), failure_count INTEGER, -- Timestamp attempted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: audit.permission_changes -- Cambios en permisos y roles -- ===================== CREATE TABLE IF NOT EXISTS audit.permission_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Actor changed_by UUID NOT NULL REFERENCES auth.users(id), -- Usuario afectado target_user_id UUID NOT NULL REFERENCES auth.users(id), target_user_email VARCHAR(255), -- Tipo de cambio change_type VARCHAR(30) NOT NULL, -- role_assigned, role_revoked, permission_granted, permission_revoked -- Rol/Permiso role_id UUID, role_code VARCHAR(50), permission_id UUID, permission_code VARCHAR(100), -- Contexto branch_id UUID REFERENCES core.branches(id), scope VARCHAR(30), -- global, tenant, branch -- Valores anteriores previous_roles TEXT[], previous_permissions TEXT[], -- Razón reason TEXT, -- Timestamp changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- TABLA: audit.config_changes -- Cambios en configuración del sistema -- ===================== CREATE TABLE IF NOT EXISTS audit.config_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID REFERENCES auth.tenants(id) ON DELETE CASCADE, -- NULL = config global -- Actor changed_by UUID NOT NULL REFERENCES auth.users(id), -- Configuración config_type VARCHAR(50) NOT NULL, -- tenant_settings, user_settings, system_settings, feature_flags config_key VARCHAR(100) NOT NULL, config_path TEXT, -- Path jerárquico: billing.invoicing.prefix -- Valores old_value JSONB, new_value JSONB, -- Contexto reason TEXT, ticket_id VARCHAR(50), -- Referencia a ticket de soporte -- Timestamp changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- ===================== -- INDICES -- ===================== -- Indices para audit_logs CREATE INDEX IF NOT EXISTS idx_audit_logs_tenant ON audit.audit_logs(tenant_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_user ON audit.audit_logs(user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit.audit_logs(action); CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit.audit_logs(resource_type, resource_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit.audit_logs(created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_logs_category ON audit.audit_logs(action_category); CREATE INDEX IF NOT EXISTS idx_audit_logs_status ON audit.audit_logs(status) WHERE status = 'failure'; CREATE INDEX IF NOT EXISTS idx_audit_logs_tags ON audit.audit_logs USING GIN(tags); -- Indices para entity_changes CREATE INDEX IF NOT EXISTS idx_entity_changes_tenant ON audit.entity_changes(tenant_id); CREATE INDEX IF NOT EXISTS idx_entity_changes_entity ON audit.entity_changes(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_entity_changes_changed_by ON audit.entity_changes(changed_by); CREATE INDEX IF NOT EXISTS idx_entity_changes_date ON audit.entity_changes(changed_at DESC); -- Indices para sensitive_data_access CREATE INDEX IF NOT EXISTS idx_sensitive_access_tenant ON audit.sensitive_data_access(tenant_id); CREATE INDEX IF NOT EXISTS idx_sensitive_access_user ON audit.sensitive_data_access(user_id); CREATE INDEX IF NOT EXISTS idx_sensitive_access_type ON audit.sensitive_data_access(data_type); CREATE INDEX IF NOT EXISTS idx_sensitive_access_date ON audit.sensitive_data_access(accessed_at DESC); CREATE INDEX IF NOT EXISTS idx_sensitive_unauthorized ON audit.sensitive_data_access(was_authorized) WHERE was_authorized = FALSE; -- Indices para data_exports CREATE INDEX IF NOT EXISTS idx_exports_tenant ON audit.data_exports(tenant_id); CREATE INDEX IF NOT EXISTS idx_exports_user ON audit.data_exports(user_id); CREATE INDEX IF NOT EXISTS idx_exports_status ON audit.data_exports(status); CREATE INDEX IF NOT EXISTS idx_exports_date ON audit.data_exports(requested_at DESC); -- Indices para login_history CREATE INDEX IF NOT EXISTS idx_login_tenant ON audit.login_history(tenant_id); CREATE INDEX IF NOT EXISTS idx_login_user ON audit.login_history(user_id); CREATE INDEX IF NOT EXISTS idx_login_status ON audit.login_history(status); CREATE INDEX IF NOT EXISTS idx_login_date ON audit.login_history(attempted_at DESC); CREATE INDEX IF NOT EXISTS idx_login_ip ON audit.login_history(ip_address); CREATE INDEX IF NOT EXISTS idx_login_suspicious ON audit.login_history(is_suspicious) WHERE is_suspicious = TRUE; CREATE INDEX IF NOT EXISTS idx_login_failed ON audit.login_history(status, email) WHERE status = 'failed'; -- Indices para permission_changes CREATE INDEX IF NOT EXISTS idx_perm_changes_tenant ON audit.permission_changes(tenant_id); CREATE INDEX IF NOT EXISTS idx_perm_changes_target ON audit.permission_changes(target_user_id); CREATE INDEX IF NOT EXISTS idx_perm_changes_date ON audit.permission_changes(changed_at DESC); -- Indices para config_changes CREATE INDEX IF NOT EXISTS idx_config_changes_tenant ON audit.config_changes(tenant_id); CREATE INDEX IF NOT EXISTS idx_config_changes_type ON audit.config_changes(config_type); CREATE INDEX IF NOT EXISTS idx_config_changes_date ON audit.config_changes(changed_at DESC); -- ===================== -- RLS POLICIES -- ===================== ALTER TABLE audit.audit_logs ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_audit_logs ON audit.audit_logs USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.entity_changes ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_entity_changes ON audit.entity_changes USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.sensitive_data_access ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_sensitive ON audit.sensitive_data_access USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.data_exports ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_exports ON audit.data_exports USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.login_history ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_login ON audit.login_history USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.permission_changes ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_perm_changes ON audit.permission_changes USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE audit.config_changes ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_config_changes ON audit.config_changes USING ( tenant_id IS NULL OR tenant_id = current_setting('app.current_tenant_id', true)::uuid ); -- ===================== -- FUNCIONES -- ===================== -- Función para registrar log de auditoría CREATE OR REPLACE FUNCTION audit.log( p_tenant_id UUID, p_user_id UUID, p_action VARCHAR(50), p_resource_type VARCHAR(100), p_resource_id UUID DEFAULT NULL, p_old_values JSONB DEFAULT NULL, p_new_values JSONB DEFAULT NULL, p_metadata JSONB DEFAULT '{}' ) RETURNS UUID AS $$ DECLARE v_log_id UUID; v_changed_fields TEXT[]; BEGIN -- Calcular campos cambiados IF p_old_values IS NOT NULL AND p_new_values IS NOT NULL THEN SELECT ARRAY_AGG(key) INTO v_changed_fields FROM ( SELECT key FROM jsonb_object_keys(p_old_values) AS key WHERE p_old_values->key IS DISTINCT FROM p_new_values->key UNION SELECT key FROM jsonb_object_keys(p_new_values) AS key WHERE NOT p_old_values ? key ) AS changed; END IF; INSERT INTO audit.audit_logs ( tenant_id, user_id, action, resource_type, resource_id, old_values, new_values, changed_fields, metadata ) VALUES ( p_tenant_id, p_user_id, p_action, p_resource_type, p_resource_id, p_old_values, p_new_values, v_changed_fields, p_metadata ) RETURNING id INTO v_log_id; RETURN v_log_id; END; $$ LANGUAGE plpgsql; -- Función para registrar cambio de entidad con versionamiento CREATE OR REPLACE FUNCTION audit.log_entity_change( p_tenant_id UUID, p_entity_type VARCHAR(100), p_entity_id UUID, p_data_snapshot JSONB, p_changes JSONB DEFAULT '[]', p_changed_by UUID DEFAULT NULL, p_change_type VARCHAR(20) DEFAULT 'update', p_change_reason TEXT DEFAULT NULL ) RETURNS INTEGER AS $$ DECLARE v_version INTEGER; v_prev_version INTEGER; BEGIN -- Obtener versión actual SELECT COALESCE(MAX(version), 0) INTO v_prev_version FROM audit.entity_changes WHERE entity_type = p_entity_type AND entity_id = p_entity_id; v_version := v_prev_version + 1; INSERT INTO audit.entity_changes ( tenant_id, entity_type, entity_id, version, previous_version, data_snapshot, changes, changed_by, change_type, change_reason ) VALUES ( p_tenant_id, p_entity_type, p_entity_id, v_version, v_prev_version, p_data_snapshot, p_changes, p_changed_by, p_change_type, p_change_reason ); RETURN v_version; END; $$ LANGUAGE plpgsql; -- Función para obtener historial de una entidad CREATE OR REPLACE FUNCTION audit.get_entity_history( p_entity_type VARCHAR(100), p_entity_id UUID, p_limit INTEGER DEFAULT 50 ) RETURNS TABLE ( version INTEGER, change_type VARCHAR(20), data_snapshot JSONB, changes JSONB, changed_by UUID, change_reason TEXT, changed_at TIMESTAMPTZ ) AS $$ BEGIN RETURN QUERY SELECT ec.version, ec.change_type, ec.data_snapshot, ec.changes, ec.changed_by, ec.change_reason, ec.changed_at FROM audit.entity_changes ec WHERE ec.entity_type = p_entity_type AND ec.entity_id = p_entity_id ORDER BY ec.version DESC LIMIT p_limit; END; $$ LANGUAGE plpgsql STABLE; -- Función para obtener snapshot de una entidad en un momento dado CREATE OR REPLACE FUNCTION audit.get_entity_at_time( p_entity_type VARCHAR(100), p_entity_id UUID, p_at_time TIMESTAMPTZ ) RETURNS JSONB AS $$ BEGIN RETURN ( SELECT data_snapshot FROM audit.entity_changes WHERE entity_type = p_entity_type AND entity_id = p_entity_id AND changed_at <= p_at_time ORDER BY changed_at DESC LIMIT 1 ); END; $$ LANGUAGE plpgsql STABLE; -- Función para registrar acceso a datos sensibles CREATE OR REPLACE FUNCTION audit.log_sensitive_access( p_tenant_id UUID, p_user_id UUID, p_data_type VARCHAR(100), p_access_type VARCHAR(30), p_entity_type VARCHAR(100) DEFAULT NULL, p_entity_id UUID DEFAULT NULL, p_was_authorized BOOLEAN DEFAULT TRUE, p_access_reason TEXT DEFAULT NULL ) RETURNS UUID AS $$ DECLARE v_access_id UUID; BEGIN INSERT INTO audit.sensitive_data_access ( tenant_id, user_id, data_type, access_type, entity_type, entity_id, was_authorized, access_reason ) VALUES ( p_tenant_id, p_user_id, p_data_type, p_access_type, p_entity_type, p_entity_id, p_was_authorized, p_access_reason ) RETURNING id INTO v_access_id; RETURN v_access_id; END; $$ LANGUAGE plpgsql; -- Función para registrar login CREATE OR REPLACE FUNCTION audit.log_login( p_user_id UUID, p_tenant_id UUID, p_email VARCHAR(255), p_status VARCHAR(20), p_auth_method VARCHAR(30) DEFAULT 'password', p_ip_address INET DEFAULT NULL, p_user_agent TEXT DEFAULT NULL, p_device_info JSONB DEFAULT '{}' ) RETURNS UUID AS $$ DECLARE v_login_id UUID; v_is_new_device BOOLEAN := FALSE; v_is_new_location BOOLEAN := FALSE; v_failure_count INTEGER := 0; BEGIN -- Verificar si es dispositivo nuevo IF p_device_info->>'fingerprint' IS NOT NULL THEN SELECT NOT EXISTS ( SELECT 1 FROM audit.login_history WHERE user_id = p_user_id AND device_fingerprint = p_device_info->>'fingerprint' AND status = 'success' ) INTO v_is_new_device; END IF; -- Contar intentos fallidos recientes IF p_status = 'failed' THEN SELECT COUNT(*) INTO v_failure_count FROM audit.login_history WHERE email = p_email AND status = 'failed' AND attempted_at > CURRENT_TIMESTAMP - INTERVAL '1 hour'; END IF; INSERT INTO audit.login_history ( user_id, tenant_id, email, status, auth_method, ip_address, user_agent, device_fingerprint, device_type, device_os, device_browser, is_new_device, failure_count ) VALUES ( p_user_id, p_tenant_id, p_email, p_status, p_auth_method, p_ip_address, p_user_agent, p_device_info->>'fingerprint', p_device_info->>'type', p_device_info->>'os', p_device_info->>'browser', v_is_new_device, v_failure_count ) RETURNING id INTO v_login_id; RETURN v_login_id; END; $$ LANGUAGE plpgsql; -- Función para obtener estadísticas de auditoría CREATE OR REPLACE FUNCTION audit.get_stats( p_tenant_id UUID, p_days INTEGER DEFAULT 30 ) RETURNS TABLE ( total_actions BIGINT, unique_users BIGINT, actions_by_category JSONB, actions_by_day JSONB, top_resources JSONB, failed_actions BIGINT ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*) as total_actions, COUNT(DISTINCT al.user_id) as unique_users, jsonb_object_agg(COALESCE(al.action_category, 'other'), cat_count) as actions_by_category, jsonb_object_agg(day_date, day_count) as actions_by_day, jsonb_agg(DISTINCT jsonb_build_object('type', al.resource_type, 'count', res_count)) as top_resources, COUNT(*) FILTER (WHERE al.status = 'failure') as failed_actions FROM audit.audit_logs al LEFT JOIN ( SELECT action_category, COUNT(*) as cat_count FROM audit.audit_logs WHERE tenant_id = p_tenant_id AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY action_category ) cat ON cat.action_category = al.action_category LEFT JOIN ( SELECT DATE(created_at) as day_date, COUNT(*) as day_count FROM audit.audit_logs WHERE tenant_id = p_tenant_id AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY DATE(created_at) ) days ON TRUE LEFT JOIN ( SELECT resource_type, COUNT(*) as res_count FROM audit.audit_logs WHERE tenant_id = p_tenant_id AND created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL GROUP BY resource_type ORDER BY res_count DESC LIMIT 10 ) res ON res.resource_type = al.resource_type WHERE al.tenant_id = p_tenant_id AND al.created_at > CURRENT_TIMESTAMP - (p_days || ' days')::INTERVAL LIMIT 1; END; $$ LANGUAGE plpgsql STABLE; -- Función para limpiar logs antiguos CREATE OR REPLACE FUNCTION audit.cleanup_old_logs( p_audit_days INTEGER DEFAULT 365, p_login_days INTEGER DEFAULT 90, p_export_days INTEGER DEFAULT 30 ) RETURNS TABLE ( audit_deleted INTEGER, login_deleted INTEGER, export_deleted INTEGER ) AS $$ DECLARE v_audit INTEGER; v_login INTEGER; v_export INTEGER; BEGIN -- Limpiar audit_logs DELETE FROM audit.audit_logs WHERE created_at < CURRENT_TIMESTAMP - (p_audit_days || ' days')::INTERVAL; GET DIAGNOSTICS v_audit = ROW_COUNT; -- Limpiar login_history DELETE FROM audit.login_history WHERE attempted_at < CURRENT_TIMESTAMP - (p_login_days || ' days')::INTERVAL; GET DIAGNOSTICS v_login = ROW_COUNT; -- Limpiar data_exports completados/expirados DELETE FROM audit.data_exports WHERE (status IN ('completed', 'expired', 'failed')) AND requested_at < CURRENT_TIMESTAMP - (p_export_days || ' days')::INTERVAL; GET DIAGNOSTICS v_export = ROW_COUNT; RETURN QUERY SELECT v_audit, v_login, v_export; END; $$ LANGUAGE plpgsql; -- ===================== -- TRIGGER GENÉRICO PARA AUDITORÍA -- ===================== -- Función trigger para auditoría automática CREATE OR REPLACE FUNCTION audit.audit_trigger_func() RETURNS TRIGGER AS $$ DECLARE v_old_data JSONB; v_new_data JSONB; v_tenant_id UUID; v_user_id UUID; BEGIN -- Obtener tenant_id y user_id del contexto v_tenant_id := current_setting('app.current_tenant_id', true)::uuid; v_user_id := current_setting('app.current_user_id', true)::uuid; IF TG_OP = 'INSERT' THEN v_new_data := to_jsonb(NEW); PERFORM audit.log_entity_change( v_tenant_id, TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, (v_new_data->>'id')::uuid, v_new_data, '[]'::jsonb, v_user_id, 'create' ); RETURN NEW; ELSIF TG_OP = 'UPDATE' THEN v_old_data := to_jsonb(OLD); v_new_data := to_jsonb(NEW); -- Solo registrar si hay cambios reales IF v_old_data IS DISTINCT FROM v_new_data THEN PERFORM audit.log_entity_change( v_tenant_id, TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, (v_new_data->>'id')::uuid, v_new_data, jsonb_build_array(jsonb_build_object( 'old', v_old_data, 'new', v_new_data )), v_user_id, 'update' ); END IF; RETURN NEW; ELSIF TG_OP = 'DELETE' THEN v_old_data := to_jsonb(OLD); PERFORM audit.log_entity_change( v_tenant_id, TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME, (v_old_data->>'id')::uuid, v_old_data, '[]'::jsonb, v_user_id, 'delete' ); RETURN OLD; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; -- ===================== -- COMENTARIOS -- ===================== COMMENT ON TABLE audit.audit_logs IS 'Log de auditoría general para todas las acciones'; COMMENT ON TABLE audit.entity_changes IS 'Historial de cambios con versionamiento por entidad'; COMMENT ON TABLE audit.sensitive_data_access IS 'Log de acceso a datos sensibles'; COMMENT ON TABLE audit.data_exports IS 'Registro de exportaciones de datos'; COMMENT ON TABLE audit.login_history IS 'Historial de intentos de inicio de sesión'; COMMENT ON TABLE audit.permission_changes IS 'Log de cambios en permisos y roles'; COMMENT ON TABLE audit.config_changes IS 'Log de cambios en configuración del sistema'; COMMENT ON FUNCTION audit.log IS 'Registra una acción en el log de auditoría'; COMMENT ON FUNCTION audit.log_entity_change IS 'Registra un cambio versionado de una entidad'; COMMENT ON FUNCTION audit.get_entity_history IS 'Obtiene el historial de cambios de una entidad'; COMMENT ON FUNCTION audit.get_entity_at_time IS 'Obtiene el snapshot de una entidad en un momento específico'; COMMENT ON FUNCTION audit.log_login IS 'Registra un intento de inicio de sesión'; COMMENT ON FUNCTION audit.audit_trigger_func IS 'Función trigger para auditoría automática de tablas';