feat(wbs): AEG-VS-01-05 Event/Job/Inbox - Part 2-3 Complete (Outbox/Inbox + Consumers)
Part 2: Transaction + Outbox Integration - RegisterIdentityEndpoint: DbConnection → DbTransaction → Outbox write - RegisterIdentitySql: Accept NpgsqlConnection + NpgsqlTransaction (Dapper) - Fixed schema references: identity.identity → public.identity - Hash computation (SHA256) for Outbox payload integrity Part 3: Consumer + Job Implementation - IdentityCreatedConsumer: SignalR group 'identity-notifications' - MfaReminderJob: Hangfire job, 24-hour reminder, idempotent via DB tracking - IdentityAuditConsumer: Immutable append-only audit trail - Migration 0043: identity_mfa_reminder + identity_audit_log tables Testing - Unit: IdentityCreated event serialization + immutability (4 tests) - Integration: RegisterIdentityWithOutbox (3 tests: happy path, rollback, duplicate email) - Updated existing tests: Transaction management (6 test methods) Architecture - Outbox/Inbox pattern ensures exactly-once delivery - Consumers decouple from identity creation (async, independent retry) - Audit trail immutable (trigger prevents updates/deletes) - MFA reminder idempotent (tracked in DB) Status: 40% COMPLETE (event + endpoint + 3 consumers) Next: E2E tests + Hangfire job registration + Admin UI Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
-- Migration 0043: Identity MFA Tracking and Audit Logging
|
||||
-- AEG-VS-01-05: Event/Job/Inbox - MFA Reminder Job + Audit Consumer
|
||||
-- Created: 2026-08-17
|
||||
-- Purpose: Track MFA reminder sends (idempotency) and maintain audit trail for identity events
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. MFA REMINDER TRACKING TABLE
|
||||
-- Tracks when MFA setup reminders have been sent to prevent duplicate emails
|
||||
CREATE TABLE IF NOT EXISTS public.identity_mfa_reminder (
|
||||
reminder_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE,
|
||||
sent_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Idempotency: one reminder record per identity
|
||||
UNIQUE(identity_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_mfa_reminder_identity ON public.identity_mfa_reminder(identity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_mfa_reminder_sent_at ON public.identity_mfa_reminder(sent_at);
|
||||
|
||||
COMMENT ON TABLE public.identity_mfa_reminder IS
|
||||
'Tracks MFA setup reminder sends for idempotency. Prevents duplicate emails if job retries.';
|
||||
|
||||
COMMENT ON COLUMN public.identity_mfa_reminder.identity_id IS
|
||||
'Identity that received the MFA reminder. Links to identity(identity_id).';
|
||||
|
||||
COMMENT ON COLUMN public.identity_mfa_reminder.sent_at IS
|
||||
'Timestamp when reminder was sent (or marked as sent). Used for 24-hour delay tracking.';
|
||||
|
||||
-- 2. IDENTITY AUDIT LOG TABLE
|
||||
-- Immutable append-only audit trail for identity lifecycle events
|
||||
CREATE TABLE IF NOT EXISTS public.identity_audit_log (
|
||||
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
email VARCHAR(255),
|
||||
display_name VARCHAR(255),
|
||||
correlation_id UUID,
|
||||
occurred_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
-- Idempotency: one audit entry per (identity_id, action) combination
|
||||
-- Allow multiple entries for same action but at different times
|
||||
CONSTRAINT identity_audit_unique_per_action UNIQUE(identity_id, action, occurred_at)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_audit_identity ON public.identity_audit_log(identity_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_audit_action ON public.identity_audit_log(action);
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_audit_correlation ON public.identity_audit_log(correlation_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_audit_created_at ON public.identity_audit_log(created_at DESC);
|
||||
|
||||
COMMENT ON TABLE public.identity_audit_log IS
|
||||
'Immutable append-only audit trail for identity events (CREATE, MFA_SETUP, STATE_CHANGE, etc).';
|
||||
|
||||
COMMENT ON COLUMN public.identity_audit_log.action IS
|
||||
'Event type: CREATED, MFA_SETUP_REQUIRED, MFA_CONFIGURED, STATE_CHANGED, etc.';
|
||||
|
||||
COMMENT ON COLUMN public.identity_audit_log.correlation_id IS
|
||||
'Links audit entry to request trace for end-to-end tracing and compliance.';
|
||||
|
||||
-- Prevent accidental updates/deletes on audit log
|
||||
CREATE TRIGGER identity_audit_log_immutable
|
||||
BEFORE UPDATE OR DELETE ON public.identity_audit_log
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION raise_immutable_error();
|
||||
|
||||
-- Create immutable trigger function if it doesn't exist
|
||||
CREATE OR REPLACE FUNCTION raise_immutable_error()
|
||||
RETURNS TRIGGER
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'Audit log entries are immutable and cannot be modified or deleted.';
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user