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
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*
694 lines
22 KiB
Markdown
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()` |
|