Files
tailscale-custom/.agent/uploads/1773926307026_audit_service_variants.md
T
huanld 2fb067ecbf
checklocks / checklocks (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
natlab-integrationtest / natlab-integrationtest (push) Has been cancelled
CI / gomod-cache (push) Has been cancelled
CI / race-root-integration (1/4) (push) Has been cancelled
CI / race-root-integration (2/4) (push) Has been cancelled
CI / race-root-integration (3/4) (push) Has been cancelled
CI / race-root-integration (4/4) (push) Has been cancelled
CI / test (-race, amd64, 1/3) (push) Has been cancelled
CI / test (-race, amd64, 2/3) (push) Has been cancelled
CI / test (-race, amd64, 3/3) (push) Has been cancelled
CI / test (386) (push) Has been cancelled
CI / test (amd64) (push) Has been cancelled
CI / Windows (benchmarks) (push) Has been cancelled
CI / Windows (1/2) (push) Has been cancelled
CI / Windows (2/2) (push) Has been cancelled
CI / macos (push) Has been cancelled
CI / privileged (push) Has been cancelled
CI / vm (push) Has been cancelled
CI / cross (386, linux) (push) Has been cancelled
CI / cross (amd64, darwin) (push) Has been cancelled
CI / cross (amd64, freebsd) (push) Has been cancelled
CI / cross (amd64, openbsd) (push) Has been cancelled
CI / cross (amd64, windows) (push) Has been cancelled
CI / cross (arm, 5, linux) (push) Has been cancelled
CI / cross (arm, 7, linux) (push) Has been cancelled
CI / cross (arm64, darwin) (push) Has been cancelled
CI / cross (arm64, linux) (push) Has been cancelled
CI / cross (arm64, windows) (push) Has been cancelled
CI / cross (loong64, linux) (push) Has been cancelled
CI / ios (push) Has been cancelled
CI / crossmin (amd64, illumos) (push) Has been cancelled
CI / crossmin (amd64, plan9) (push) Has been cancelled
CI / crossmin (amd64, solaris) (push) Has been cancelled
CI / crossmin (ppc64, aix) (push) Has been cancelled
CI / android (push) Has been cancelled
CI / wasm (push) Has been cancelled
CI / tailscale_go (push) Has been cancelled
CI / fuzz (push) Has been cancelled
CI / depaware (push) Has been cancelled
CI / go_generate (push) Has been cancelled
CI / make_tidy (push) Has been cancelled
CI / licenses (push) Has been cancelled
CI / staticcheck (macOS) (push) Has been cancelled
CI / staticcheck (Linux) (push) Has been cancelled
CI / staticcheck (Windows) (push) Has been cancelled
CI / staticcheck (Portable (1/4)) (push) Has been cancelled
CI / staticcheck (Portable (2/4)) (push) Has been cancelled
CI / staticcheck (Portable (3/4)) (push) Has been cancelled
CI / staticcheck (Portable (4/4)) (push) Has been cancelled
CI / notify_slack (push) Has been cancelled
CI / merge_blocker (push) Has been cancelled
CI / check_mergeability_strict (push) Has been cancelled
CI / check_mergeability (push) Has been cancelled
Dockerfile build / deploy (push) Has been cancelled
test installer.sh / test (curl, alpine:3.21) (push) Has been cancelled
test installer.sh / test (curl, alpine:edge) (push) Has been cancelled
test installer.sh / test (curl, alpine:latest) (push) Has been cancelled
test installer.sh / test (curl, amazonlinux:latest) (push) Has been cancelled
test installer.sh / test (curl, archlinux:latest) (push) Has been cancelled
test installer.sh / test (curl, debian:oldstable-slim) (push) Has been cancelled
test installer.sh / test (curl, debian:sid-slim) (push) Has been cancelled
test installer.sh / test (curl, debian:stable-slim, 1.80.0) (push) Has been cancelled
test installer.sh / test (curl, debian:testing-slim) (push) Has been cancelled
test installer.sh / test (curl, elementary/docker:stable) (push) Has been cancelled
test installer.sh / test (curl, elementary/docker:unstable) (push) Has been cancelled
test installer.sh / test (curl, fedora:latest, 1.80.0) (push) Has been cancelled
test installer.sh / test (curl, kalilinux/kali-dev) (push) Has been cancelled
test installer.sh / test (curl, kalilinux/kali-rolling) (push) Has been cancelled
test installer.sh / test (curl, opensuse/leap:latest) (push) Has been cancelled
test installer.sh / test (curl, opensuse/tumbleweed:latest) (push) Has been cancelled
test installer.sh / test (curl, oraclelinux:8) (push) Has been cancelled
test installer.sh / test (curl, oraclelinux:9) (push) Has been cancelled
test installer.sh / test (curl, parrotsec/core:latest) (push) Has been cancelled
test installer.sh / test (curl, rockylinux:8.7) (push) Has been cancelled
test installer.sh / test (curl, rockylinux:9) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:20.04) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:22.04) (push) Has been cancelled
test installer.sh / test (curl, ubuntu:24.04, 1.80.0) (push) Has been cancelled
test installer.sh / test (wget, debian:oldstable-slim) (push) Has been cancelled
test installer.sh / test (wget, debian:sid-slim) (push) Has been cancelled
update-flake / update-flake (push) Has been cancelled
tailscale.com/cmd/vet / vet (push) Has been cancelled
test installer.sh / notify-slack (push) Has been cancelled
feat: security hardening, production roadmap, admin panel v1
Client security fixes (cmd/tailscale-tray/main.go):
- SSRF protection in Add Server dialog (validateControlURL): reject
  private/loopback/link-local/cloud-metadata IPs via DNS resolution
- RCE gate on AuthURL/BrowseToURL exec paths (validateAuthURL)
- Sanitized URL logging (sanitizeURLForLog drops query auth tokens)
- Error handling on exec.Command with user-facing showError()

Admin panel security (web-admin):
- Bcrypt password hashing (replaces SHA256)
- Rate limiting: 5 failed logins → 15-min lockout
- Session + login attempt cleanup goroutine (hourly)
- url.QueryEscape / encodeURIComponent for all API params
- Fail-hard startup when no TLS and non-loopback bind
- ADMIN_PASSWORD required (no default), password min 12 chars
- Username regex whitelist

Installer hardening (Setup.wxs):
- util:PermissionEx restricts SCM access: only Administrators +
  SYSTEM can start/stop/reconfigure service. Authenticated Users
  limited to QueryStatus/QueryConfig/Interrogate
- Vital="yes" on ServiceInstall

Docs & roadmap:
- PRODUCTION_ROADMAP.md: 5-milestone plan (security + features +
  distribution + ops) with granular tasks, effort, done-when
- CLIENT_SECURITY_AUDIT.md, SECURITY_FIXES.md, DEPLOYMENT.md
- AI assistant rules (.cursorrules, .antigravityrules, etc.)

Build & distribution:
- build-msi.ps1, deploy-and-sign.ps1, sign-release.ps1
- redeploy.ps1, tray-deploy.ps1, test-msi.ps1
- installer/msi/ alternative WXS setup
- Restored .github/workflows/ removed in mirror cleanup

.gitignore hardened: *.pfx, *.p12, *.key, *.pem, .env*
2026-04-22 15:18:11 +07:00

22 KiB

Audit Service Platform Variants — Implementation Guide

Triển khai IAuditService abstraction cho 3 nền tảng: Blazor WASM, Blazor Server, API-only


Kiến trúc tổng quan

graph TD
    subgraph "Shared Contract (INS.Shared)"
        I["IAuditService"]
    end

    subgraph "Blazor WASM"
        W["WasmAuditService"]
        W -->|"HTTP POST"| MC["Module Controller"]
        MC -->|"gRPC"| GS["LoggingGrpcService"]
        GS -->|"Publish"| RMQ
    end

    subgraph "Blazor Server"
        S["ServerAuditService"]
        S -->|"Direct inject"| PUB["IAuditLogPublisher"]
        PUB -->|"Publish"| RMQ
    end

    subgraph "API Only"
        A["ApiAuditService"]
        A -->|"Direct inject"| PUB
    end

    I --> W
    I --> S
    I --> A

    RMQ["RabbitMQ"] --> CON["AuditLogConsumerService"]
    CON --> DB["SQL Server"]

File 1: IAuditService.cs — Shared Contract

Location: lib/INS.Shared/Services/IAuditService.cs

namespace INS.Shared.Services
{
    /// <summary>
    /// Platform-agnostic audit logging interface.
    /// Implementations: WasmAuditService, ServerAuditService, ApiAuditService
    /// </summary>
    public interface IAuditService
    {
        /// <summary>
        /// Log INSERT operation
        /// </summary>
        Task LogInsertAsync(string tableName, string rowId, object? newData,
            string? schemaName = null, string? functionName = null);

        /// <summary>
        /// Log UPDATE operation
        /// </summary>
        Task LogUpdateAsync(string tableName, string rowId,
            object? oldData, object? newData,
            string? schemaName = null, string? primaryKeyColumns = null,
            string? functionName = null);

        /// <summary>
        /// Log DELETE operation
        /// </summary>
        Task LogDeleteAsync(string tableName, string rowId, object? oldData,
            string? schemaName = null, string? functionName = null);

        /// <summary>
        /// Log custom operation (APPROVE, REJECT, EXPORT, etc.)
        /// </summary>
        Task LogCustomAsync(string tableName, string operation,
            string? rowId = null, object? oldData = null, object? newData = null,
            string? schemaName = null, string? functionName = null);
    }
}

File 2: WasmAuditService.cs — Blazor WASM Variant

Location: lib/INS.DXComponents/Logging/WasmAuditService.cs
Flow: HttpClient POST → Module Controller → gRPC → RabbitMQ

using System.Net.Http.Json;
using System.Text.Json;
using INS.Shared.Services;
using INS.Module.Shared.Models;

namespace INS.DXComponents.Logging;

/// <summary>
/// Blazor WASM implementation: gửi audit log qua HTTP tới Module Backend.
/// Module Backend forward qua gRPC tới INS.Server → RabbitMQ.
/// 
/// Dùng khi: Client-side rendering, không có direct access tới RabbitMQ.
/// DI Lifetime: Scoped (per-circuit)
/// </summary>
public class WasmAuditService : IAuditService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IAuditUserProvider _userProvider;

    public WasmAuditService(
        IHttpClientFactory httpClientFactory,
        IAuditUserProvider userProvider)
    {
        _httpClientFactory = httpClientFactory;
        _userProvider = userProvider;
    }

    public Task LogInsertAsync(string tableName, string rowId, object? newData,
        string? schemaName = null, string? functionName = null)
        => SendAsync(tableName, "INSERT", rowId, null, newData, schemaName, functionName);

    public Task LogUpdateAsync(string tableName, string rowId,
        object? oldData, object? newData,
        string? schemaName = null, string? primaryKeyColumns = null,
        string? functionName = null)
        => SendAsync(tableName, "UPDATE", rowId, oldData, newData, schemaName, functionName);

    public Task LogDeleteAsync(string tableName, string rowId, object? oldData,
        string? schemaName = null, string? functionName = null)
        => SendAsync(tableName, "DELETE", rowId, oldData, null, schemaName, functionName);

    public Task LogCustomAsync(string tableName, string operation,
        string? rowId = null, object? oldData = null, object? newData = null,
        string? schemaName = null, string? functionName = null)
        => SendAsync(tableName, operation, rowId ?? "", oldData, newData, schemaName, functionName);

    private async Task SendAsync(string tableName, string operation, string rowId,
        object? oldData, object? newData, string? schemaName, string? functionName)
    {
        try
        {
            var user = await _userProvider.GetCurrentUserAsync();

            var dto = new RabbitMQAuditLogDto
            {
                TableName = tableName,
                RowId = rowId,
                Operation = operation,
                OldData = Serialize(oldData),
                NewData = Serialize(newData),
                ChangedBy = user.Username ?? user.UserId ?? "System",
                Timestamp = DateTime.UtcNow
            };

            using var client = _httpClientFactory.CreateClient("AuthenticatedClient");
            await client.PostAsJsonAsync("/api/logging/audit", dto);
        }
        catch
        {
            // Swallow - audit failure không break UI
        }
    }

    private static string? Serialize(object? data)
        => data is null ? null
         : data is string s ? s
         : JsonSerializer.Serialize(data);
}

File 3: ServerAuditService.cs — Blazor Server Variant

Location: lib/INS.DXComponents/Logging/ServerAuditService.cs
Flow: Direct inject IAuditLogPublisher → RabbitMQ (KHÔNG qua HTTP)

using System.Text.Json;
using INS.Shared.Services;

namespace INS.DXComponents.Logging;

/// <summary>
/// Blazor Server implementation: inject trực tiếp IAuditLogPublisher.
/// KHÔNG gọi HTTP tới chính server (tránh circular call).
/// 
/// Dùng khi: Server-side rendering, có direct access tới RabbitMQ.
/// DI Lifetime: Scoped (per-circuit)
/// </summary>
public class ServerAuditService : IAuditService
{
    private readonly IAuditLogPublisher _publisher;
    private readonly IAuditUserProvider _userProvider;
    private readonly ICircuitContextAccessor _circuitContext;

    public ServerAuditService(
        IAuditLogPublisher publisher,
        IAuditUserProvider userProvider,
        ICircuitContextAccessor circuitContext)
    {
        _publisher = publisher;
        _userProvider = userProvider;
        _circuitContext = circuitContext;
    }

    public Task LogInsertAsync(string tableName, string rowId, object? newData,
        string? schemaName = null, string? functionName = null)
        => PublishAsync(schemaName ?? "dbo", tableName, rowId, "INSERT",
            null, newData, null, functionName);

    public Task LogUpdateAsync(string tableName, string rowId,
        object? oldData, object? newData,
        string? schemaName = null, string? primaryKeyColumns = null,
        string? functionName = null)
        => PublishAsync(schemaName ?? "dbo", tableName, rowId, "UPDATE",
            oldData, newData, primaryKeyColumns, functionName);

    public Task LogDeleteAsync(string tableName, string rowId, object? oldData,
        string? schemaName = null, string? functionName = null)
        => PublishAsync(schemaName ?? "dbo", tableName, rowId, "DELETE",
            oldData, null, null, functionName);

    public Task LogCustomAsync(string tableName, string operation,
        string? rowId = null, object? oldData = null, object? newData = null,
        string? schemaName = null, string? functionName = null)
        => PublishAsync(schemaName ?? "dbo", tableName, rowId ?? "", operation,
            oldData, newData, null, functionName);

    private async Task PublishAsync(string schemaName, string tableName,
        string rowId, string operation,
        object? oldData, object? newData,
        string? primaryKeyColumns, string? functionName)
    {
        try
        {
            await _publisher.PublishAsync(
                schemaName: schemaName,
                tableName: tableName,
                rowId: rowId,
                operation: operation,
                oldData: Serialize(oldData),
                newData: Serialize(newData),
                primaryKeyColumns: primaryKeyColumns,
                functionName: functionName ?? "BlazorServer");
        }
        catch
        {
            // Swallow - audit failure không break UI
        }
    }

    private static string? Serialize(object? data)
        => data is null ? null
         : data is string s ? s
         : JsonSerializer.Serialize(data);
}

File 4: ApiAuditService.cs — API-only Variant

Location: src/INS.Backend/Services/ApiAuditService.cs
Flow: Direct inject IAuditLogPublisher → RabbitMQ

using System.Text.Json;
using INS.Shared.Services;

namespace INS.Backend.Services;

/// <summary>
/// API-only implementation: inject IAuditLogPublisher + IRequestContextAccessor.
/// Auto-enrich từ HTTP context (đã có CorrelationIdMiddleware).
/// 
/// Dùng khi: Pure API backend (không có Blazor component).
/// DI Lifetime: Scoped (per-request)
/// </summary>
public class ApiAuditService : IAuditService
{
    private readonly IAuditLogPublisher _publisher;

    public ApiAuditService(IAuditLogPublisher publisher)
    {
        _publisher = publisher;
    }

    public Task LogInsertAsync(string tableName, string rowId, object? newData,
        string? schemaName = null, string? functionName = null)
        => _publisher.PublishAsync(schemaName ?? "dbo", tableName, rowId,
            "INSERT", null, Serialize(newData), null, functionName);

    public Task LogUpdateAsync(string tableName, string rowId,
        object? oldData, object? newData,
        string? schemaName = null, string? primaryKeyColumns = null,
        string? functionName = null)
        => _publisher.PublishAsync(schemaName ?? "dbo", tableName, rowId,
            "UPDATE", Serialize(oldData), Serialize(newData), primaryKeyColumns, functionName);

    public Task LogDeleteAsync(string tableName, string rowId, object? oldData,
        string? schemaName = null, string? functionName = null)
        => _publisher.PublishAsync(schemaName ?? "dbo", tableName, rowId,
            "DELETE", Serialize(oldData), null, null, functionName);

    public Task LogCustomAsync(string tableName, string operation,
        string? rowId = null, object? oldData = null, object? newData = null,
        string? schemaName = null, string? functionName = null)
        => _publisher.PublishAsync(schemaName ?? "dbo", tableName, rowId ?? "",
            operation, Serialize(oldData), Serialize(newData), null, functionName);

    private static string? Serialize(object? data)
        => data is null ? null
         : data is string s ? s
         : JsonSerializer.Serialize(data);
}

File 5: IAuditUserProvider.cs — User Context Abstraction

Location: lib/INS.Shared/Services/IAuditUserProvider.cs

namespace INS.Shared.Services
{
    /// <summary>
    /// Abstraction để lấy thông tin user hiện tại, hoạt động trên cả 3 platform.
    /// Blazor WASM/Server: từ AuthenticationStateProvider
    /// API: từ HttpContext.User
    /// </summary>
    public interface IAuditUserProvider
    {
        Task<AuditUserInfo> GetCurrentUserAsync();
    }

    public record AuditUserInfo(
        string? UserId,
        string? Username,
        string? IpAddress = null,
        string? SessionId = null);
}

5a. Blazor Implementation (cả WASM và Server)

Location: lib/INS.DXComponents/Logging/BlazorAuditUserProvider.cs

using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;
using INS.Shared.Services;

namespace INS.DXComponents.Logging;

/// <summary>
/// Lấy user info từ AuthenticationStateProvider (hoạt động cả WASM + Server)
/// </summary>
public class BlazorAuditUserProvider : IAuditUserProvider
{
    private readonly AuthenticationStateProvider _authState;

    public BlazorAuditUserProvider(AuthenticationStateProvider authState)
    {
        _authState = authState;
    }

    public async Task<AuditUserInfo> GetCurrentUserAsync()
    {
        var state = await _authState.GetAuthenticationStateAsync();
        var user = state.User;

        if (user.Identity?.IsAuthenticated != true)
            return new AuditUserInfo(null, "Anonymous");

        return new AuditUserInfo(
            UserId: user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
            Username: user.FindFirst(ClaimTypes.Name)?.Value
                      ?? user.FindFirst("preferred_username")?.Value
        );
    }
}

5b. API Implementation

Location: src/INS.Backend/Services/ApiAuditUserProvider.cs

using Microsoft.AspNetCore.Http;
using System.Security.Claims;
using INS.Shared.Services;

namespace INS.Backend.Services;

/// <summary>
/// Lấy user info từ HttpContext (API-only, có HTTP pipeline)
/// </summary>
public class ApiAuditUserProvider : IAuditUserProvider
{
    private readonly IHttpContextAccessor _httpContext;

    public ApiAuditUserProvider(IHttpContextAccessor httpContext)
    {
        _httpContext = httpContext;
    }

    public Task<AuditUserInfo> GetCurrentUserAsync()
    {
        var user = _httpContext.HttpContext?.User;

        if (user?.Identity?.IsAuthenticated != true)
            return Task.FromResult(new AuditUserInfo(null, "System"));

        var ip = _httpContext.HttpContext?.Connection?.RemoteIpAddress?.ToString();

        return Task.FromResult(new AuditUserInfo(
            UserId: user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
            Username: user.FindFirst(ClaimTypes.Name)?.Value,
            IpAddress: ip
        ));
    }
}

File 6: ICircuitContextAccessor.cs — Blazor Server Context

Location: lib/INS.DXComponents/Logging/ICircuitContextAccessor.cs

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;

namespace INS.DXComponents.Logging;

/// <summary>
/// Blazor Server thay thế cho IRequestContextAccessor.
/// Capture HttpContext tại thời điểm kết nối SignalR (initial connection).
/// SAU ĐÓ SignalR không còn HttpContext → phải lưu lại.
/// </summary>
public interface ICircuitContextAccessor
{
    string? CorrelationId { get; }
    string? IpAddress { get; }
    string? UserAgent { get; }
    string? SessionId { get; }
    string CircuitId { get; }
}

/// <summary>
/// Capture context từ HttpContext lúc circuit mở.
/// Registered: Scoped (per-circuit)
/// </summary>
public class CircuitContextAccessor : ICircuitContextAccessor
{
    public string? CorrelationId { get; set; }
    public string? IpAddress { get; set; }
    public string? UserAgent { get; set; }
    public string? SessionId { get; set; }
    public string CircuitId { get; set; } = "";
}

/// <summary>
/// CircuitHandler populate CircuitContextAccessor khi circuit mở.
/// Capture IP, UserAgent, SessionId từ initial HTTP handshake.
/// </summary>
public class AuditCircuitHandler : CircuitHandler
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly CircuitContextAccessor _circuitContext;

    public AuditCircuitHandler(
        IHttpContextAccessor httpContextAccessor,
        CircuitContextAccessor circuitContext)
    {
        _httpContextAccessor = httpContextAccessor;
        _circuitContext = circuitContext;
    }

    public override Task OnCircuitOpenedAsync(Circuit circuit, CancellationToken ct)
    {
        var httpContext = _httpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            _circuitContext.CircuitId = circuit.Id;
            _circuitContext.IpAddress = httpContext.Connection.RemoteIpAddress?.ToString();
            _circuitContext.UserAgent = httpContext.Request.Headers.UserAgent.ToString();
            _circuitContext.SessionId = httpContext.Session?.Id;
            _circuitContext.CorrelationId = httpContext.Request.Headers["X-Correlation-ID"]
                .FirstOrDefault() ?? Guid.NewGuid().ToString("N");
        }
        return Task.CompletedTask;
    }
}

File 7: AuditServiceExtensions.cs — DI Registration

Location: lib/INS.DXComponents/Logging/AuditServiceExtensions.cs

using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.Extensions.DependencyInjection;
using INS.Shared.Services;

namespace INS.DXComponents.Logging;

public static class AuditServiceExtensions
{
    /// <summary>
    /// Blazor WASM: audit log qua HTTP → Module Controller → gRPC → RabbitMQ
    /// Yêu cầu: IHttpClientFactory đã cấu hình "AuthenticatedClient"
    /// </summary>
    public static IServiceCollection AddAuditService_BlazorWasm(this IServiceCollection services)
    {
        services.AddScoped<IAuditUserProvider, BlazorAuditUserProvider>();
        services.AddScoped<IAuditService, WasmAuditService>();
        return services;
    }

    /// <summary>
    /// Blazor Server: audit log direct inject → IAuditLogPublisher → RabbitMQ
    /// Yêu cầu: IAuditLogPublisher, IRabbitMQService đã đăng ký
    /// </summary>
    public static IServiceCollection AddAuditService_BlazorServer(this IServiceCollection services)
    {
        services.AddHttpContextAccessor();

        // Context capture từ SignalR initial connection
        services.AddScoped<CircuitContextAccessor>();
        services.AddScoped<ICircuitContextAccessor>(sp => sp.GetRequiredService<CircuitContextAccessor>());
        services.AddScoped<CircuitHandler, AuditCircuitHandler>();

        // Audit services
        services.AddScoped<IAuditUserProvider, BlazorAuditUserProvider>();
        services.AddScoped<IAuditService, ServerAuditService>();
        return services;
    }

    /// <summary>
    /// API-only: audit log direct inject → IAuditLogPublisher → RabbitMQ
    /// Yêu cầu: IAuditLogPublisher, IRequestContextAccessor đã đăng ký
    /// </summary>
    public static IServiceCollection AddAuditService_Api(this IServiceCollection services)
    {
        services.AddHttpContextAccessor();
        services.AddScoped<IAuditUserProvider, ApiAuditUserProvider>();
        services.AddScoped<IAuditService, ApiAuditService>();
        return services;
    }
}

File 8: INS_AuditLogger.razor — Updated Component (Multi-Platform)

Location: lib/INS.DXComponents/Logging/INS_AuditLogger.razor
Thay đổi: Sử dụng IAuditService thay vì LogService trực tiếp

@using INS.Shared.Services
@inject IAuditService AuditService

@code {
    /// <summary>
    /// Component ẩn để tự động log audit khi có thay đổi dữ liệu.
    /// Hoạt động trên CẢ 3 platform (WASM, Server, API).
    /// Sử dụng: <INS_AuditLogger @ref="auditLogger" />
    /// </summary>

    public Task LogInsertAsync(string tableName, string? rowId, object? newValue)
        => AuditService.LogInsertAsync(tableName, rowId ?? "", newValue);

    public Task LogUpdateAsync(string tableName, string? rowId,
        object? oldValue, object? newValue)
        => AuditService.LogUpdateAsync(tableName, rowId ?? "", oldValue, newValue);

    public Task LogDeleteAsync(string tableName, string? rowId, object? oldValue)
        => AuditService.LogDeleteAsync(tableName, rowId ?? "", oldValue);

    public Task LogCustomAsync(string tableName, string operation,
        string? rowId = null, object? oldValue = null, object? newValue = null)
        => AuditService.LogCustomAsync(tableName, operation, rowId, oldValue, newValue);
}

Cấu hình Program.cs theo nền tảng

Blazor WASM

// Program.cs (Client project)
builder.Services.AddAuditService_BlazorWasm();

Blazor Server

// Program.cs (Server project)
// Prerequisites
builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();

// Audit service
builder.Services.AddAuditService_BlazorServer();

API-only

// Program.cs (API project)
// Prerequisites
builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();

// Middleware pipeline
app.UseCorrelationId();
app.UseActivityLogging();

// Audit service
builder.Services.AddAuditService_Api();

Usage Pattern — Giống nhau trên cả 3 platform

Blazor Page

<INS_AuditLogger @ref="auditLogger" />

@code {
    private INS_AuditLogger auditLogger = default!;

    async Task SaveUser(UserDto user)
    {
        var oldData = await LoadUser(user.Id);
        await UpdateUserInDb(user);
        await auditLogger.LogUpdateAsync("Users", user.Id.ToString(), oldData, user);
    }
}

API Controller (inject trực tiếp)

[ApiController]
public class UsersController : ControllerBase
{
    private readonly IAuditService _audit;

    [HttpPut("{id}")]
    public async Task<IActionResult> Update(int id, UserDto dto)
    {
        var old = await _repo.GetById(id);
        await _repo.Update(dto);
        await _audit.LogUpdateAsync("Users", id.ToString(), old, dto,
            functionName: nameof(Update));
        return Ok();
    }
}

So sánh 3 variants

Aspect Blazor WASM Blazor Server API-only
Class WasmAuditService ServerAuditService ApiAuditService
Transport HTTP → gRPC → RMQ Direct → RMQ Direct → RMQ
Network hop 2 (HTTP + gRPC) 0 0
Latency ~10-50ms ~1-5ms ~1-5ms
User context BlazorAuditUserProvider BlazorAuditUserProvider ApiAuditUserProvider
Request context N/A (client-side) ICircuitContextAccessor IRequestContextAccessor
CorrelationId From server response header From initial SignalR handshake From CorrelationIdMiddleware
Requires RabbitMQ (module backend has it) Direct Direct
Dependencies IHttpClientFactory IAuditLogPublisher IAuditLogPublisher
Registration AddAuditService_BlazorWasm() AddAuditService_BlazorServer() AddAuditService_Api()