feat[agent]: add soft delete and audit logging for core tables
This commit is contained in:
parent
f1c75faba1
commit
f1ece4b182
1 changed files with 130 additions and 20 deletions
|
|
@ -1,24 +1,134 @@
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- FreightDesk — Migration 003: Soft Delete + Security
|
-- SOFT DELETE & AUDIT LOG EXTENSION
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
|
-- Add soft delete columns to core tables
|
||||||
|
-- ============================================================
|
||||||
|
ALTER TABLE loads ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE shippers ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE vehicles ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE payments ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
ALTER TABLE portal_users ADD COLUMN deleted_at TIMESTAMPTZ;
|
||||||
|
|
||||||
-- Add soft-delete columns
|
-- ============================================================
|
||||||
ALTER TABLE loads ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
-- AUDIT LOG TABLE
|
||||||
ALTER TABLE payments ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
-- ============================================================
|
||||||
ALTER TABLE shippers ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
CREATE TABLE audit_logs (
|
||||||
ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
|
||||||
-- Add role column to portal_users if not exists
|
table_name TEXT NOT NULL, -- e.g., 'loads', 'shippers'
|
||||||
ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'admin';
|
row_id UUID, -- UUID of the affected row (if applicable)
|
||||||
|
before_json JSONB, -- state before change
|
||||||
-- Add index for soft-delete queries
|
after_json JSONB, -- state after change
|
||||||
CREATE INDEX IF NOT EXISTS idx_loads_deleted_at ON loads(deleted_at) WHERE deleted_at IS NULL;
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
CREATE INDEX IF NOT EXISTS idx_payments_deleted_at ON payments(deleted_at) WHERE deleted_at IS NULL;
|
user_id UUID, -- references admin who made change (nullable)
|
||||||
|
notes TEXT
|
||||||
-- Add load_count to shippers for quick reference
|
|
||||||
ALTER TABLE shippers ADD COLUMN IF NOT EXISTS load_count INTEGER DEFAULT 0;
|
|
||||||
|
|
||||||
-- Update load_count for existing shippers
|
|
||||||
UPDATE shippers SET load_count = (
|
|
||||||
SELECT COUNT(*) FROM loads WHERE loads.shipper_id = shippers.id
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Indexes for fast lookup
|
||||||
|
CREATE INDEX idx_audit_logs_table ON audit_logs(table_name);
|
||||||
|
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
|
||||||
|
CREATE INDEX idx_audit_logs_time ON audit_logs(created_at DESC);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- TRIGGERS FOR SOFT DELETE & AUDIT LOGGING
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- Function to perform soft delete and log action
|
||||||
|
CREATE OR REPLACE FUNCTION soft_delete_and_audit()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
BEGIN
|
||||||
|
-- Determine which table triggered the trigger
|
||||||
|
IF TG_TABLE_NAME = 'loads' THEN
|
||||||
|
IF OLD.deleted_at IS NOT NULL THEN
|
||||||
|
RETURN OLD; -- already soft-deleted
|
||||||
|
END IF;
|
||||||
|
UPDATE loads SET deleted_at = NOW() WHERE id = OLD.id;
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
|
||||||
|
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
|
||||||
|
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'INSERT' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
|
||||||
|
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, NULL, NOW(), current_setting('user_id')::UUID);
|
||||||
|
END IF;
|
||||||
|
ELSIF TG_TABLE_NAME = 'shippers' THEN
|
||||||
|
IF OLD.deleted_at IS NOT NULL THEN
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
UPDATE shippers SET deleted_at = NOW() WHERE id = OLD.id;
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
|
||||||
|
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
|
||||||
|
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'INSERT' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
|
||||||
|
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
END IF;
|
||||||
|
ELSIF TG_TABLE_NAME = 'vehicles' THEN
|
||||||
|
IF OLD.deleted_at IS NOT NULL THEN
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
UPDATE vehicles SET deleted_at = NOW() WHERE id = OLD.id;
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
|
||||||
|
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
|
||||||
|
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'INSERT' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
|
||||||
|
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
END IF;
|
||||||
|
ELSIF TG_TABLE_NAME = 'payments' THEN
|
||||||
|
IF OLD.deleted_at IS NOT NULL THEN
|
||||||
|
RETURN OLD;
|
||||||
|
END IF;
|
||||||
|
UPDATE payments SET deleted_at = NOW() WHERE id = OLD.id;
|
||||||
|
IF TG_OP = 'DELETE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
|
||||||
|
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'UPDATE' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
|
||||||
|
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
ELSIF TG_OP = 'INSERT' THEN
|
||||||
|
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
|
||||||
|
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
RETURN COALESCE(NEW, OLD);
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Create triggers for soft delete and audit logging on each table
|
||||||
|
CREATE TRIGGER trg_loads_soft_delete BEFORE UPDATE OR DELETE ON loads
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
CREATE TRIGGER trg_loads_soft_delete_INSERT BEFORE INSERT ON loads
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_shippers_soft_delete BEFORE UPDATE OR DELETE ON shippers
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
CREATE TRIGGER trg_shippers_soft_delete_INSERT BEFORE INSERT ON shippers
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_vehicles_soft_delete BEFORE UPDATE OR DELETE ON vehicles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
CREATE TRIGGER trg_vehicles_soft_delete_INSERT BEFORE INSERT ON vehicles
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_payments_soft_delete BEFORE UPDATE OR DELETE ON payments
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
CREATE TRIGGER trg_payments_soft_delete_INSERT BEFORE INSERT ON payments
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- AUDIT SETUP FUNCTION TO CAPTURE USER ID FROM SESSION CONTEXT
|
||||||
|
-- ============================================================
|
||||||
|
-- Note: PostgreSQL cannot directly access Express session, so we pass user_id
|
||||||
|
-- via SET user_id = '<session_user_id>' before triggering audits.
|
||||||
|
-- Application must set this variable before performing DB operations.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue