@@ -0,0 +1,10 @@
|
||||
namespace ROLAC.API.Entities.Base;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in marker: entities implementing this are diffed by <c>AuditLogInterceptor</c>, which
|
||||
/// writes a before→after AuditLog row on every Create/Update/Delete. Applied only to business
|
||||
/// entities the church cares about — not to internal/high-churn rows (RefreshToken, log tables).
|
||||
/// </summary>
|
||||
public interface IAuditable
|
||||
{
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace ROLAC.API.Entities;
|
||||
/// expenses (its <see cref="Lines"/>). The payee name/address are snapshotted at
|
||||
/// issue time so the printed check is reproducible even if member data later changes.
|
||||
/// </summary>
|
||||
public class Check : SoftDeleteEntity
|
||||
public class Check : SoftDeleteEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string CheckNumber { get; set; } = null!;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace ROLAC.API.Entities;
|
||||
/// Singleton (Id == 1) holding the issuing church's identity, bank details, and the
|
||||
/// running check-number counter used when disbursing checks. Seeded on startup.
|
||||
/// </summary>
|
||||
public class ChurchProfile : AuditableEntity
|
||||
public class ChurchProfile : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = null!;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using ROLAC.API.Entities.Base;
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class Expense : SoftDeleteEntity
|
||||
public class Expense : SoftDeleteEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int MinistryId { get; set; }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using ROLAC.API.Entities.Base;
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class ExpenseCategoryGroup : AuditableEntity
|
||||
public class ExpenseCategoryGroup : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name_en { get; set; } = null!;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using ROLAC.API.Entities.Base;
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class ExpenseSubCategory : AuditableEntity
|
||||
public class ExpenseSubCategory : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int GroupId { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ using ROLAC.API.Entities.Base;
|
||||
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class Giving : AuditableEntity
|
||||
public class Giving : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int? MemberId { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ using ROLAC.API.Entities.Base;
|
||||
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class GivingCategory : AuditableEntity
|
||||
public class GivingCategory : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name_en { get; set; } = null!;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
namespace ROLAC.API.Entities.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// An append-only audit row recording a meaningful action: a data change (Create/Update/
|
||||
/// Delete with before→after values), a security event (login, role/permission change), or a
|
||||
/// key business action (check issued, expense approved, ...). Does NOT inherit AuditableEntity.
|
||||
/// </summary>
|
||||
public class AuditLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public LogLevelEnum Level { get; set; } = LogLevelEnum.Information;
|
||||
|
||||
/// <summary>One of <see cref="AuditActions"/>.</summary>
|
||||
public string Action { get; set; } = null!;
|
||||
|
||||
/// <summary>One of <see cref="AuditCategories"/> — drives the UI grouping.</summary>
|
||||
public string Category { get; set; } = null!;
|
||||
|
||||
public string? EntityName { get; set; }
|
||||
|
||||
/// <summary>String to cover int, Guid and string primary keys uniformly.</summary>
|
||||
public string? EntityId { get; set; }
|
||||
|
||||
/// <summary>JSON <c>{ "before": {...}, "after": {...} }</c> (jsonb column); sensitive fields masked.</summary>
|
||||
public string? Changes { get; set; }
|
||||
|
||||
/// <summary>Human-readable one-liner, e.g. "Check #1042 issued to Acme — $1,200.00".</summary>
|
||||
public string? Summary { get; set; }
|
||||
|
||||
public string? UserId { get; set; }
|
||||
/// <summary>Denormalized actor email — survives user deletion and avoids a join in the grid.</summary>
|
||||
public string? UserEmail { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public string? CorrelationId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Canonical audit action names (stored verbatim in <see cref="AuditLog.Action"/>).</summary>
|
||||
public static class AuditActions
|
||||
{
|
||||
public const string Create = "Create";
|
||||
public const string Update = "Update";
|
||||
public const string Delete = "Delete";
|
||||
public const string Login = "Login";
|
||||
public const string Logout = "Logout";
|
||||
public const string LoginFailed = "LoginFailed";
|
||||
public const string RoleChanged = "RoleChanged";
|
||||
public const string UserDeactivated = "UserDeactivated";
|
||||
public const string PermissionChanged = "PermissionChanged";
|
||||
public const string CheckIssued = "CheckIssued";
|
||||
public const string CheckVoided = "CheckVoided";
|
||||
public const string ExpenseApproved = "ExpenseApproved";
|
||||
public const string StatementFinalized = "StatementFinalized";
|
||||
|
||||
public static readonly IReadOnlyList<string> All =
|
||||
[
|
||||
Create, Update, Delete, Login, Logout, LoginFailed, RoleChanged,
|
||||
UserDeactivated, PermissionChanged, CheckIssued, CheckVoided,
|
||||
ExpenseApproved, StatementFinalized,
|
||||
];
|
||||
}
|
||||
|
||||
/// <summary>Top-level audit grouping (stored verbatim in <see cref="AuditLog.Category"/>).</summary>
|
||||
public static class AuditCategories
|
||||
{
|
||||
public const string DataChange = "DataChange";
|
||||
public const string Security = "Security";
|
||||
public const string Business = "Business";
|
||||
|
||||
public static readonly IReadOnlyList<string> All = [DataChange, Security, Business];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace ROLAC.API.Entities.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Persisted severity for system and audit logs. Byte-backed so it stores compactly
|
||||
/// as <c>smallint</c> and sorts/filters by ordinal. Deliberately omits the
|
||||
/// <see cref="MsLogLevel.None"/> sentinel (value 6) — "None" means "log nothing" and
|
||||
/// is meaningless once a row already exists.
|
||||
/// </summary>
|
||||
public enum LogLevelEnum : byte
|
||||
{
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Information = 2,
|
||||
Warning = 3,
|
||||
Error = 4,
|
||||
Critical = 5,
|
||||
}
|
||||
|
||||
public static class LogLevelMap
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a framework <see cref="MsLogLevel"/> to our persisted enum.
|
||||
/// <see cref="MsLogLevel.None"/> falls through to <see cref="LogLevelEnum.Critical"/>
|
||||
/// (it never reaches the sink because the floor filter drops it first).
|
||||
/// </summary>
|
||||
public static LogLevelEnum FromMs(MsLogLevel level) => level switch
|
||||
{
|
||||
MsLogLevel.Trace => LogLevelEnum.Trace,
|
||||
MsLogLevel.Debug => LogLevelEnum.Debug,
|
||||
MsLogLevel.Information => LogLevelEnum.Information,
|
||||
MsLogLevel.Warning => LogLevelEnum.Warning,
|
||||
MsLogLevel.Error => LogLevelEnum.Error,
|
||||
MsLogLevel.Critical => LogLevelEnum.Critical,
|
||||
_ => LogLevelEnum.Critical,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace ROLAC.API.Entities.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// An append-only operational log row — one per persisted framework/app log event,
|
||||
/// including every unhandled API exception captured by ExceptionHandlingMiddleware.
|
||||
/// Intentionally does NOT inherit AuditableEntity: these rows are never updated and
|
||||
/// must not be re-stamped or re-audited (that would recurse through the log pipeline).
|
||||
/// </summary>
|
||||
public class SystemLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public DateTimeOffset Timestamp { get; set; }
|
||||
public LogLevelEnum Level { get; set; }
|
||||
|
||||
/// <summary>The ILogger category (source), e.g. "ROLAC.API.Controllers.GivingsController".</summary>
|
||||
public string Category { get; set; } = null!;
|
||||
public int? EventId { get; set; }
|
||||
public string Message { get; set; } = null!;
|
||||
|
||||
/// <summary>Full <c>exception.ToString()</c> (type + message + stack), when present.</summary>
|
||||
public string? Exception { get; set; }
|
||||
|
||||
public string? RequestPath { get; set; }
|
||||
public string? HttpMethod { get; set; }
|
||||
public int? StatusCode { get; set; }
|
||||
|
||||
/// <summary>The acting user id ("sub" claim), or null for background/system events.</summary>
|
||||
public string? UserId { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public string? CorrelationId { get; set; }
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using ROLAC.API.Entities.Base;
|
||||
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class Member : SoftDeleteEntity
|
||||
public class Member : SoftDeleteEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string FirstName_en { get; set; } = null!;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using ROLAC.API.Entities.Base;
|
||||
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class Ministry
|
||||
public class Ministry : IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name_en { get; set; } = null!;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using ROLAC.API.Entities.Base;
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class MonthlyStatement : AuditableEntity
|
||||
public class MonthlyStatement : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int Year { get; set; }
|
||||
|
||||
@@ -2,7 +2,7 @@ using ROLAC.API.Entities.Base;
|
||||
|
||||
namespace ROLAC.API.Entities;
|
||||
|
||||
public class OfferingSession : AuditableEntity
|
||||
public class OfferingSession : AuditableEntity, IAuditable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public DateOnly SessionDate { get; set; }
|
||||
|
||||
Reference in New Issue
Block a user