feat: Phase 2 — Delegated State, Auth, Review-Gate, Notifications, Zombie-Reset
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Nexus.Api.Data;
|
||||
using Nexus.Api.Models;
|
||||
@@ -5,9 +7,13 @@ using Nexus.Api.Services;
|
||||
|
||||
namespace Nexus.Api.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/dashboard")]
|
||||
public class DashboardController(IDashboardService dashboardService, ITaskService taskService) : ControllerBase
|
||||
public class DashboardController(
|
||||
IDashboardService dashboardService,
|
||||
ITaskService taskService,
|
||||
IHttpContextAccessor httpContextAccessor) : ControllerBase
|
||||
{
|
||||
[HttpGet("status")]
|
||||
public async Task<DashboardStatus> GetStatus()
|
||||
@@ -115,9 +121,16 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
if (string.IsNullOrWhiteSpace(request.Title))
|
||||
return BadRequest(new { error = "Title is required." });
|
||||
|
||||
var task = await taskService.CreateDashboardTaskAsync(
|
||||
request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, ct);
|
||||
return Created($"/api/dashboard/tasks/{task.Id}", MapToDto(task));
|
||||
try
|
||||
{
|
||||
var task = await taskService.CreateDashboardTaskAsync(
|
||||
request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.ParentTaskId, ct);
|
||||
return Created($"/api/dashboard/tasks/{task.Id}", MapToDto(task));
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("tasks/{id:guid}")]
|
||||
@@ -125,7 +138,7 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
Guid id, [FromBody] UpdateDashboardTaskRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await taskService.UpdateDashboardTaskAsync(
|
||||
id, request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, ct);
|
||||
id, request.Title, request.Detail, request.Source, request.Priority, request.AssignedTo, request.DueDate, ct);
|
||||
return result.Outcome switch
|
||||
{
|
||||
TaskOperationOutcome.NotFound => NotFound(new { error = "Task not found." }),
|
||||
@@ -149,6 +162,20 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
public async Task<ActionResult<DashboardTaskDto>> UpdateTaskStatus(
|
||||
Guid id, [FromBody] UpdateDashboardTaskStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
// Bao review gate: Check if moving OUT of Review
|
||||
var currentTask = await taskService.GetByIdAsync(id, ct);
|
||||
if (currentTask is not null &&
|
||||
string.Equals(currentTask.State, "Review", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(request.Status, "Review", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var user = httpContextAccessor.HttpContext?.User;
|
||||
var isOwner = user?.IsInRole("Owner") == true ||
|
||||
user?.IsInRole("owner") == true ||
|
||||
user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value == "bao";
|
||||
if (!isOwner)
|
||||
return StatusCode(403, new { error = "Only the owner can move tasks out of Review." });
|
||||
}
|
||||
|
||||
var result = await taskService.UpdateStatusAsync(id, request.Status, ct);
|
||||
return result.Outcome switch
|
||||
{
|
||||
@@ -171,6 +198,20 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
if (string.IsNullOrWhiteSpace(request.State))
|
||||
return BadRequest(new { error = "State is required." });
|
||||
|
||||
// Bao review gate: Check if moving OUT of Review
|
||||
var currentTask = await taskService.GetByIdAsync(id, ct);
|
||||
if (currentTask is not null &&
|
||||
string.Equals(currentTask.State, "Review", StringComparison.OrdinalIgnoreCase) &&
|
||||
!string.Equals(request.State, "Review", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var user = httpContextAccessor.HttpContext?.User;
|
||||
var isOwner = user?.IsInRole("Owner") == true ||
|
||||
user?.IsInRole("owner") == true ||
|
||||
user?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value == "bao";
|
||||
if (!isOwner)
|
||||
return StatusCode(403, new { error = "Only the owner can move tasks out of Review." });
|
||||
}
|
||||
|
||||
var result = await taskService.MoveTaskAsync(id, request.State, ct);
|
||||
return result.Outcome switch
|
||||
{
|
||||
@@ -180,6 +221,33 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
};
|
||||
}
|
||||
|
||||
// ── New Endpoints: Reset Stale, Children, Activity ──
|
||||
|
||||
[HttpPost("tasks/reset-stale")]
|
||||
public async Task<ActionResult<ResetStaleResponse>> ResetStale(
|
||||
[FromBody] ResetStaleRequest request, CancellationToken ct)
|
||||
{
|
||||
var threshold = TimeSpan.FromHours(Math.Max(1, request.StaleHours));
|
||||
var count = await taskService.ResetStaleInProgressTasksAsync(threshold, ct);
|
||||
return Ok(new ResetStaleResponse(count));
|
||||
}
|
||||
|
||||
[HttpGet("tasks/{id:guid}/children")]
|
||||
public async Task<ActionResult<List<DashboardTaskDto>>> GetChildren(Guid id, CancellationToken ct)
|
||||
{
|
||||
var children = await taskService.GetChildTasksAsync(id, ct);
|
||||
return Ok(children.Select(MapToDto).ToList());
|
||||
}
|
||||
|
||||
[HttpGet("tasks/{id:guid}/activity")]
|
||||
public async Task<ActionResult<List<ActivityEvent>>> GetTaskActivity(Guid id, CancellationToken ct)
|
||||
{
|
||||
var events = await taskService.GetTaskActivityAsync(id, ct);
|
||||
return Ok(events);
|
||||
}
|
||||
|
||||
// ── Import ──
|
||||
|
||||
[HttpPost("tasks/import-from-iris-todo")]
|
||||
public async Task<ActionResult<ImportResultDto>> ImportFromIrisTodo(
|
||||
[FromQuery] bool delete = false, CancellationToken ct = default)
|
||||
@@ -189,5 +257,6 @@ public class DashboardController(IDashboardService dashboardService, ITaskServic
|
||||
}
|
||||
|
||||
private static DashboardTaskDto MapToDto(WorkTask t) => new(
|
||||
t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo, t.CreatedAt, t.UpdatedAt);
|
||||
t.Id, t.Title, t.Detail, t.Source, t.State, t.Priority, t.AssignedTo,
|
||||
t.ParentTaskId, t.DueDate, t.CreatedAt, t.UpdatedAt);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user