130 lines
5.0 KiB
C#
130 lines
5.0 KiB
C#
using JobTrackerApi.Models;
|
|
using JobTrackerApi.Services;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
namespace JobTrackerApi.Controllers;
|
|
|
|
[ApiController]
|
|
[Route("api/operations")]
|
|
[Authorize(AuthenticationSchemes = "local")]
|
|
public sealed class OperationsController(UserOperationStore operations) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
public async Task<ActionResult<IReadOnlyList<OperationDto>>> List([FromQuery] int limit = 25, CancellationToken cancellationToken = default)
|
|
{
|
|
if (limit is < 1 or > 100) return BadRequest(new { code = "invalid_limit", message = "Limit must be between 1 and 100." });
|
|
return Ok((await operations.ListAsync(limit, cancellationToken)).Select(ToDto).ToList());
|
|
}
|
|
|
|
[HttpGet("{id:guid}")]
|
|
public async Task<ActionResult<OperationDto>> Get(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
var operation = await operations.GetAsync(id, cancellationToken);
|
|
return operation is null ? NotFound() : Ok(ToDto(operation));
|
|
}
|
|
|
|
[HttpPost("{id:guid}/cancel")]
|
|
public async Task<ActionResult<OperationDto>> Cancel(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
if (await operations.GetAsync(id, cancellationToken) is null) return NotFound();
|
|
if (!await operations.RequestCancellationAsync(id, cancellationToken))
|
|
return Conflict(new { code = "operation_not_cancellable", message = "This operation can no longer be cancelled." });
|
|
return Ok(ToDto((await operations.GetAsync(id, cancellationToken))!));
|
|
}
|
|
|
|
[HttpPost("{id:guid}/retry")]
|
|
public async Task<ActionResult<OperationDto>> Retry(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
if (await operations.GetAsync(id, cancellationToken) is null) return NotFound();
|
|
if (!await operations.RetryAsync(id, cancellationToken))
|
|
return Conflict(new { code = "operation_not_retryable", message = "Only failed or cancelled operations can be retried." });
|
|
return Ok(ToDto((await operations.GetAsync(id, cancellationToken))!));
|
|
}
|
|
|
|
private static OperationDto ToDto(UserOperation operation) => new(
|
|
operation.Id,
|
|
operation.TaskType,
|
|
operation.Status,
|
|
operation.SubjectType,
|
|
operation.CreatedAtUtc,
|
|
operation.StartedAtUtc,
|
|
operation.CompletedAtUtc,
|
|
operation.DeadlineAtUtc,
|
|
operation.CancellationRequestedAtUtc,
|
|
operation.ProgressStage,
|
|
operation.ProgressPercent,
|
|
operation.FailureCategory,
|
|
!OperationStatuses.IsTerminal(operation.Status) && operation.CancellationRequestedAtUtc is null,
|
|
operation.Status is OperationStatuses.Failed or OperationStatuses.Cancelled);
|
|
}
|
|
|
|
public sealed record OperationDto(
|
|
Guid Id,
|
|
string TaskType,
|
|
string Status,
|
|
string? SubjectType,
|
|
DateTime CreatedAtUtc,
|
|
DateTime? StartedAtUtc,
|
|
DateTime? CompletedAtUtc,
|
|
DateTime? DeadlineAtUtc,
|
|
DateTime? CancellationRequestedAtUtc,
|
|
string? ProgressStage,
|
|
int? ProgressPercent,
|
|
string? FailureCategory,
|
|
bool CanCancel,
|
|
bool CanRetry);
|
|
|
|
[ApiController]
|
|
[Route("api/notifications")]
|
|
[Authorize(AuthenticationSchemes = "local")]
|
|
public sealed class NotificationsController(UserNotificationStore notifications) : ControllerBase
|
|
{
|
|
[HttpGet]
|
|
public async Task<ActionResult<IReadOnlyList<NotificationDto>>> List([FromQuery] int limit = 25, CancellationToken cancellationToken = default)
|
|
{
|
|
if (limit is < 1 or > 100) return BadRequest(new { code = "invalid_limit", message = "Limit must be between 1 and 100." });
|
|
return Ok((await notifications.ListAsync(limit, cancellationToken)).Select(ToDto).ToList());
|
|
}
|
|
|
|
[HttpGet("unread-count")]
|
|
public async Task<IActionResult> UnreadCount(CancellationToken cancellationToken) =>
|
|
Ok(new { count = await notifications.UnreadCountAsync(cancellationToken) });
|
|
|
|
[HttpPost("{id:guid}/read")]
|
|
public async Task<IActionResult> MarkRead(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
if (await notifications.GetAsync(id, cancellationToken) is null) return NotFound();
|
|
await notifications.MarkReadAsync(id, cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
[HttpDelete("{id:guid}")]
|
|
public async Task<IActionResult> Dismiss(Guid id, CancellationToken cancellationToken)
|
|
{
|
|
if (await notifications.GetAsync(id, cancellationToken) is null) return NotFound();
|
|
await notifications.DismissAsync(id, cancellationToken);
|
|
return NoContent();
|
|
}
|
|
|
|
private static NotificationDto ToDto(UserNotification notification) => new(
|
|
notification.Id,
|
|
notification.OperationId,
|
|
notification.Kind,
|
|
notification.Title,
|
|
notification.Message,
|
|
notification.LinkPath,
|
|
notification.CreatedAtUtc,
|
|
notification.ReadAtUtc);
|
|
}
|
|
|
|
public sealed record NotificationDto(
|
|
Guid Id,
|
|
Guid? OperationId,
|
|
string Kind,
|
|
string Title,
|
|
string Message,
|
|
string? LinkPath,
|
|
DateTime CreatedAtUtc,
|
|
DateTime? ReadAtUtc);
|