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

694 lines
22 KiB
Markdown

# 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
```mermaid
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`
```csharp
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`
```csharp
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)
```csharp
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`
```csharp
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`
```csharp
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`
```csharp
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`
```csharp
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`
```csharp
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`
```csharp
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
```razor
@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
```csharp
// Program.cs (Client project)
builder.Services.AddAuditService_BlazorWasm();
```
### Blazor Server
```csharp
// 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
```csharp
// 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
```razor
<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)
```csharp
[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()` |