Skip to content
Merged
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.5" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>
</Project>
</Project>
1 change: 1 addition & 0 deletions Voxen-Server.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Project Path="src/bundles/Voxen.Server/Voxen.Server.csproj" />
</Folder>
<Folder Name="/src/modules/">
<Project Path="src/modules/Voxen.Server.Audits/Voxen.Server.Audits.csproj" Id="58751d38-b758-4acd-9143-8840332fb6af" />
<Project Path="src/modules/Voxen.Server.Authentication/Voxen.Server.Authentication.csproj" />
<Project Path="src/modules/Voxen.Server.Channels/Voxen.Server.Channels.csproj" Id="2cdf63df-e3a2-44a4-a49a-3228a490d5ac" />
<Project Path="src/modules/Voxen.Server.Domain/Voxen.Server.Domain.csproj" Id="329f3eab-3c7a-40a5-a814-c92c0bda7577" />
Expand Down
2 changes: 2 additions & 0 deletions src/bundles/Voxen.Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using FastEndpoints;
using FastEndpoints.Swagger;
using Voxen.Server.Audits.Extensions;
using Voxen.Server.Authentication.Extensions;
using Voxen.Server.Channels.Extensions;
using Voxen.Server.Domain.Extensions;
Expand All @@ -14,6 +15,7 @@

var jwtSettings = builder.Configuration.GetSection("Jwt");
builder.Services
.AddVoxenAudits()
.AddVoxenApiServices()
.AddVoxenAuthentication(jwtSettings)
.AddVoxenChannels()
Expand Down
1 change: 1 addition & 0 deletions src/bundles/Voxen.Server/Voxen.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\modules\Voxen.Server.Audits\Voxen.Server.Audits.csproj" />
<ProjectReference Include="..\..\modules\Voxen.Server.Authentication\Voxen.Server.Authentication.csproj" />
<ProjectReference Include="..\..\modules\Voxen.Server.Channels\Voxen.Server.Channels.csproj" />
<ProjectReference Include="..\..\modules\Voxen.Server.Domain\Voxen.Server.Domain.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using FastEndpoints;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;
using Voxen.Server.Audits.Models;
using Voxen.Server.Domain;
using Voxen.Server.Domain.Enums;

namespace Voxen.Server.Audits.Endpoints.GetAuditLogs;

/// <summary>
/// An endpoint for retrieving audit logs.
/// </summary>
public sealed class GetAuditLogsEndpoint(VoxenDbContext db) : EndpointWithoutRequest<List<GetAuditLogsResponse>>
{
/// <inheritdoc />
public override void Configure()
{
Get("/audits");
Roles(nameof(ServerRole.Admin));
}

/// <inheritdoc />
public override async Task HandleAsync(CancellationToken ct)
{
var logs = await db.AuditLogs
.AsNoTracking()
.Include(a => a.User)
.OrderByDescending(a => a.CreatedAt)
.Select(a => new GetAuditLogsResponse
{
Id = a.Id,
UserId = a.UserId,
UserName = a.User.UserName,
Action = a.Action,
Category = a.Category,
EntityId = a.EntityId,
Changes = string.IsNullOrWhiteSpace(a.ChangesJson)
? null
: JsonSerializer.Deserialize<List<AuditChange>>(a.ChangesJson),
CreatedAt = a.CreatedAt
})
.ToListAsync(ct);

await Send.OkAsync(logs, ct);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Voxen.Server.Audits.Models;
using Voxen.Server.Domain.Enums;

namespace Voxen.Server.Audits.Endpoints.GetAuditLogs;

/// <summary>
/// Represents the response structure for audit log data.
/// </summary>
public class GetAuditLogsResponse
{
/// <summary>
/// Gets or sets the unique identifier for the audit log.
/// </summary>
public Guid Id { get; set; }

/// <summary>
/// Gets or sets the identifier of the user associated with the audit log.
/// </summary>
public Guid UserId { get; set; }

/// <summary>
/// Gets or sets the name of the user associated with the audit log.
/// </summary>
public string? UserName { get; set; }

/// <summary>
/// Gets or sets the type of action performed in the audit log.
/// </summary>
public AuditAction Action { get; set; }

/// <summary>
/// Gets or sets the category of the action logged in the audit log.
/// </summary>
public AuditCategory Category { get; set; }

/// <summary>
/// Gets or sets the identifier of the entity affected by the audit log.
/// </summary>
public Guid? EntityId { get; set; }

/// <summary>
/// Gets or sets the deserialized list of changes (old/new values) for this audit log.
/// </summary>
public List<AuditChange>? Changes { get; set; }

/// <summary>
/// Gets or sets the timestamp when the audit log was created.
/// </summary>
public DateTime CreatedAt { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Extensions.DependencyInjection;
using Voxen.Server.Audits.Interfaces;
using Voxen.Server.Audits.Services;

namespace Voxen.Server.Audits.Extensions;

/// <summary>
/// Provides extension methods for configuring Voxen Audits services in an <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Adds the Voxen Audits services to the specified <see cref="IServiceCollection"/>.
/// </summary>
public static IServiceCollection AddVoxenAudits(this IServiceCollection services)
{
services.AddScoped<IAuditLogService, AuditLogService>();

return services;
}
}
22 changes: 22 additions & 0 deletions src/modules/Voxen.Server.Audits/Interfaces/IAuditLogService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Voxen.Server.Audits.Models;
using Voxen.Server.Domain.Entities;
using Voxen.Server.Domain.Enums;

namespace Voxen.Server.Audits.Interfaces;

/// <summary>
/// Defines the contract for an audit log service that handles logging of audit entries.
/// </summary>
public interface IAuditLogService
{
/// <summary>
/// Logs an audit entry to the database.
/// </summary>
/// <param name="actor">The user who performed the action.</param>
/// <param name="action">The type of action being logged.</param>
/// <param name="category">The category of the action being logged.</param>
/// <param name="entityId">The unique identifier of the entity associated with the audit log.</param>
/// <param name="changes">A collection of changes made to the entity, represented as <see cref="AuditChange"/> objects.</param>
/// <param name="ct">A cancellation token used to cancel the asynchronous action if needed.</param>
public Task LogAsync(User actor, AuditAction action, AuditCategory category, Guid entityId, IEnumerable<AuditChange> changes, CancellationToken ct = default);
}
22 changes: 22 additions & 0 deletions src/modules/Voxen.Server.Audits/Models/AuditChange.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Voxen.Server.Audits.Models;

/// <summary>
/// Represents a change made to an entity during an audit.
/// </summary>
public class AuditChange
{
/// <summary>
/// Gets or sets the name of the property that was changed during the audit.
/// </summary>
public string PropertyName { get; set; } = null!;

/// <summary>
/// Gets or sets the previous value of the property before the change was made.
/// </summary>
public string? OldValue { get; set; }

/// <summary>
/// Gets or sets the new value of the property after the change.
/// </summary>
public string NewValue { get; set; } = null!;
}
40 changes: 40 additions & 0 deletions src/modules/Voxen.Server.Audits/Services/AuditLogService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using System.Text.Json;
using Voxen.Server.Audits.Interfaces;
using Voxen.Server.Audits.Models;
using Voxen.Server.Domain;
using Voxen.Server.Domain.Entities;
using Voxen.Server.Domain.Enums;

namespace Voxen.Server.Audits.Services;

/// <summary>
/// Provides functionality for logging audit entries to the database.
/// </summary>
public class AuditLogService(VoxenDbContext db) : IAuditLogService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};

/// <inheritdoc />
public async Task LogAsync(User actor, AuditAction action, AuditCategory category, Guid entityId, IEnumerable<AuditChange> changes, CancellationToken ct = default)
{
var changeList = changes.ToList();
var audit = new Audit
{
UserId = actor.Id,
Action = action,
Category = category,
EntityId = entityId,
ChangesJson = changeList is { Count: > 0 }
? JsonSerializer.Serialize(changeList, JsonOptions)
: null,
CreatedAt = DateTime.UtcNow
};

db.AuditLogs.Add(audit);
await db.SaveChangesAsync(ct);
}
}
11 changes: 11 additions & 0 deletions src/modules/Voxen.Server.Audits/Voxen.Server.Audits.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<PackageReference Include="FastEndpoints" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Voxen.Server.Domain\Voxen.Server.Domain.csproj" />
</ItemGroup>
</Project>


49 changes: 49 additions & 0 deletions src/modules/Voxen.Server.Domain/Entities/Audit.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Voxen.Server.Domain.Enums;

namespace Voxen.Server.Domain.Entities;

/// <summary>
/// Represents an audit log entry within the Voxen server.
/// </summary>
public class Audit
{
/// <summary>
/// Unique identifier for the audit log entry.
/// </summary>
public Guid Id { get; set; }

/// <summary>
/// The user who performed the action.
/// </summary>
public Guid UserId { get; set; }

/// <summary>
/// Navigation property for the user.
/// </summary>
public User User { get; set; } = null!;

/// <summary>
/// The action that was performed (Create, Update, Delete, etc.).
/// </summary>
public AuditAction Action { get; set; }

/// <summary>
/// The category/type of entity affected (Channel, User, Server, etc.).
/// </summary>
public AuditCategory Category { get; set; }

/// <summary>
/// The identifier of the affected entity (e.g., ChannelId, UserId).
/// </summary>
public Guid? EntityId { get; set; }

/// <summary>
/// Backing field for JSON storage in DB.
/// </summary>
public string? ChangesJson { get; set; }

/// <summary>
/// Timestamp when the action occurred (UTC).
/// </summary>
public DateTime CreatedAt { get; set; }
}
12 changes: 12 additions & 0 deletions src/modules/Voxen.Server.Domain/Enums/AuditAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Voxen.Server.Domain.Enums;

/// <summary>
/// Represents the type of action performed in an audit log entry.
/// </summary>
public enum AuditAction
{
/// <summary>
/// Represents the creation of a new entity or record in the audit log.
/// </summary>
Create
}
17 changes: 17 additions & 0 deletions src/modules/Voxen.Server.Domain/Enums/AuditCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Voxen.Server.Domain.Enums;

/// <summary>
/// Represents the category of an action logged in the audit system.
/// </summary>
public enum AuditCategory
{
/// <summary>
/// Represents a change to server-related properties logged in the audit system.
/// </summary>
Server,

/// <summary>
/// Represents a change to user-related properties logged in the audit system.
/// </summary>
User
}
Loading
Loading