-- ============================================ -- 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';