Skip to content

Commit 1135b85

Browse files
Merge pull request #25 from VoxenLabs/feature/#24/create-audit-log-implementation
Create audit log implementation
2 parents d0ff385 + 5991e79 commit 1135b85

File tree

19 files changed

+500
-9
lines changed

19 files changed

+500
-9
lines changed

Directory.Packages.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
1717
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
1818
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
19+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
1920
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
2021
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" />
2122
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
2223
</ItemGroup>
23-
</Project>
24+
</Project>

Voxen-Server.slnx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<Project Path="src/bundles/Voxen.Server/Voxen.Server.csproj" />
2020
</Folder>
2121
<Folder Name="/src/modules/">
22+
<Project Path="src/modules/Voxen.Server.Audits/Voxen.Server.Audits.csproj" Id="58751d38-b758-4acd-9143-8840332fb6af" />
2223
<Project Path="src/modules/Voxen.Server.Authentication/Voxen.Server.Authentication.csproj" />
2324
<Project Path="src/modules/Voxen.Server.Channels/Voxen.Server.Channels.csproj" Id="2cdf63df-e3a2-44a4-a49a-3228a490d5ac" />
2425
<Project Path="src/modules/Voxen.Server.Domain/Voxen.Server.Domain.csproj" Id="329f3eab-3c7a-40a5-a814-c92c0bda7577" />

src/bundles/Voxen.Server/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using FastEndpoints;
22
using FastEndpoints.Swagger;
3+
using Voxen.Server.Audits.Extensions;
34
using Voxen.Server.Authentication.Extensions;
45
using Voxen.Server.Channels.Extensions;
56
using Voxen.Server.Domain.Extensions;
@@ -14,6 +15,7 @@
1415

1516
var jwtSettings = builder.Configuration.GetSection("Jwt");
1617
builder.Services
18+
.AddVoxenAudits()
1719
.AddVoxenApiServices()
1820
.AddVoxenAuthentication(jwtSettings)
1921
.AddVoxenChannels()

src/bundles/Voxen.Server/Voxen.Server.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</PackageReference>
99
</ItemGroup>
1010
<ItemGroup>
11+
<ProjectReference Include="..\..\modules\Voxen.Server.Audits\Voxen.Server.Audits.csproj" />
1112
<ProjectReference Include="..\..\modules\Voxen.Server.Authentication\Voxen.Server.Authentication.csproj" />
1213
<ProjectReference Include="..\..\modules\Voxen.Server.Channels\Voxen.Server.Channels.csproj" />
1314
<ProjectReference Include="..\..\modules\Voxen.Server.Domain\Voxen.Server.Domain.csproj" />
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
using FastEndpoints;
2+
using Microsoft.EntityFrameworkCore;
3+
using System.Text.Json;
4+
using Voxen.Server.Audits.Models;
5+
using Voxen.Server.Domain;
6+
using Voxen.Server.Domain.Enums;
7+
8+
namespace Voxen.Server.Audits.Endpoints.GetAuditLogs;
9+
10+
/// <summary>
11+
/// An endpoint for retrieving audit logs.
12+
/// </summary>
13+
public sealed class GetAuditLogsEndpoint(VoxenDbContext db) : EndpointWithoutRequest<List<GetAuditLogsResponse>>
14+
{
15+
/// <inheritdoc />
16+
public override void Configure()
17+
{
18+
Get("/audits");
19+
Roles(nameof(ServerRole.Admin));
20+
}
21+
22+
/// <inheritdoc />
23+
public override async Task HandleAsync(CancellationToken ct)
24+
{
25+
var logs = await db.AuditLogs
26+
.AsNoTracking()
27+
.Include(a => a.User)
28+
.OrderByDescending(a => a.CreatedAt)
29+
.Select(a => new GetAuditLogsResponse
30+
{
31+
Id = a.Id,
32+
UserId = a.UserId,
33+
UserName = a.User.UserName,
34+
Action = a.Action,
35+
Category = a.Category,
36+
EntityId = a.EntityId,
37+
Changes = string.IsNullOrWhiteSpace(a.ChangesJson)
38+
? null
39+
: JsonSerializer.Deserialize<List<AuditChange>>(a.ChangesJson),
40+
CreatedAt = a.CreatedAt
41+
})
42+
.ToListAsync(ct);
43+
44+
await Send.OkAsync(logs, ct);
45+
}
46+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using Voxen.Server.Audits.Models;
2+
using Voxen.Server.Domain.Enums;
3+
4+
namespace Voxen.Server.Audits.Endpoints.GetAuditLogs;
5+
6+
/// <summary>
7+
/// Represents the response structure for audit log data.
8+
/// </summary>
9+
public class GetAuditLogsResponse
10+
{
11+
/// <summary>
12+
/// Gets or sets the unique identifier for the audit log.
13+
/// </summary>
14+
public Guid Id { get; set; }
15+
16+
/// <summary>
17+
/// Gets or sets the identifier of the user associated with the audit log.
18+
/// </summary>
19+
public Guid UserId { get; set; }
20+
21+
/// <summary>
22+
/// Gets or sets the name of the user associated with the audit log.
23+
/// </summary>
24+
public string? UserName { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets the type of action performed in the audit log.
28+
/// </summary>
29+
public AuditAction Action { get; set; }
30+
31+
/// <summary>
32+
/// Gets or sets the category of the action logged in the audit log.
33+
/// </summary>
34+
public AuditCategory Category { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets the identifier of the entity affected by the audit log.
38+
/// </summary>
39+
public Guid? EntityId { get; set; }
40+
41+
/// <summary>
42+
/// Gets or sets the deserialized list of changes (old/new values) for this audit log.
43+
/// </summary>
44+
public List<AuditChange>? Changes { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the timestamp when the audit log was created.
48+
/// </summary>
49+
public DateTime CreatedAt { get; set; }
50+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using Voxen.Server.Audits.Interfaces;
3+
using Voxen.Server.Audits.Services;
4+
5+
namespace Voxen.Server.Audits.Extensions;
6+
7+
/// <summary>
8+
/// Provides extension methods for configuring Voxen Audits services in an <see cref="IServiceCollection"/>.
9+
/// </summary>
10+
public static class ServiceCollectionExtensions
11+
{
12+
/// <summary>
13+
/// Adds the Voxen Audits services to the specified <see cref="IServiceCollection"/>.
14+
/// </summary>
15+
public static IServiceCollection AddVoxenAudits(this IServiceCollection services)
16+
{
17+
services.AddScoped<IAuditLogService, AuditLogService>();
18+
19+
return services;
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Voxen.Server.Audits.Models;
2+
using Voxen.Server.Domain.Entities;
3+
using Voxen.Server.Domain.Enums;
4+
5+
namespace Voxen.Server.Audits.Interfaces;
6+
7+
/// <summary>
8+
/// Defines the contract for an audit log service that handles logging of audit entries.
9+
/// </summary>
10+
public interface IAuditLogService
11+
{
12+
/// <summary>
13+
/// Logs an audit entry to the database.
14+
/// </summary>
15+
/// <param name="actor">The user who performed the action.</param>
16+
/// <param name="action">The type of action being logged.</param>
17+
/// <param name="category">The category of the action being logged.</param>
18+
/// <param name="entityId">The unique identifier of the entity associated with the audit log.</param>
19+
/// <param name="changes">A collection of changes made to the entity, represented as <see cref="AuditChange"/> objects.</param>
20+
/// <param name="ct">A cancellation token used to cancel the asynchronous action if needed.</param>
21+
public Task LogAsync(User actor, AuditAction action, AuditCategory category, Guid entityId, IEnumerable<AuditChange> changes, CancellationToken ct = default);
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Voxen.Server.Audits.Models;
2+
3+
/// <summary>
4+
/// Represents a change made to an entity during an audit.
5+
/// </summary>
6+
public class AuditChange
7+
{
8+
/// <summary>
9+
/// Gets or sets the name of the property that was changed during the audit.
10+
/// </summary>
11+
public string PropertyName { get; set; } = null!;
12+
13+
/// <summary>
14+
/// Gets or sets the previous value of the property before the change was made.
15+
/// </summary>
16+
public string? OldValue { get; set; }
17+
18+
/// <summary>
19+
/// Gets or sets the new value of the property after the change.
20+
/// </summary>
21+
public string NewValue { get; set; } = null!;
22+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using System.Text.Json;
2+
using Voxen.Server.Audits.Interfaces;
3+
using Voxen.Server.Audits.Models;
4+
using Voxen.Server.Domain;
5+
using Voxen.Server.Domain.Entities;
6+
using Voxen.Server.Domain.Enums;
7+
8+
namespace Voxen.Server.Audits.Services;
9+
10+
/// <summary>
11+
/// Provides functionality for logging audit entries to the database.
12+
/// </summary>
13+
public class AuditLogService(VoxenDbContext db) : IAuditLogService
14+
{
15+
private static readonly JsonSerializerOptions JsonOptions = new()
16+
{
17+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
18+
WriteIndented = false
19+
};
20+
21+
/// <inheritdoc />
22+
public async Task LogAsync(User actor, AuditAction action, AuditCategory category, Guid entityId, IEnumerable<AuditChange> changes, CancellationToken ct = default)
23+
{
24+
var changeList = changes.ToList();
25+
var audit = new Audit
26+
{
27+
UserId = actor.Id,
28+
Action = action,
29+
Category = category,
30+
EntityId = entityId,
31+
ChangesJson = changeList is { Count: > 0 }
32+
? JsonSerializer.Serialize(changeList, JsonOptions)
33+
: null,
34+
CreatedAt = DateTime.UtcNow
35+
};
36+
37+
db.AuditLogs.Add(audit);
38+
await db.SaveChangesAsync(ct);
39+
}
40+
}

0 commit comments

Comments
 (0)