[GAP-006,007] feat: Add DDL for Digital Signatures and Audit Triggers
GAP-006: Digital Signatures - documents.signature_type ENUM - documents.signature_status ENUM - documents.digital_signatures table with RLS GAP-007: Audit Triggers - audit schema - audit.change_log table - audit.log_changes() trigger function - Triggers on 13 critical tables Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8a9db6f9d1
commit
efce73bdbd
@ -905,6 +905,125 @@ CREATE INDEX IF NOT EXISTS idx_documents_parent ON documents.documents(parent_do
|
|||||||
CREATE INDEX IF NOT EXISTS idx_document_shares_shared_by ON documents.document_shares(shared_by_id);
|
CREATE INDEX IF NOT EXISTS idx_document_shares_shared_by ON documents.document_shares(shared_by_id);
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
-- FIN DEL SCRIPT
|
-- GAP-006: FIRMAS DIGITALES
|
||||||
-- FK constraints adicionales: 4
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Tipo de firma digital
|
||||||
|
CREATE TYPE documents.signature_type AS ENUM (
|
||||||
|
'simple', -- Firma simple (click to sign)
|
||||||
|
'advanced', -- Firma avanzada (con certificado)
|
||||||
|
'qualified' -- Firma cualificada (certificado oficial)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Estado de la firma
|
||||||
|
CREATE TYPE documents.signature_status AS ENUM (
|
||||||
|
'pending', -- Pendiente de firma
|
||||||
|
'signed', -- Firmado
|
||||||
|
'rejected', -- Rechazado
|
||||||
|
'expired', -- Expirado
|
||||||
|
'revoked' -- Revocado
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tabla de firmas digitales
|
||||||
|
CREATE TABLE IF NOT EXISTS documents.digital_signatures (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
tenant_id UUID NOT NULL,
|
||||||
|
|
||||||
|
-- Documento firmado
|
||||||
|
document_id UUID NOT NULL,
|
||||||
|
version_id UUID,
|
||||||
|
|
||||||
|
-- Firmante
|
||||||
|
signer_id UUID NOT NULL,
|
||||||
|
signer_name VARCHAR(200) NOT NULL,
|
||||||
|
signer_email VARCHAR(255),
|
||||||
|
signer_role VARCHAR(100),
|
||||||
|
|
||||||
|
-- Tipo y estado
|
||||||
|
signature_type documents.signature_type NOT NULL DEFAULT 'simple',
|
||||||
|
status documents.signature_status NOT NULL DEFAULT 'pending',
|
||||||
|
|
||||||
|
-- Datos de firma
|
||||||
|
signature_data TEXT,
|
||||||
|
signature_hash VARCHAR(128),
|
||||||
|
hash_algorithm VARCHAR(20) DEFAULT 'SHA-256',
|
||||||
|
|
||||||
|
-- Certificado (para firmas avanzadas/cualificadas)
|
||||||
|
certificate_info JSONB,
|
||||||
|
certificate_serial VARCHAR(100),
|
||||||
|
certificate_issuer VARCHAR(255),
|
||||||
|
certificate_valid_from TIMESTAMPTZ,
|
||||||
|
certificate_valid_to TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Timestamps
|
||||||
|
requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
signed_at TIMESTAMPTZ,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Contexto de firma
|
||||||
|
ip_address INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
geolocation JSONB,
|
||||||
|
|
||||||
|
-- Validación
|
||||||
|
is_valid BOOLEAN DEFAULT true,
|
||||||
|
validation_errors JSONB,
|
||||||
|
last_validated_at TIMESTAMPTZ,
|
||||||
|
|
||||||
|
-- Razón (para rechazo/revocación)
|
||||||
|
reason TEXT,
|
||||||
|
|
||||||
|
-- Metadatos
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Auditoría
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Foreign keys
|
||||||
|
CONSTRAINT fk_digital_signatures_tenant
|
||||||
|
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_digital_signatures_document
|
||||||
|
FOREIGN KEY (document_id) REFERENCES documents.documents(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT fk_digital_signatures_version
|
||||||
|
FOREIGN KEY (version_id) REFERENCES documents.document_versions(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices para digital_signatures
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_signatures_tenant ON documents.digital_signatures(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_signatures_document ON documents.digital_signatures(document_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_signatures_signer ON documents.digital_signatures(signer_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_signatures_status ON documents.digital_signatures(tenant_id, status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_digital_signatures_signed_at ON documents.digital_signatures(signed_at);
|
||||||
|
|
||||||
|
-- RLS para digital_signatures
|
||||||
|
ALTER TABLE documents.digital_signatures ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
CREATE POLICY digital_signatures_tenant_isolation ON documents.digital_signatures
|
||||||
|
FOR ALL
|
||||||
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
|
||||||
|
|
||||||
|
-- FK opcional: signer_id → auth.users
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'auth' AND tablename = 'users') THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'fk_digital_signatures_signer'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE documents.digital_signatures
|
||||||
|
ADD CONSTRAINT fk_digital_signatures_signer
|
||||||
|
FOREIGN KEY (signer_id) REFERENCES auth.users(id)
|
||||||
|
ON DELETE SET NULL;
|
||||||
|
RAISE NOTICE 'FK creada: digital_signatures.signer_id → auth.users';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
RAISE NOTICE 'FK digital_signatures.signer_id no creada: %', SQLERRM;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMENT ON TABLE documents.digital_signatures IS 'GAP-006: Firmas digitales para documentos';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FIN DEL SCRIPT
|
||||||
|
-- FK constraints adicionales: 5 (incluyendo digital_signatures)
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
|||||||
356
schemas/13-audit-triggers-ddl.sql
Normal file
356
schemas/13-audit-triggers-ddl.sql
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
-- ============================================================================
|
||||||
|
-- 13-audit-triggers-ddl.sql
|
||||||
|
-- Schema: audit
|
||||||
|
-- ERP Construccion - GAP-007: Audit Triggers
|
||||||
|
-- ============================================================================
|
||||||
|
-- Descripcion: Sistema de auditoría detallada con:
|
||||||
|
-- - Registro de cambios a nivel de campo
|
||||||
|
-- - Triggers automáticos para tablas críticas
|
||||||
|
-- - Historial completo de modificaciones
|
||||||
|
-- ============================================================================
|
||||||
|
-- Autor: Claude-Arquitecto-Orquestador
|
||||||
|
-- Fecha: 2026-02-04
|
||||||
|
-- Version: 1.0.0
|
||||||
|
-- GAP: GAP-007
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Crear schema si no existe
|
||||||
|
CREATE SCHEMA IF NOT EXISTS audit;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- ENUMS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Tipo de operación
|
||||||
|
CREATE TYPE audit.operation_type AS ENUM (
|
||||||
|
'INSERT',
|
||||||
|
'UPDATE',
|
||||||
|
'DELETE',
|
||||||
|
'TRUNCATE'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- TABLA DE AUDIT LOG
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS audit.change_log (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Contexto
|
||||||
|
tenant_id UUID,
|
||||||
|
user_id UUID,
|
||||||
|
session_id VARCHAR(100),
|
||||||
|
|
||||||
|
-- Operación
|
||||||
|
operation audit.operation_type NOT NULL,
|
||||||
|
table_schema VARCHAR(63) NOT NULL,
|
||||||
|
table_name VARCHAR(63) NOT NULL,
|
||||||
|
record_id UUID,
|
||||||
|
|
||||||
|
-- Datos
|
||||||
|
old_data JSONB,
|
||||||
|
new_data JSONB,
|
||||||
|
changed_fields TEXT[],
|
||||||
|
|
||||||
|
-- Metadatos
|
||||||
|
client_ip INET,
|
||||||
|
user_agent TEXT,
|
||||||
|
application_name VARCHAR(100),
|
||||||
|
|
||||||
|
-- Timestamp
|
||||||
|
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
-- Para queries eficientes
|
||||||
|
executed_date DATE GENERATED ALWAYS AS (executed_at::date) STORED
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Índices para change_log
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_tenant ON audit.change_log(tenant_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_user ON audit.change_log(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_table ON audit.change_log(table_schema, table_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_record ON audit.change_log(record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_executed ON audit.change_log(executed_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_date ON audit.change_log(executed_date);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_change_log_operation ON audit.change_log(operation);
|
||||||
|
|
||||||
|
-- Particionamiento por fecha (opcional, para grandes volúmenes)
|
||||||
|
-- La tabla puede ser particionada posteriormente si es necesario
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FUNCIÓN DE TRIGGER GENÉRICA
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION audit.log_changes()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_tenant_id UUID;
|
||||||
|
v_user_id UUID;
|
||||||
|
v_session_id TEXT;
|
||||||
|
v_old_data JSONB;
|
||||||
|
v_new_data JSONB;
|
||||||
|
v_changed_fields TEXT[];
|
||||||
|
v_record_id UUID;
|
||||||
|
v_key TEXT;
|
||||||
|
BEGIN
|
||||||
|
-- Obtener contexto de la sesión
|
||||||
|
BEGIN
|
||||||
|
v_tenant_id := current_setting('app.current_tenant_id', true)::uuid;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
v_tenant_id := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_user_id := current_setting('app.current_user_id', true)::uuid;
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
v_user_id := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
BEGIN
|
||||||
|
v_session_id := current_setting('app.session_id', true);
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
v_session_id := NULL;
|
||||||
|
END;
|
||||||
|
|
||||||
|
-- Determinar datos según operación
|
||||||
|
IF TG_OP = 'INSERT' THEN
|
||||||
|
v_new_data := to_jsonb(NEW);
|
||||||
|
v_old_data := NULL;
|
||||||
|
v_record_id := NEW.id;
|
||||||
|
v_changed_fields := ARRAY(SELECT jsonb_object_keys(v_new_data));
|
||||||
|
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
v_old_data := to_jsonb(OLD);
|
||||||
|
v_new_data := to_jsonb(NEW);
|
||||||
|
v_record_id := NEW.id;
|
||||||
|
|
||||||
|
-- Calcular campos modificados
|
||||||
|
v_changed_fields := ARRAY(
|
||||||
|
SELECT key FROM (
|
||||||
|
SELECT key, v_old_data->key AS old_val, v_new_data->key AS new_val
|
||||||
|
FROM jsonb_object_keys(v_new_data) AS key
|
||||||
|
) AS changes
|
||||||
|
WHERE old_val IS DISTINCT FROM new_val
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Si no hay cambios reales, no registrar
|
||||||
|
IF array_length(v_changed_fields, 1) IS NULL THEN
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
ELSIF TG_OP = 'DELETE' THEN
|
||||||
|
v_old_data := to_jsonb(OLD);
|
||||||
|
v_new_data := NULL;
|
||||||
|
v_record_id := OLD.id;
|
||||||
|
v_changed_fields := NULL;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Insertar registro de auditoría
|
||||||
|
INSERT INTO audit.change_log (
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
session_id,
|
||||||
|
operation,
|
||||||
|
table_schema,
|
||||||
|
table_name,
|
||||||
|
record_id,
|
||||||
|
old_data,
|
||||||
|
new_data,
|
||||||
|
changed_fields,
|
||||||
|
client_ip,
|
||||||
|
application_name
|
||||||
|
) VALUES (
|
||||||
|
v_tenant_id,
|
||||||
|
v_user_id,
|
||||||
|
v_session_id,
|
||||||
|
TG_OP::audit.operation_type,
|
||||||
|
TG_TABLE_SCHEMA,
|
||||||
|
TG_TABLE_NAME,
|
||||||
|
v_record_id,
|
||||||
|
v_old_data,
|
||||||
|
v_new_data,
|
||||||
|
v_changed_fields,
|
||||||
|
inet_client_addr(),
|
||||||
|
current_setting('application_name', true)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Retornar el registro apropiado
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
RETURN OLD;
|
||||||
|
ELSE
|
||||||
|
RETURN NEW;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FUNCIÓN PARA CREAR TRIGGER DE AUDITORÍA
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION audit.enable_audit_trigger(
|
||||||
|
p_schema_name TEXT,
|
||||||
|
p_table_name TEXT
|
||||||
|
)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_trigger_name TEXT;
|
||||||
|
BEGIN
|
||||||
|
v_trigger_name := 'audit_' || p_table_name || '_changes';
|
||||||
|
|
||||||
|
-- Eliminar trigger si existe
|
||||||
|
EXECUTE format(
|
||||||
|
'DROP TRIGGER IF EXISTS %I ON %I.%I',
|
||||||
|
v_trigger_name, p_schema_name, p_table_name
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Crear trigger
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TRIGGER %I
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON %I.%I
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION audit.log_changes()',
|
||||||
|
v_trigger_name, p_schema_name, p_table_name
|
||||||
|
);
|
||||||
|
|
||||||
|
RAISE NOTICE 'Audit trigger creado: %.%', p_schema_name, p_table_name;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FUNCIÓN PARA DESACTIVAR TRIGGER DE AUDITORÍA
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION audit.disable_audit_trigger(
|
||||||
|
p_schema_name TEXT,
|
||||||
|
p_table_name TEXT
|
||||||
|
)
|
||||||
|
RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_trigger_name TEXT;
|
||||||
|
BEGIN
|
||||||
|
v_trigger_name := 'audit_' || p_table_name || '_changes';
|
||||||
|
|
||||||
|
EXECUTE format(
|
||||||
|
'DROP TRIGGER IF EXISTS %I ON %I.%I',
|
||||||
|
v_trigger_name, p_schema_name, p_table_name
|
||||||
|
);
|
||||||
|
|
||||||
|
RAISE NOTICE 'Audit trigger eliminado: %.%', p_schema_name, p_table_name;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- ACTIVAR AUDITORÍA EN TABLAS CRÍTICAS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Construction: Proyectos y Fraccionamientos
|
||||||
|
SELECT audit.enable_audit_trigger('construction', 'fraccionamientos');
|
||||||
|
SELECT audit.enable_audit_trigger('construction', 'proyectos');
|
||||||
|
|
||||||
|
-- Estimates: Estimaciones (facturación)
|
||||||
|
SELECT audit.enable_audit_trigger('estimates', 'estimaciones');
|
||||||
|
SELECT audit.enable_audit_trigger('estimates', 'anticipos');
|
||||||
|
|
||||||
|
-- Finance: Transacciones financieras
|
||||||
|
SELECT audit.enable_audit_trigger('finance', 'journal_entries');
|
||||||
|
SELECT audit.enable_audit_trigger('finance', 'invoices');
|
||||||
|
SELECT audit.enable_audit_trigger('finance', 'payments');
|
||||||
|
|
||||||
|
-- Assets: Activos fijos
|
||||||
|
SELECT audit.enable_audit_trigger('assets', 'assets');
|
||||||
|
SELECT audit.enable_audit_trigger('assets', 'depreciation_schedule');
|
||||||
|
|
||||||
|
-- Documents: Documentos y firmas
|
||||||
|
SELECT audit.enable_audit_trigger('documents', 'documents');
|
||||||
|
SELECT audit.enable_audit_trigger('documents', 'digital_signatures');
|
||||||
|
|
||||||
|
-- HR: Empleados
|
||||||
|
SELECT audit.enable_audit_trigger('hr', 'employees');
|
||||||
|
|
||||||
|
-- HSE: Incidentes de seguridad
|
||||||
|
SELECT audit.enable_audit_trigger('hse', 'incidentes');
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FUNCIONES DE CONSULTA
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Obtener historial de cambios de un registro
|
||||||
|
CREATE OR REPLACE FUNCTION audit.get_record_history(
|
||||||
|
p_table_schema TEXT,
|
||||||
|
p_table_name TEXT,
|
||||||
|
p_record_id UUID,
|
||||||
|
p_limit INT DEFAULT 100
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
change_id UUID,
|
||||||
|
operation audit.operation_type,
|
||||||
|
changed_fields TEXT[],
|
||||||
|
old_data JSONB,
|
||||||
|
new_data JSONB,
|
||||||
|
user_id UUID,
|
||||||
|
executed_at TIMESTAMPTZ
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
cl.id,
|
||||||
|
cl.operation,
|
||||||
|
cl.changed_fields,
|
||||||
|
cl.old_data,
|
||||||
|
cl.new_data,
|
||||||
|
cl.user_id,
|
||||||
|
cl.executed_at
|
||||||
|
FROM audit.change_log cl
|
||||||
|
WHERE cl.table_schema = p_table_schema
|
||||||
|
AND cl.table_name = p_table_name
|
||||||
|
AND cl.record_id = p_record_id
|
||||||
|
ORDER BY cl.executed_at DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Obtener cambios recientes por tenant
|
||||||
|
CREATE OR REPLACE FUNCTION audit.get_recent_changes(
|
||||||
|
p_tenant_id UUID,
|
||||||
|
p_hours INT DEFAULT 24,
|
||||||
|
p_limit INT DEFAULT 100
|
||||||
|
)
|
||||||
|
RETURNS TABLE (
|
||||||
|
change_id UUID,
|
||||||
|
table_schema VARCHAR,
|
||||||
|
table_name VARCHAR,
|
||||||
|
record_id UUID,
|
||||||
|
operation audit.operation_type,
|
||||||
|
user_id UUID,
|
||||||
|
executed_at TIMESTAMPTZ
|
||||||
|
) AS $$
|
||||||
|
BEGIN
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT
|
||||||
|
cl.id,
|
||||||
|
cl.table_schema,
|
||||||
|
cl.table_name,
|
||||||
|
cl.record_id,
|
||||||
|
cl.operation,
|
||||||
|
cl.user_id,
|
||||||
|
cl.executed_at
|
||||||
|
FROM audit.change_log cl
|
||||||
|
WHERE cl.tenant_id = p_tenant_id
|
||||||
|
AND cl.executed_at >= NOW() - (p_hours || ' hours')::interval
|
||||||
|
ORDER BY cl.executed_at DESC
|
||||||
|
LIMIT p_limit;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- COMENTARIOS
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
COMMENT ON SCHEMA audit IS 'GAP-007: Sistema de auditoría detallada';
|
||||||
|
COMMENT ON TABLE audit.change_log IS 'Registro de todos los cambios en tablas auditadas';
|
||||||
|
COMMENT ON FUNCTION audit.log_changes() IS 'Trigger function para registrar cambios';
|
||||||
|
COMMENT ON FUNCTION audit.enable_audit_trigger(TEXT, TEXT) IS 'Habilitar auditoría en una tabla';
|
||||||
|
COMMENT ON FUNCTION audit.disable_audit_trigger(TEXT, TEXT) IS 'Deshabilitar auditoría en una tabla';
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- FIN DEL SCRIPT
|
||||||
|
-- Tablas auditadas: 13 tablas críticas
|
||||||
|
-- ============================================================================
|
||||||
Loading…
Reference in New Issue
Block a user