Skip to content

Commit e089018

Browse files
authored
Merge pull request #20 from Chris0Jeky/copilot/plan-and-implement-backend-docs
[WIP] Plan implementation for backend documentation updates Remaining gap vs full backend docs: chat/ops/log-stream/worker activation is still pending implementation
2 parents 54d39b8 + cfcc931 commit e089018

File tree

57 files changed

+8624
-1
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+8624
-1
lines changed
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using Microsoft.AspNetCore.Authorization;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Taskdeck.Application.DTOs;
4+
using Taskdeck.Application.Interfaces;
5+
using Taskdeck.Application.Services;
6+
using Taskdeck.Domain.Entities;
7+
using Taskdeck.Domain.Exceptions;
8+
9+
namespace Taskdeck.Api.Controllers;
10+
11+
/// <summary>
12+
/// API endpoints for managing archived items and restoring them.
13+
/// </summary>
14+
[ApiController]
15+
[Authorize]
16+
[Route("api/archive")]
17+
public class ArchiveController : ControllerBase
18+
{
19+
private readonly IArchiveRecoveryService _archiveService;
20+
private readonly IUserContext _userContext;
21+
22+
public ArchiveController(
23+
IArchiveRecoveryService archiveService,
24+
IUserContext userContext)
25+
{
26+
_archiveService = archiveService;
27+
_userContext = userContext;
28+
}
29+
30+
/// <summary>
31+
/// Gets a list of archived items with optional filters.
32+
/// </summary>
33+
/// <param name="entityType">Filter by entity type (board, column, card)</param>
34+
/// <param name="boardId">Filter by board ID</param>
35+
/// <param name="status">Filter by restore status</param>
36+
/// <param name="limit">Maximum number of results (default: 100)</param>
37+
/// <param name="cancellationToken">Cancellation token</param>
38+
/// <returns>List of archive items</returns>
39+
[HttpGet("items")]
40+
public async Task<IActionResult> GetArchiveItems(
41+
[FromQuery] string? entityType,
42+
[FromQuery] Guid? boardId,
43+
[FromQuery] RestoreStatus? status,
44+
[FromQuery] int limit = 100,
45+
CancellationToken cancellationToken = default)
46+
{
47+
var result = await _archiveService.GetArchiveItemsAsync(
48+
entityType,
49+
boardId,
50+
status,
51+
limit,
52+
cancellationToken);
53+
54+
if (result.IsSuccess)
55+
return Ok(result.Value);
56+
57+
return result.ErrorCode switch
58+
{
59+
"NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
60+
"ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
61+
_ => Problem(result.ErrorMessage, statusCode: 500)
62+
};
63+
}
64+
65+
/// <summary>
66+
/// Gets a specific archive item by ID.
67+
/// </summary>
68+
/// <param name="id">Archive item ID</param>
69+
/// <param name="cancellationToken">Cancellation token</param>
70+
/// <returns>Archive item details</returns>
71+
[HttpGet("items/{id}")]
72+
public async Task<IActionResult> GetArchiveItem(Guid id, CancellationToken cancellationToken = default)
73+
{
74+
var result = await _archiveService.GetArchiveItemByIdAsync(id, cancellationToken);
75+
76+
if (!result.IsSuccess)
77+
{
78+
return result.ErrorCode == "NotFound"
79+
? NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage })
80+
: Problem(result.ErrorMessage, statusCode: 500);
81+
}
82+
83+
return Ok(result.Value);
84+
}
85+
86+
/// <summary>
87+
/// Restores an archived item.
88+
/// </summary>
89+
/// <param name="entityType">Entity type (board, column, card)</param>
90+
/// <param name="entityId">Entity ID to restore</param>
91+
/// <param name="dto">Restore options</param>
92+
/// <param name="cancellationToken">Cancellation token</param>
93+
/// <returns>Restore result</returns>
94+
[HttpPost("{entityType}/{entityId}/restore")]
95+
public async Task<IActionResult> RestoreArchivedItem(
96+
string entityType,
97+
Guid entityId,
98+
[FromBody] RestoreArchiveItemDto dto,
99+
CancellationToken cancellationToken = default)
100+
{
101+
if (!TryNormalizeEntityType(entityType, out var normalizedEntityType, out var invalidTypeResult))
102+
return invalidTypeResult!;
103+
104+
if (!TryGetCurrentUserId(out var restoredByUserId, out var userErrorResult))
105+
return userErrorResult!;
106+
107+
var archiveItemResult = await _archiveService.GetArchiveItemByEntityAsync(
108+
normalizedEntityType,
109+
entityId,
110+
cancellationToken);
111+
112+
if (!archiveItemResult.IsSuccess)
113+
{
114+
return archiveItemResult.ErrorCode switch
115+
{
116+
"NotFound" => NotFound(new { errorCode = archiveItemResult.ErrorCode, message = archiveItemResult.ErrorMessage }),
117+
"ValidationError" => BadRequest(new { errorCode = archiveItemResult.ErrorCode, message = archiveItemResult.ErrorMessage }),
118+
"AuthenticationFailed" => Unauthorized(new { errorCode = archiveItemResult.ErrorCode, message = archiveItemResult.ErrorMessage }),
119+
"Unauthorized" => Unauthorized(new { errorCode = archiveItemResult.ErrorCode, message = archiveItemResult.ErrorMessage }),
120+
"Forbidden" => StatusCode(403, new { errorCode = archiveItemResult.ErrorCode, message = archiveItemResult.ErrorMessage }),
121+
_ => Problem(archiveItemResult.ErrorMessage, statusCode: 500)
122+
};
123+
}
124+
125+
var archiveItem = archiveItemResult.Value;
126+
if (archiveItem.RestoreStatus != RestoreStatus.Available)
127+
{
128+
return Conflict(new
129+
{
130+
errorCode = ErrorCodes.InvalidOperation,
131+
message = $"Archive item for {normalizedEntityType} with ID {entityId} is in status {archiveItem.RestoreStatus}"
132+
});
133+
}
134+
135+
var result = await _archiveService.RestoreArchiveItemAsync(
136+
archiveItem.Id,
137+
dto,
138+
restoredByUserId,
139+
cancellationToken);
140+
141+
if (!result.IsSuccess)
142+
{
143+
return result.ErrorCode switch
144+
{
145+
"NotFound" => NotFound(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
146+
"ValidationError" => BadRequest(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
147+
"Conflict" => Conflict(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
148+
ErrorCodes.InvalidOperation => Conflict(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
149+
"AuthenticationFailed" => Unauthorized(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
150+
"Unauthorized" => Unauthorized(new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
151+
"Forbidden" => StatusCode(403, new { errorCode = result.ErrorCode, message = result.ErrorMessage }),
152+
_ => Problem(result.ErrorMessage, statusCode: 500)
153+
};
154+
}
155+
156+
return Ok(result.Value);
157+
}
158+
159+
private static bool TryNormalizeEntityType(string entityType, out string normalizedEntityType, out IActionResult? errorResult)
160+
{
161+
normalizedEntityType = string.Empty;
162+
errorResult = null;
163+
164+
if (string.IsNullOrWhiteSpace(entityType))
165+
{
166+
errorResult = new BadRequestObjectResult(new
167+
{
168+
errorCode = ErrorCodes.ValidationError,
169+
message = "EntityType is required"
170+
});
171+
return false;
172+
}
173+
174+
normalizedEntityType = entityType.Trim().ToLowerInvariant();
175+
if (normalizedEntityType != "board" && normalizedEntityType != "column" && normalizedEntityType != "card")
176+
{
177+
errorResult = new BadRequestObjectResult(new
178+
{
179+
errorCode = ErrorCodes.ValidationError,
180+
message = "EntityType must be 'board', 'column', or 'card'"
181+
});
182+
return false;
183+
}
184+
185+
return true;
186+
}
187+
188+
private bool TryGetCurrentUserId(out Guid userId, out IActionResult? errorResult)
189+
{
190+
userId = Guid.Empty;
191+
errorResult = null;
192+
193+
if (!_userContext.IsAuthenticated || string.IsNullOrWhiteSpace(_userContext.UserId))
194+
{
195+
errorResult = Unauthorized(new
196+
{
197+
errorCode = ErrorCodes.AuthenticationFailed,
198+
message = "Authenticated user context is required"
199+
});
200+
return false;
201+
}
202+
203+
if (!Guid.TryParse(_userContext.UserId, out userId))
204+
{
205+
errorResult = Unauthorized(new
206+
{
207+
errorCode = ErrorCodes.AuthenticationFailed,
208+
message = "Authenticated user id claim is invalid"
209+
});
210+
return false;
211+
}
212+
213+
return true;
214+
}
215+
}

0 commit comments

Comments
 (0)