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:
2026-08-17 18:41:10 +09:00
parent 023bfa97bf
commit c289a698c5
11 changed files with 825 additions and 70 deletions
@@ -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;