Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddScoped<ICaptureService, CaptureService>();
services.AddScoped<ICaptureTriageService, CaptureTriageService>();
services.AddScoped<HistoryService>();
services.AddScoped<IHistoryService>(sp => sp.GetRequiredService<HistoryService>());
services.AddScoped<IAutomationProposalService, AutomationProposalService>();
services.AddScoped<IAutomationPolicyEngine, AutomationPolicyEngine>();
services.AddScoped<IAutomationPlannerService, AutomationPlannerService>();
Expand Down
23 changes: 21 additions & 2 deletions backend/src/Taskdeck.Application/Services/BoardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
using Taskdeck.Domain.Enums;
using Taskdeck.Domain.Exceptions;

namespace Taskdeck.Application.Services;
Expand All @@ -11,20 +12,30 @@ public class BoardService
private readonly IUnitOfWork _unitOfWork;
private readonly IAuthorizationService? _authorizationService;
private readonly IBoardRealtimeNotifier _realtimeNotifier;
private readonly IHistoryService? _historyService;

public BoardService(IUnitOfWork unitOfWork)
: this(unitOfWork, authorizationService: null, realtimeNotifier: null)
: this(unitOfWork, authorizationService: null, realtimeNotifier: null, historyService: null)
{
}

public BoardService(
IUnitOfWork unitOfWork,
IAuthorizationService? authorizationService,
IBoardRealtimeNotifier? realtimeNotifier = null)
IBoardRealtimeNotifier? realtimeNotifier = null,
IHistoryService? historyService = null)
{
_unitOfWork = unitOfWork;
_authorizationService = authorizationService;
_realtimeNotifier = realtimeNotifier ?? NoOpBoardRealtimeNotifier.Instance;
_historyService = historyService;
}

private async Task SafeLogAsync(string entityType, Guid entityId, AuditAction action, Guid? userId = null, string? changes = null)
{
if (_historyService == null) return;
try { await _historyService.LogActionAsync(entityType, entityId, action, userId, changes); }
catch (Exception) { /* Audit is secondary — never crash the mutation */ }
}

public async Task<Result<BoardDto>> CreateBoardAsync(CreateBoardDto dto, Guid actingUserId, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -139,6 +150,7 @@ private async Task<Result<BoardDto>> CreateBoardInternalAsync(CreateBoardDto dto
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(board.Id, "board", "created", board.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("board", board.Id, AuditAction.Created, ownerId, $"name={board.Name}");

return Result.Success(MapToDto(board));
}
Expand Down Expand Up @@ -171,6 +183,12 @@ private async Task<Result<BoardDto>> UpdateBoardInternalAsync(Guid id, UpdateBoa
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(board.Id, "board", "updated", board.Id, DateTimeOffset.UtcNow),
cancellationToken);
if (dto.IsArchived == true)
await SafeLogAsync("board", board.Id, AuditAction.Archived, changes: $"name={board.Name}");
else if (dto.IsArchived == false)
await SafeLogAsync("board", board.Id, AuditAction.Updated, changes: $"unarchived; name={board.Name}");
else
await SafeLogAsync("board", board.Id, AuditAction.Updated, changes: $"name={board.Name}");
return Result.Success(MapToDto(board));
}
catch (DomainException ex)
Expand Down Expand Up @@ -199,6 +217,7 @@ private async Task<Result> DeleteBoardInternalAsync(Guid id, CancellationToken c
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(board.Id, "board", "archived", board.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("board", board.Id, AuditAction.Archived, changes: $"name={board.Name}");
return Result.Success();
}

Expand Down
17 changes: 15 additions & 2 deletions backend/src/Taskdeck.Application/Services/CardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,25 @@ public class CardService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IBoardRealtimeNotifier _realtimeNotifier;
private readonly IHistoryService? _historyService;

public CardService(IUnitOfWork unitOfWork)
: this(unitOfWork, realtimeNotifier: null)
: this(unitOfWork, realtimeNotifier: null, historyService: null)
{
}

public CardService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null)
public CardService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null, IHistoryService? historyService = null)
{
_unitOfWork = unitOfWork;
_realtimeNotifier = realtimeNotifier ?? NoOpBoardRealtimeNotifier.Instance;
_historyService = historyService;
}

private async Task SafeLogAsync(string entityType, Guid entityId, AuditAction action, Guid? userId = null, string? changes = null)
{
if (_historyService == null) return;
try { await _historyService.LogActionAsync(entityType, entityId, action, userId, changes); }
catch (Exception) { /* Audit is secondary — never crash the mutation */ }
}

public async Task<Result<CardDto>> CreateCardAsync(CreateCardDto dto, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -74,6 +83,7 @@ public async Task<Result<CardDto>> CreateCardAsync(
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(card.BoardId, "card", "created", card.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("card", card.Id, AuditAction.Created, changes: $"title={card.Title}");

var createdCard = await _unitOfWork.Cards.GetByIdWithLabelsAsync(card.Id, cancellationToken);
return Result.Success(MapToDto(createdCard!));
Expand Down Expand Up @@ -134,6 +144,7 @@ public async Task<Result<CardDto>> UpdateCardAsync(
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(card.BoardId, "card", "updated", card.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("card", card.Id, AuditAction.Updated, actorUserId);

var updatedCard = await _unitOfWork.Cards.GetByIdWithLabelsAsync(id, cancellationToken);
return Result.Success(MapToDto(updatedCard!));
Expand Down Expand Up @@ -213,6 +224,7 @@ public async Task<Result<CardDto>> MoveCardAsync(Guid id, MoveCardDto dto, Cance
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(card.BoardId, "card", "moved", card.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("card", card.Id, AuditAction.Moved, changes: $"target_column={dto.TargetColumnId}; position={dto.TargetPosition}");

var movedCard = await _unitOfWork.Cards.GetByIdWithLabelsAsync(id, cancellationToken);
return Result.Success(MapToDto(movedCard!));
Expand Down Expand Up @@ -299,6 +311,7 @@ public async Task<Result> DeleteCardAsync(Guid id, CancellationToken cancellatio
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(card.BoardId, "card", "deleted", card.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("card", card.Id, AuditAction.Deleted, changes: $"title={card.Title}");

return Result.Success();
}
Expand Down
18 changes: 16 additions & 2 deletions backend/src/Taskdeck.Application/Services/ColumnService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
using Taskdeck.Domain.Enums;
using Taskdeck.Domain.Exceptions;

namespace Taskdeck.Application.Services;
Expand All @@ -10,16 +11,25 @@ public class ColumnService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IBoardRealtimeNotifier _realtimeNotifier;
private readonly IHistoryService? _historyService;

public ColumnService(IUnitOfWork unitOfWork)
: this(unitOfWork, realtimeNotifier: null)
: this(unitOfWork, realtimeNotifier: null, historyService: null)
{
}

public ColumnService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null)
public ColumnService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null, IHistoryService? historyService = null)
{
_unitOfWork = unitOfWork;
_realtimeNotifier = realtimeNotifier ?? NoOpBoardRealtimeNotifier.Instance;
_historyService = historyService;
}

private async Task SafeLogAsync(string entityType, Guid entityId, AuditAction action, Guid? userId = null, string? changes = null)
{
if (_historyService == null) return;
try { await _historyService.LogActionAsync(entityType, entityId, action, userId, changes); }
catch (Exception) { /* Audit is secondary — never crash the mutation */ }
}

public async Task<Result<ColumnDto>> CreateColumnAsync(CreateColumnDto dto, CancellationToken cancellationToken = default)
Expand All @@ -45,6 +55,7 @@ public async Task<Result<ColumnDto>> CreateColumnAsync(CreateColumnDto dto, Canc
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(column.BoardId, "column", "created", column.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("column", column.Id, AuditAction.Created, changes: $"name={column.Name}");

return Result.Success(MapToDto(column));
}
Expand All @@ -67,6 +78,7 @@ public async Task<Result<ColumnDto>> UpdateColumnAsync(Guid id, UpdateColumnDto
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(column.BoardId, "column", "updated", column.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("column", column.Id, AuditAction.Updated);

return Result.Success(MapToDto(column));
}
Expand Down Expand Up @@ -105,6 +117,7 @@ public async Task<Result> DeleteColumnAsync(Guid id, CancellationToken cancellat
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(column.BoardId, "column", "deleted", column.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("column", column.Id, AuditAction.Deleted, changes: $"name={column.Name}");

return Result.Success();
}
Expand Down Expand Up @@ -166,6 +179,7 @@ public async Task<Result<IEnumerable<ColumnDto>>> ReorderColumnsAsync(Guid board
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(boardId, "column", "reordered", null, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("column", boardId, AuditAction.Updated, changes: $"reordered; count={dto.ColumnIds.Count}");

// Return reordered columns
var reorderedColumns = dto.ColumnIds.Select(id => MapToDto(columnDict[id]));
Expand Down
6 changes: 6 additions & 0 deletions backend/src/Taskdeck.Application/Services/HistoryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ public async Task<Result> LogActionAsync(string entityType, Guid entityId, Audit
{
return Result.Failure(ex.ErrorCode, ex.Message);
}
catch (Exception)
{
// Audit logging is secondary to the mutation — never let infrastructure
// failures (e.g. DB full, concurrency) crash the calling operation.
return Result.Failure(ErrorCodes.UnexpectedError, $"Failed to persist audit log for {entityType}/{entityId}/{action}");
}
}

private static AuditLogDto MapToDto(AuditLog log)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ namespace Taskdeck.Application.Services;

/// <summary>
/// Service interface for audit log and history operations.
/// SCAFFOLDING: Implementation pending.
/// </summary>
public interface IHistoryService
{
Expand Down
17 changes: 15 additions & 2 deletions backend/src/Taskdeck.Application/Services/LabelService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Taskdeck.Application.Interfaces;
using Taskdeck.Domain.Common;
using Taskdeck.Domain.Entities;
using Taskdeck.Domain.Enums;
using Taskdeck.Domain.Exceptions;

namespace Taskdeck.Application.Services;
Expand All @@ -10,16 +11,25 @@ public class LabelService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IBoardRealtimeNotifier _realtimeNotifier;
private readonly IHistoryService? _historyService;

public LabelService(IUnitOfWork unitOfWork)
: this(unitOfWork, realtimeNotifier: null)
: this(unitOfWork, realtimeNotifier: null, historyService: null)
{
}

public LabelService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null)
public LabelService(IUnitOfWork unitOfWork, IBoardRealtimeNotifier? realtimeNotifier = null, IHistoryService? historyService = null)
{
_unitOfWork = unitOfWork;
_realtimeNotifier = realtimeNotifier ?? NoOpBoardRealtimeNotifier.Instance;
_historyService = historyService;
}

private async Task SafeLogAsync(string entityType, Guid entityId, AuditAction action, Guid? userId = null, string? changes = null)
{
if (_historyService == null) return;
try { await _historyService.LogActionAsync(entityType, entityId, action, userId, changes); }
catch (Exception) { /* Audit is secondary — never crash the mutation */ }
}

public async Task<Result<LabelDto>> CreateLabelAsync(CreateLabelDto dto, CancellationToken cancellationToken = default)
Expand All @@ -36,6 +46,7 @@ public async Task<Result<LabelDto>> CreateLabelAsync(CreateLabelDto dto, Cancell
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(label.BoardId, "label", "created", label.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("label", label.Id, AuditAction.Created, changes: $"name={label.Name}");

return Result.Success(MapToDto(label));
}
Expand All @@ -58,6 +69,7 @@ public async Task<Result<LabelDto>> UpdateLabelAsync(Guid id, UpdateLabelDto dto
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(label.BoardId, "label", "updated", label.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("label", label.Id, AuditAction.Updated);

return Result.Success(MapToDto(label));
}
Expand Down Expand Up @@ -93,6 +105,7 @@ public async Task<Result> DeleteLabelAsync(Guid id, CancellationToken cancellati
await _realtimeNotifier.NotifyBoardMutationAsync(
new BoardRealtimeEvent(label.BoardId, "label", "deleted", label.Id, DateTimeOffset.UtcNow),
cancellationToken);
await SafeLogAsync("label", label.Id, AuditAction.Deleted, changes: $"name={label.Name}");

return Result.Success();
}
Expand Down
Loading
Loading