template-saas-database-v2/ddl/schemas/audit/tables/01-audit-logs.sql
rckrdmrd 27de049441 [TEMPLATE-SAAS-DB] chore: Update audit schema and add migrations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 04:38:47 -06:00

156 lines
5.1 KiB
PL/PgSQL

-- ============================================
-- TEMPLATE-SAAS: Audit Logs
-- Schema: audit
-- Version: 1.0.0
-- ============================================
CREATE TABLE audit.audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Actor
user_id UUID REFERENCES users.users(id),
user_email VARCHAR(255),
actor_type VARCHAR(50) DEFAULT 'user', -- 'user', 'system', 'api_key', 'webhook'
-- Action
action audit.action_type NOT NULL,
resource_type VARCHAR(100) NOT NULL, -- 'user', 'product', 'invoice', etc.
resource_id VARCHAR(255),
resource_name VARCHAR(255),
-- Details
description TEXT,
metadata JSONB DEFAULT '{}'::jsonb,
-- Change tracking (expanded structure for better query flexibility)
old_values JSONB, -- Previous values: { field1: oldVal1, ... }
new_values JSONB, -- New values: { field1: newVal1, ... }
changed_fields JSONB, -- List of changed fields: ['field1', 'field2', ...]
-- Severity
severity audit.severity DEFAULT 'info',
-- Context
ip_address INET,
user_agent TEXT,
request_id VARCHAR(100),
session_id UUID,
-- HTTP context (for API audit)
endpoint VARCHAR(255),
http_method VARCHAR(10),
response_status SMALLINT,
duration_ms INTEGER,
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
-- Partitioning support (optional)
partition_key DATE DEFAULT CURRENT_DATE
);
-- Activity logs (lighter weight, for analytics)
CREATE TABLE audit.activity_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES users.users(id),
-- Activity
activity_type VARCHAR(100) NOT NULL, -- 'page_view', 'button_click', 'search', etc.
page_url VARCHAR(500),
referrer VARCHAR(500),
-- Context
session_id UUID,
device_type VARCHAR(50),
browser VARCHAR(100),
os VARCHAR(100),
-- Data
data JSONB DEFAULT '{}'::jsonb,
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
-- Indexes
CREATE INDEX idx_audit_logs_tenant ON audit.audit_logs(tenant_id, created_at DESC);
CREATE INDEX idx_audit_logs_user ON audit.audit_logs(tenant_id, user_id, created_at DESC);
CREATE INDEX idx_audit_logs_resource ON audit.audit_logs(tenant_id, resource_type, resource_id);
CREATE INDEX idx_audit_logs_action ON audit.audit_logs(tenant_id, action, created_at DESC);
CREATE INDEX idx_audit_logs_severity ON audit.audit_logs(tenant_id, severity) WHERE severity IN ('warning', 'error', 'critical');
CREATE INDEX idx_activity_logs_tenant ON audit.activity_logs(tenant_id, created_at DESC);
CREATE INDEX idx_activity_logs_user ON audit.activity_logs(tenant_id, user_id, created_at DESC);
CREATE INDEX idx_activity_logs_type ON audit.activity_logs(tenant_id, activity_type);
-- RLS
ALTER TABLE audit.audit_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE audit.activity_logs ENABLE ROW LEVEL SECURITY;
CREATE POLICY audit_logs_tenant_isolation ON audit.audit_logs
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY activity_logs_tenant_isolation ON audit.activity_logs
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Function to log audit event
CREATE OR REPLACE FUNCTION audit.log_event(
p_tenant_id UUID,
p_user_id UUID,
p_action audit.action_type,
p_resource_type VARCHAR,
p_resource_id VARCHAR DEFAULT NULL,
p_resource_name VARCHAR DEFAULT NULL,
p_description TEXT DEFAULT NULL,
p_old_values JSONB DEFAULT NULL,
p_new_values JSONB DEFAULT NULL,
p_changed_fields JSONB DEFAULT NULL,
p_metadata JSONB DEFAULT '{}'::jsonb,
p_severity audit.severity DEFAULT 'info'
)
RETURNS UUID AS $$
DECLARE
v_id UUID;
BEGIN
INSERT INTO audit.audit_logs (
tenant_id, user_id, action, resource_type, resource_id,
resource_name, description, old_values, new_values, changed_fields,
metadata, severity
) VALUES (
p_tenant_id, p_user_id, p_action, p_resource_type, p_resource_id,
p_resource_name, p_description, p_old_values, p_new_values, p_changed_fields,
p_metadata, p_severity
) RETURNING id INTO v_id;
RETURN v_id;
END;
$$ LANGUAGE plpgsql;
-- Retention policy function
CREATE OR REPLACE FUNCTION audit.cleanup_old_logs(retention_days INT DEFAULT 90)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
WITH deleted AS (
DELETE FROM audit.audit_logs
WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL
AND severity NOT IN ('error', 'critical')
RETURNING *
)
SELECT COUNT(*) INTO deleted_count FROM deleted;
DELETE FROM audit.activity_logs
WHERE created_at < NOW() - (retention_days || ' days')::INTERVAL;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Comments
COMMENT ON TABLE audit.audit_logs IS 'Comprehensive audit trail for compliance';
COMMENT ON TABLE audit.activity_logs IS 'User activity tracking for analytics';
COMMENT ON COLUMN audit.audit_logs.changes IS 'JSON diff of field changes';