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*
31 KiB
Audit Log Flow - Tài liệu chi tiết luồng hoạt động
Mục đích: Tài liệu này mô tả đầy đủ chi tiết luồng ghi Audit Log trong hệ thống INS, đủ để tái tạo lại chức năng tương tự.
1. Tổng quan kiến trúc
Hệ thống Audit Log sử dụng kiến trúc event-driven với RabbitMQ làm message broker, tách biệt hoàn toàn việc ghi log khỏi business logic.
graph TD
subgraph "TIER 1: Frontend (Blazor WASM)"
A["INS_AuditLogger.razor<br/>(DXComponents)"]
B["LogService.cs<br/>(HTTP Client)"]
end
subgraph "TIER 2: Module Backend"
C["Module Controller<br/>(REST API /api/logging/audit)"]
D["gRPC Client<br/>(INS.LoggingController)"]
end
subgraph "TIER 3: INS.WASM.AUTH Server"
E["CorrelationIdMiddleware"]
F["ActivityLoggingMiddleware"]
G["LoggingGrpcService"]
H["AuditLogPublisher"]
I["IRabbitMQService.SendAsync()"]
end
subgraph "Message Broker"
J["RabbitMQ<br/>Exchange: ins.logs.exchange<br/>(topic)"]
K["Queue: ins.server.auditlog.queue<br/>Binding: logs.audit.#"]
end
subgraph "Consumer Layer"
L["AuditLogConsumerService<br/>(BackgroundService)"]
M["BaseRabbitMqConsumerService<br/>(lazy init + retry)"]
end
subgraph "Database"
N["SQL Server<br/>[Log].[AuditLogs]"]
end
A --> B
B -->|"POST /api/logging/audit"| C
C -->|"gRPC"| D
D -->|"gRPC SendAuditLog()"| G
G --> I
H --> I
F --> I
I -->|"Publish"| J
J -->|"Route: logs.audit.*"| K
K -->|"Consume"| L
L --> M
L -->|"EF Core INSERT"| N
E -->|"Inject CorrelationId"| G
E -->|"Inject CorrelationId"| H
3 Đường vào (Entry Points) cho Audit Log
| # | Entry Point | Routing Key | Khi nào dùng |
|---|---|---|---|
| 1 | gRPC từ Module (LoggingGrpcService) |
logs.audit.server |
Module Frontend gọi qua LogService → Module Controller → gRPC |
| 2 | AuditLogPublisher (direct call) | logs.audit.data |
Backend code gọi trực tiếp khi có data change |
| 3 | REST API (TestRabbitMQController) |
logs.audit.server |
Test/debug endpoint |
2. Chi tiết từng thành phần
2.1. Frontend Blazor: INS_AuditLogger.razor
Location:
lib/INS.DXComponents/Logging/INS_AuditLogger.razor
Chức năng: Component ẩn (không render UI), inject vào Blazor page để ghi audit log cho thao tác CRUD.
Cách sử dụng trong page:
<INS_AuditLogger @ref="auditLogger" />
@code {
private INS_AuditLogger auditLogger = default!;
// Khi INSERT
await auditLogger.LogInsertAsync("TableName", "rowId", newObject);
// Khi UPDATE
await auditLogger.LogUpdateAsync("TableName", "rowId", oldObject, newObject);
// Khi DELETE
await auditLogger.LogDeleteAsync("TableName", "rowId", oldObject);
// Custom operation
await auditLogger.LogCustomAsync("TableName", "APPROVE", "rowId", oldObj, newObj);
}
Nội bộ hoạt động:
OnInitializedAsync()→ LấyUserIdvàUsernametừAuthenticationStateProvider- Serialize old/new data thành JSON bằng
System.Text.Json.JsonSerializer - Gọi
LogService.AuditAsync()với các tham số
Dependencies: LogService, AuthenticationStateProvider
DI Registration:
services.AddScoped<INS_AuditLogger>();
2.2. LogService (HTTP Client - 3-tier Bridge)
Location:
lib/INS.DXComponents/Logging/LogService.cs
Chức năng: Bridge giữa Frontend và Module Backend, gửi log qua HTTP POST.
Flow: Frontend → LogService → HTTP POST → Module Controller → gRPC → INS.Server
public class LogService
{
private readonly IHttpClientFactory _httpClientFactory;
// Sử dụng "AuthenticatedClient" - tự động gắn Access Token
private HttpClient CreateClient() => _httpClientFactory.CreateClient("AuthenticatedClient");
public async Task<bool> AuditAsync(
string tableName,
string operation,
string? rowId = null,
string? oldValue = null,
string? newValue = null,
string? userId = null,
string? username = null)
{
var dto = new RabbitMQAuditLogDto
{
TableName = tableName,
RowId = rowId ?? string.Empty,
Operation = operation,
OldData = oldValue,
NewData = newValue,
ChangedBy = username ?? userId ?? "System",
Timestamp = DateTime.UtcNow
};
using var client = CreateClient();
var response = await client.PostAsJsonAsync("/api/logging/audit", dto);
return response.IsSuccessStatusCode;
}
}
Đặc điểm quan trọng:
- Sử dụng
IHttpClientFactoryvới named client"AuthenticatedClient"(tự gắn JWT token) - Swallow exceptions (return
false) → Log failure KHÔNG break UI flow - POST tới
/api/logging/audittrên Module Backend
DI Registration:
// Extension method
services.AddINSLogging(); // → AddScoped<LogService>()
2.3. gRPC Proto Definition
Location:
INS.WASM.AUTH/src/INS.Backend/Protos/logging.proto
syntax = "proto3";
option csharp_namespace = "INS.LoggingController";
import "google/protobuf/timestamp.proto";
package logging;
service LoggingService {
rpc SendAuditLog (AuditLogRequest) returns (LogResponse);
rpc SendAppLog (AppLogRequest) returns (LogResponse);
rpc SendActivityLog (ActivityLogRequest) returns (LogResponse);
}
message AuditLogRequest {
string table_name = 1;
string row_id = 2;
string operation = 3; // INSERT, UPDATE, DELETE
string old_data = 4; // JSON
string new_data = 5; // JSON
string changed_by = 6;
google.protobuf.Timestamp timestamp = 7;
string user_id = 8;
repeated string roles = 9;
}
message LogResponse {
bool success = 1;
string message = 2;
}
NuGet package: INS.LoggingController (v1.0.4) - chứa generated gRPC client/server code
2.4. LoggingGrpcService (gRPC → RabbitMQ Bridge)
Location:
INS.WASM.AUTH/src/INS.Backend/Services/Grpc/LoggingGrpcService.cs
Chức năng: Nhận gRPC call từ modules, chuyển đổi sang RabbitMQAuditLogDto, publish lên RabbitMQ.
public class LoggingGrpcService : LoggingService.LoggingServiceBase
{
private readonly IRabbitMQService _rabbitMQService;
public override async Task<LogResponse> SendAuditLog(
AuditLogRequest request, ServerCallContext context)
{
// Map Protobuf → DTO
var dto = new RabbitMQAuditLogDto
{
TableName = request.TableName,
RowId = request.RowId,
Operation = request.Operation,
OldData = request.OldData,
NewData = request.NewData,
ChangedBy = request.ChangedBy,
Timestamp = request.Timestamp.ToDateTime()
};
// Publish tới RabbitMQ
await _rabbitMQService.SendAsync(
exchangeName: "ins.logs.exchange",
routingKey: "logs.audit.server",
message: dto
);
return new LogResponse { Success = true, Message = "Audit log sent successfully" };
}
}
Routing Key: logs.audit.server (fixed)
Exchange: ins.logs.exchange (topic exchange)
2.5. AuditLogPublisher (Direct Publisher)
Location:
INS.WASM.AUTH/src/INS.Backend/Services/AuditLogPublisher.cs
Chức năng: Publisher trực tiếp cho backend code, tự động enrich message với request context và generate rollback SQL.
Interface:
public interface IAuditLogPublisher
{
// Simplified API - auto-enrich từ request context
Task PublishAsync(
string schemaName,
string tableName,
string rowId,
string operation, // INSERT, UPDATE, DELETE
string? oldData, // JSON
string? newData, // JSON
string? primaryKeyColumns = null, // JSON array: ["Id"] hoặc ["Code", "Version"]
string? functionName = null);
// Full control API
Task PublishAsync(AuditLogMessage message);
}
Luồng xử lý chi tiết:
PublishAsync(params)
│
├── 1. Generate Rollback SQL
│ └── IRollbackSqlGenerator.GenerateRollbackSql()
│
├── 2. Detect Changed Columns (nếu UPDATE)
│ └── IRollbackSqlGenerator.GetChangedColumns(oldData, newData)
│
├── 3. Build AuditLogMessage (enrich từ IRequestContextAccessor)
│ ├── WHO: UserId, Username, IpAddress, SessionId, MachineName
│ ├── WHAT: Schema, Table, RowId, Operation, OldData, NewData, ChangedColumns
│ ├── WHEN: Timestamp (UtcNow)
│ ├── HOW TO ROLLBACK: RollbackSql
│ └── WHERE FROM: ModuleId, CorrelationId, ParentCorrelationId, RequestUrl, HttpMethod
│
├── 4. Kiểm tra Enabled
│ └── RabbitMQ:Enabled && RabbitMQ:AuditLog:Enabled
│
└── 5. Publish lên RabbitMQ
└── IRabbitMQService.SendAsync(exchangeName, routingKey, message)
Configuration (appsettings.json):
{
"RabbitMQ": {
"Enabled": true,
"AuditLog": {
"Enabled": true,
"ExchangeName": "ins.logs.exchange",
"PublishRoutingKey": "logs.audit.data"
}
},
"ModuleRegistration": {
"ModuleId": "INS.Server"
}
}
Dependencies:
| Service | Lifetime | Chức năng |
|---|---|---|
IRabbitMQService? |
Singleton | Publish message (optional - nullable) |
IRequestContextAccessor |
Scoped | Lấy context từ HTTP request hiện tại |
IRollbackSqlGenerator |
Singleton | Generate rollback SQL + detect changed columns |
IConfiguration |
Singleton | Đọc config |
DI Registration:
builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();
Error handling: Swallow exceptions - audit log failure KHÔNG break business flow:
catch (Exception ex)
{
_logger.LogError(ex, "Failed to publish AuditLog for {Table}/{Operation}", ...);
// Don't throw - audit log failure shouldn't break the main flow
}
2.6. CorrelationIdMiddleware (Distributed Tracing)
Location:
INS.WASM.AUTH/src/INS.Backend/Middleware/CorrelationIdMiddleware.cs
Chức năng: Inject/generate CorrelationId cho mỗi HTTP request, cho phép trace toàn bộ luồng từ request → audit log → database.
HTTP Headers sử dụng:
| Header | Mục đích | Auto-generate? |
|---|---|---|
X-Correlation-ID |
Unique ID cho request | ✅ Guid.NewGuid() nếu không có |
X-Parent-Correlation-ID |
ID của request cha (chained calls) | ❌ |
X-Trace-Span-ID |
Span ID cho distributed tracing | ✅ 16-char substring |
X-Module-ID |
Module nào gọi request | ❌ |
X-Sequence-Number |
Thứ tự trong chain | ❌ |
Luồng xử lý:
- Đọc/generate
CorrelationIdtừ request header - Populate
IRequestContextAccessor(scoped service) - Store vào
HttpContext.Items - Gắn
CorrelationIdvào response header - Gọi
_next(context)→ pipeline tiếp tục
Middleware registration order:
app.UseCorrelationId(); // ← PHẢI trước các middleware khác
app.UseActivityLogging(); // Activity log middleware
// ... other middleware
2.7. IRequestContextAccessor (Scoped Context)
Location:
INS.WASM.AUTH/src/INS.Backend/Middleware/IRequestContextAccessor.cs
Chức năng: Scoped service chứa thông tin request hiện tại, được populate bởi CorrelationIdMiddleware và ActivityLoggingMiddleware.
public interface IRequestContextAccessor
{
// Tracing
string? CorrelationId { get; set; }
string? ParentCorrelationId { get; set; }
string? TraceSpanId { get; set; }
string? ModuleId { get; set; }
int SequenceNumber { get; set; }
// Request
string? RequestUrl { get; set; }
string? HttpMethod { get; set; }
string? RequestBody { get; set; }
string? ResponseBody { get; set; }
string? IpAddress { get; set; }
string? UserAgent { get; set; }
// User
string? UserId { get; set; }
string? Username { get; set; }
string? SessionId { get; set; }
// Timing
DateTime RequestStartTime { get; set; }
long GetDurationMs();
// System
string? GetSystemState();
}
Lifetime: Scoped → mỗi HTTP request có một instance riêng, đảm bảo data isolation.
2.8. RollbackSqlGenerator
Location:
INS.WASM.AUTH/src/INS.Backend/Services/RollbackSqlGenerator.cs
Chức năng: Tự động generate SQL để rollback thay đổi dựa trên operation type.
Logic:
| Operation | Rollback SQL | Input cần |
|---|---|---|
INSERT |
DELETE FROM [schema].[table] WHERE [pk] = [value] |
PK columns + newData |
UPDATE |
UPDATE [schema].[table] SET [cols] = [old_values] WHERE [pk] = [value] |
PK columns + oldData |
DELETE |
INSERT INTO [schema].[table] ([cols]) VALUES ([old_values]) |
oldData |
GetChangedColumns(): So sánh oldData và newData (JSON), trả về danh sách columns đã thay đổi.
WHERE clause strategy (ưu tiên):
- Parse
rowIdas JSON object →{"Id": 123, "Code": "ABC"}→[Id] = 123 AND [Code] = N'ABC' - Dùng
primaryKeyColumns+ data → extract PK values từ data - Fallback:
[Id] = rowId(int/Guid/string detection)
Value formatting:
null→NULLtrue/false→1/0- number → literal
- string →
N'escaped_value'(SQL injection protection via'→'')
2.9. Shared DTO: RabbitMQAuditLogDto
Location:
lib/INS.Shared/Models/RabbitMQLogDtos.cs
public class RabbitMQAuditLogDto
{
public string TableName { get; set; } = string.Empty;
public string RowId { get; set; } = string.Empty;
public string Operation { get; set; } = string.Empty; // INSERT, UPDATE, DELETE
public string? OldData { get; set; } // JSON
public string? NewData { get; set; } // JSON
public string? ChangedBy { get; set; }
public string? DeviceName { get; set; }
public DateTime? Timestamp { get; set; }
public string? CorrelationId { get; set; }
}
Important
RabbitMQAuditLogDto(lightweight, dùng cho gRPC path) khác vớiAuditLogMessage(full, dùng cho AuditLogPublisher). Consumer phải handle cả 2 format.
2.10. AuditLogMessage (Full Message Model)
Location:
INS.Backend/Models/RabbitMQLogModels.cs(cả INS.Server và INS.WASM.AUTH)
public class AuditLogMessage
{
// WHO
public string? UserId { get; set; }
public string? ChangedBy { get; set; }
public string? IpAddress { get; set; }
public string? SessionId { get; set; }
public string? DeviceName { get; set; }
// WHAT
public string SchemaName { get; set; }
public string TableName { get; set; }
public string RowId { get; set; }
public string? PrimaryKeyColumns { get; set; }
public string Operation { get; set; } // INSERT, UPDATE, DELETE
public string? OldData { get; set; } // JSON
public string? NewData { get; set; } // JSON
public string? ChangedColumns { get; set; }
// WHEN
public DateTime? Timestamp { get; set; }
// HOW TO ROLLBACK
public string? RollbackSql { get; set; }
// TRACING
public string? ModuleId { get; set; }
public string? CorrelationId { get; set; }
public string? ParentCorrelationId { get; set; }
public string? RequestUrl { get; set; }
public string? HttpMethod { get; set; }
public string? RequestBody { get; set; }
public string? FunctionName { get; set; }
}
2.11. RabbitMQ Configuration
Exchange: ins.logs.exchange (type: topic)
Routing Keys:
| Publisher | Routing Key | Mô tả |
|---|---|---|
LoggingGrpcService.SendAuditLog() |
logs.audit.server |
Từ module gRPC |
AuditLogPublisher.PublishAsync() |
logs.audit.data |
Từ backend direct call |
ActivityLoggingMiddleware |
logs.activity |
Auto HTTP tracking |
Queue Binding: ins.server.auditlog.queue bind với pattern logs.audit.# → match cả logs.audit.server và logs.audit.data
Core Library: INS.RabbitMQ (v1.2.1, RabbitMQ.Client 7.2.0)
- Interface:
IRabbitMQService - Methods:
SendAsync(exchange, routingKey, message),CreateConsumer<T>()
2.12. BaseRabbitMqConsumerService (Consumer Infrastructure)
Location:
INS.WASM.AUTH/src/INS.Backend/Services/Consumers/BaseRabbitMqConsumerService.cs
Chức năng: Abstract base class cho tất cả consumer, kế thừa BackgroundService.
Đặc điểm kiến trúc:
- Lazy initialization: Không connect RabbitMQ tại startup, đợi 5s rồi mới connect
- Auto-retry: Nếu connect fail, retry mỗi
RetryIntervalSeconds(default: 30s) - Fluent API: Sử dụng
IRabbitMQService.CreateConsumer<T>()fluent builder
public abstract class BaseRabbitMqConsumerService<TMessage> : BackgroundService
where TMessage : class
{
protected readonly string QueueName;
protected readonly string ExchangeName;
protected readonly string RoutingKey;
protected readonly ushort PrefetchCount; // default: 10
protected readonly bool Enabled;
protected readonly int RetryIntervalSeconds; // default: 30
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (!Enabled) return;
await Task.Delay(5_000); // Đợi app khởi động
while (!stoppingToken.IsCancellationRequested)
{
if (!_isConnected)
{
await TryConnectAsync(stoppingToken);
}
if (!_isConnected)
{
await Task.Delay(RetryIntervalSeconds * 1000);
}
else
{
await Task.Delay(60_000); // Health check mỗi 1 phút
}
}
}
private async Task TryConnectAsync(CancellationToken ct)
{
// Fluent API registration
await RabbitMQService.CreateConsumer<TMessage>()
.WithQueue(QueueName)
.WithExchange(ExchangeName, RoutingKey)
.WithPrefetchCount(PrefetchCount)
.WithHandler(async message => await ProcessMessageAsync(message))
.RegisterAsync();
}
// Subclass implement: return true = ACK, false = NACK (requeue)
protected abstract Task<bool> ProcessMessageAsync(TMessage message);
}
Config từ appsettings (section name truyền qua constructor):
{
"RabbitMQ": {
"Enabled": true,
"RetryIntervalSeconds": 30,
"AuditLog": {
"Enabled": true,
"QueueName": "ins.server.auditlog.queue",
"ExchangeName": "ins.logs.exchange",
"RoutingKey": "logs.audit.#",
"PrefetchCount": 10
}
}
}
2.13. AuditLogConsumerService (Write to DB)
Location:
INS.WASM.AUTH/src/INS.Backend/Services/Consumers/AuditLogConsumerService.cs
Chức năng: Consume AuditLogMessage từ RabbitMQ và INSERT vào [Log].[audit_log] table.
public class AuditLogConsumerService : BaseRabbitMqConsumerService<AuditLogMessage>
{
public AuditLogConsumerService(
IServiceProvider serviceProvider,
IConfiguration configuration,
ILogger<AuditLogConsumerService> logger)
: base(serviceProvider, configuration, logger, "AuditLog") // config section
{ }
protected override async Task<bool> ProcessMessageAsync(AuditLogMessage message)
{
// 1. Validate required fields
if (string.IsNullOrEmpty(message.TableName) ||
string.IsNullOrEmpty(message.RowId) ||
string.IsNullOrEmpty(message.Operation))
{
return true; // ACK invalid message (skip, không requeue)
}
// 2. Tạo scope mới cho DbContext (scoped service)
using var scope = ServiceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AuthDbContext>();
// 3. Map message → Entity
var auditLog = new AuditLog
{
// WHO
UserId = message.UserId,
ChangedBy = message.ChangedBy,
DeviceName = message.DeviceName,
IpAddress = message.IpAddress,
SessionId = message.SessionId,
// WHAT
SchemaName = message.SchemaName,
TableName = message.TableName,
RowId = message.RowId,
PrimaryKeyColumns = message.PrimaryKeyColumns,
Operation = message.Operation,
OldData = message.OldData,
NewData = message.NewData,
ChangedColumns = message.ChangedColumns,
// WHEN
ChangedAt = message.Timestamp ?? DateTime.UtcNow,
// HOW TO ROLLBACK
RollbackSql = message.RollbackSql,
RollbackStatus = "NotRolledBack",
// WHERE FROM
ModuleId = message.ModuleId,
CorrelationId = message.CorrelationId,
ParentCorrelationId = message.ParentCorrelationId,
RequestUrl = message.RequestUrl,
HttpMethod = message.HttpMethod,
RequestBody = message.RequestBody,
FunctionName = message.FunctionName
};
// 4. Insert vào database
dbContext.AuditLogs.Add(auditLog);
await dbContext.SaveChangesAsync();
return true; // ACK
}
}
ACK/NACK strategy:
return true(ACK) → Message processed, remove from queuereturn truecho invalid message → Skip, không requeue vĩnh viễnreturn false(NACK) → Exception xảy ra, requeue message
DI: Consumer là BackgroundService, registered as HostedService:
builder.Services.AddHostedService<AuditLogConsumerService>();
2.14. Database Entity: AuditLog
Location:
INS.WASM.AUTH/src/INS.Backend/Data/LogModels/AuditLog.cs
Table: [log].[audit_log]
[Table("audit_log", Schema = "log")]
public class AuditLog
{
[Key]
[Column("audit_id")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long AuditId { get; set; }
// WHO
[Column("user_id"), MaxLength(255)]
public string? UserId { get; set; }
[Column("changed_by"), MaxLength(255)]
public string? ChangedBy { get; set; }
[Column("device_name"), MaxLength(255)]
public string? DeviceName { get; set; }
[Column("ip_address"), MaxLength(45)]
public string? IpAddress { get; set; }
[Column("session_id"), MaxLength(255)]
public string? SessionId { get; set; }
// WHAT
[Column("schema_name"), MaxLength(128)]
public string? SchemaName { get; set; }
[Required, Column("table_name"), MaxLength(255)]
public string TableName { get; set; }
[Required, Column("row_id")]
public string RowId { get; set; }
[Column("primary_key_columns"), MaxLength(500)]
public string? PrimaryKeyColumns { get; set; }
[Required, Column("operation"), MaxLength(10)]
public string Operation { get; set; }
[Column("old_data")]
public string? OldData { get; set; }
[Column("new_data")]
public string? NewData { get; set; }
[Column("changed_columns")]
public string? ChangedColumns { get; set; }
// WHEN
[Required, Column("changed_at")]
public DateTime ChangedAt { get; set; }
// HOW TO ROLLBACK
[Column("rollback_sql")]
public string? RollbackSql { get; set; }
[Column("rollback_status"), MaxLength(20)]
public string? RollbackStatus { get; set; } // NotRolledBack, SUCCESS, FAILED
[Column("rollback_at")]
public DateTime? RollbackAt { get; set; }
[Column("rollback_by"), MaxLength(255)]
public string? RollbackBy { get; set; }
// WHERE FROM
[Column("module_id"), MaxLength(100)]
public string? ModuleId { get; set; }
[Column("correlation_id"), MaxLength(255)]
public string? CorrelationId { get; set; }
[Column("parent_correlation_id"), MaxLength(255)]
public string? ParentCorrelationId { get; set; }
[Column("request_url"), MaxLength(2048)]
public string? RequestUrl { get; set; }
[Column("http_method"), MaxLength(10)]
public string? HttpMethod { get; set; }
[Column("request_body")]
public string? RequestBody { get; set; }
[Column("function_name"), MaxLength(255)]
public string? FunctionName { get; set; }
}
Database Indexes (từ migration SQL):
CREATE INDEX [idx_audit_log_changed_at] ON [Log].[AuditLogs] ([changed_at]);
CREATE INDEX [idx_audit_log_changed_by] ON [Log].[AuditLogs] ([changed_by]);
CREATE INDEX [idx_audit_log_correlation_id] ON [Log].[AuditLogs] ([correlation_id]);
CREATE INDEX [idx_audit_log_table_name] ON [Log].[AuditLogs] ([table_name]);
CREATE INDEX [idx_audit_log_table_time] ON [Log].[AuditLogs] ([table_name], [changed_at]);
3. Full Flow: Từ UI Click đến Database
Flow 1: Frontend Blazor gọi audit (3-tier path)
1. User click "Save" trên Blazor page
2. Page code gọi: await auditLogger.LogUpdateAsync("Users", "123", oldUser, newUser)
3. INS_AuditLogger:
├── Serialize oldUser/newUser → JSON
└── Gọi LogService.AuditAsync(tableName, operation, rowId, oldJson, newJson, userId, username)
4. LogService:
├── Tạo RabbitMQAuditLogDto
└── POST /api/logging/audit → Module Backend Controller
5. Module Backend Controller:
├── Nhận DTO
└── Gọi gRPC SendAuditLog() tới INS.WASM.AUTH
6. LoggingGrpcService (INS.WASM.AUTH):
├── Map AuditLogRequest → RabbitMQAuditLogDto
└── IRabbitMQService.SendAsync("ins.logs.exchange", "logs.audit.server", dto)
7. RabbitMQ:
├── Nhận message tại exchange "ins.logs.exchange"
└── Route theo key "logs.audit.server" → match "logs.audit.#" → queue "ins.server.auditlog.queue"
8. AuditLogConsumerService (BackgroundService):
├── Consume message từ queue
├── Validate required fields
├── Map AuditLogMessage → AuditLog entity
└── dbContext.AuditLogs.Add() + SaveChangesAsync()
9. SQL Server: INSERT INTO [log].[audit_log] (...)
Flow 2: Backend direct publish (enriched path)
1. Backend API handler thực hiện data change
2. Code gọi: await _auditLogPublisher.PublishAsync("dbo", "Users", "123", "UPDATE", oldJson, newJson, "[\"Id\"]", "UpdateUser")
3. AuditLogPublisher:
├── Generate rollback SQL: UPDATE [dbo].[Users] SET [...] WHERE [Id] = 123
├── Detect changed columns: ["Name", "Email"]
├── Enrich từ IRequestContextAccessor:
│ ├── UserId, Username, IpAddress, SessionId
│ ├── CorrelationId (từ CorrelationIdMiddleware)
│ ├── RequestUrl, HttpMethod
│ └── ModuleId
└── IRabbitMQService.SendAsync("ins.logs.exchange", "logs.audit.data", message)
4. RabbitMQ → Queue → Consumer → Database (giống Flow 1 step 7-9)
4. Module Extension Pattern (CDE Example)
CDE module có hệ thống audit log riêng biệt (domain-specific), KHÁC với system-level audit log ở trên:
CDEAuditController
Location:
INS.CDE/src/INS.CDE.Backend/Controllers/CDE/CDEAuditController.cs
- Table: CDE có bảng
CDEActivityLogsriêng (không dùng chung[Log].[audit_log]) - Entity fields:
ProjectId,DocumentId,EntityType,EntityId,EntityCode,Action,Details,IPAddress,UserAgent - Endpoints:
GET /api/cde/audit/logs- Lấy logs với filters (project, document, user, action, date range)GET /api/cde/audit/documents/{id}/logs- Logs theo documentGET /api/cde/audit/stats- Statistics cho dashboard
Module Config
// ModuleOptions.cs
public bool EnableAuditLogging { get; set; } = true;
Note
Modules có thể: (1) Sử dụng system-level audit log qua
LogService/INS_AuditLogger→ ghi vào[Log].[audit_log], HOẶC (2) Có bảng audit log riêng (như CDE) cho domain-specific tracking.
5. DI Registration Summary
INS.WASM.AUTH/Program.cs
// Infrastructure
builder.Services.AddScoped<IRequestContextAccessor, RequestContextAccessor>();
builder.Services.AddSingleton<IRollbackSqlGenerator, RollbackSqlGenerator>();
// Publisher
builder.Services.AddScoped<IAuditLogPublisher, AuditLogPublisher>();
// Consumers (BackgroundServices)
builder.Services.AddHostedService<AuditLogConsumerService>();
builder.Services.AddHostedService<AppLogConsumerService>();
builder.Services.AddHostedService<ActivityLogConsumerService>();
Middleware Pipeline Order
app.UseCorrelationId(); // 1. Generate/inject CorrelationId
app.UseActivityLogging(); // 2. Auto-capture HTTP → Activity Log
// ... Authentication, Authorization
// ... Controllers, gRPC endpoints
Frontend (Blazor WASM)
services.AddINSLogging(); // → AddScoped<LogService>()
services.AddScoped<INS_AuditLogger>();
6. Checklist để tái tạo chức năng
- Shared Models: Tạo
RabbitMQAuditLogDtovàAuditLogMessage - Proto file: Định nghĩa
logging.protovớiAuditLogRequestmessage - gRPC Service: Implement
LoggingGrpcServicenhận request, map → DTO, publish RabbitMQ - RabbitMQ Core: Setup
IRabbitMQServicevớiSendAsync()vàCreateConsumer<T>()fluent API - CorrelationIdMiddleware: Inject/generate
X-Correlation-IDmỗi request - IRequestContextAccessor: Scoped service chứa request context (user, IP, correlation, URL)
- RollbackSqlGenerator: Generate rollback SQL dựa trên operation type
- AuditLogPublisher: Publisher enriches message từ context, generate rollback, publish RabbitMQ
- BaseRabbitMqConsumerService: Abstract BackgroundService với lazy init + retry logic
- AuditLogConsumerService: Consume message → validate → map → EF Core INSERT
- AuditLog Entity: EF Core entity map tới
[log].[audit_log]table - Database Schema: Create table với indexes trên
changed_at,changed_by,correlation_id,table_name - LogService: Frontend HTTP client bridge (POST → Module Controller → gRPC)
- INS_AuditLogger: Blazor component wrapper (serialize old/new → LogService)
- DI Registration: Register tất cả services với correct lifetimes
- Configuration: appsettings.json cho RabbitMQ exchange/queue/routing keys