| | | 1 | | using Chronicis.Api.Infrastructure; |
| | | 2 | | using Chronicis.Api.Services; |
| | | 3 | | using Chronicis.Shared.DTOs; |
| | | 4 | | using Microsoft.AspNetCore.Authorization; |
| | | 5 | | using Microsoft.AspNetCore.Mvc; |
| | | 6 | | |
| | | 7 | | namespace Chronicis.Api.Controllers; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// API endpoints for World Document management (file uploads to blob storage). |
| | | 11 | | /// </summary> |
| | | 12 | | [Route("worlds/{worldId:guid}/documents")] |
| | | 13 | | [Authorize] |
| | | 14 | | public class WorldDocumentsController : ControllerBase |
| | | 15 | | { |
| | | 16 | | private readonly IWorldDocumentService _documentService; |
| | | 17 | | private readonly ICurrentUserService _currentUserService; |
| | | 18 | | private readonly ILogger<WorldDocumentsController> _logger; |
| | | 19 | | |
| | 0 | 20 | | public WorldDocumentsController( |
| | 0 | 21 | | IWorldDocumentService documentService, |
| | 0 | 22 | | ICurrentUserService currentUserService, |
| | 0 | 23 | | ILogger<WorldDocumentsController> logger) |
| | | 24 | | { |
| | 0 | 25 | | _documentService = documentService; |
| | 0 | 26 | | _currentUserService = currentUserService; |
| | 0 | 27 | | _logger = logger; |
| | 0 | 28 | | } |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// GET /worlds/{worldId}/documents - Get all documents for a world. |
| | | 32 | | /// </summary> |
| | | 33 | | [HttpGet] |
| | | 34 | | public async Task<ActionResult<IEnumerable<WorldDocumentDto>>> GetWorldDocuments(Guid worldId) |
| | | 35 | | { |
| | 0 | 36 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 37 | | _logger.LogDebug("User {UserId} getting documents for world {WorldId}", user.Id, worldId); |
| | | 38 | | |
| | | 39 | | try |
| | | 40 | | { |
| | 0 | 41 | | var documents = await _documentService.GetWorldDocumentsAsync(worldId, user.Id); |
| | 0 | 42 | | return Ok(documents); |
| | | 43 | | } |
| | 0 | 44 | | catch (UnauthorizedAccessException ex) |
| | | 45 | | { |
| | 0 | 46 | | _logger.LogWarning(ex, "Unauthorized access to world documents"); |
| | 0 | 47 | | return StatusCode(403, new { error = ex.Message }); |
| | | 48 | | } |
| | 0 | 49 | | } |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Get a download URL for a document. |
| | | 53 | | /// </summary> |
| | | 54 | | [HttpGet("documents/{documentId:guid}/content")] |
| | | 55 | | [HttpGet("/documents/{documentId:guid}/content")] |
| | | 56 | | public async Task<ActionResult<WorldDocumentDownloadDto>> GetDocumentContentAsync(Guid documentId) |
| | | 57 | | { |
| | 0 | 58 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 59 | | _logger.LogDebug("User {UserId} requesting download URL for document {DocumentId}", user.Id, documentId); |
| | | 60 | | |
| | | 61 | | try |
| | | 62 | | { |
| | 0 | 63 | | var result = await _documentService.GetDocumentContentAsync(documentId, user.Id); |
| | | 64 | | |
| | 0 | 65 | | return Ok(new WorldDocumentDownloadDto |
| | 0 | 66 | | { |
| | 0 | 67 | | DownloadUrl = result.DownloadUrl, |
| | 0 | 68 | | FileName = result.FileName, |
| | 0 | 69 | | ContentType = result.ContentType, |
| | 0 | 70 | | FileSizeBytes = result.FileSizeBytes |
| | 0 | 71 | | }); |
| | | 72 | | } |
| | 0 | 73 | | catch (UnauthorizedAccessException ex) |
| | | 74 | | { |
| | 0 | 75 | | _logger.LogWarning(ex, "Unauthorized document content request"); |
| | 0 | 76 | | return StatusCode(403, new { error = ex.Message }); |
| | | 77 | | } |
| | 0 | 78 | | catch (InvalidOperationException ex) |
| | | 79 | | { |
| | 0 | 80 | | _logger.LogWarning(ex, "Document not found for content request"); |
| | 0 | 81 | | return NotFound(new { error = ex.Message }); |
| | | 82 | | } |
| | 0 | 83 | | catch (FileNotFoundException ex) |
| | | 84 | | { |
| | 0 | 85 | | _logger.LogDebug(ex, "File not found in storage"); |
| | 0 | 86 | | return NotFound(new { error = "File not found in storage" }); |
| | | 87 | | } |
| | 0 | 88 | | catch (Exception ex) |
| | | 89 | | { |
| | 0 | 90 | | _logger.LogError(ex, "Error generating download URL"); |
| | 0 | 91 | | return StatusCode(500, new { error = "Failed to generate download URL" }); |
| | | 92 | | } |
| | 0 | 93 | | } |
| | | 94 | | |
| | | 95 | | /// <summary> |
| | | 96 | | /// POST /worlds/{worldId}/documents/request-upload - Request a document upload (generates SAS URL). |
| | | 97 | | /// </summary> |
| | | 98 | | [HttpPost("request-upload")] |
| | | 99 | | public async Task<ActionResult<WorldDocumentUploadResponseDto>> RequestDocumentUpload( |
| | | 100 | | Guid worldId, |
| | | 101 | | [FromBody] WorldDocumentUploadRequestDto request) |
| | | 102 | | { |
| | 0 | 103 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 104 | | _logger.LogDebug("User {UserId} requesting document upload for world {WorldId}", user.Id, worldId); |
| | | 105 | | |
| | 0 | 106 | | if (request == null) |
| | | 107 | | { |
| | 0 | 108 | | return BadRequest(new { error = "Invalid request body" }); |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | try |
| | | 112 | | { |
| | 0 | 113 | | var result = await _documentService.RequestUploadAsync(worldId, user.Id, request); |
| | 0 | 114 | | return Ok(result); |
| | | 115 | | } |
| | 0 | 116 | | catch (UnauthorizedAccessException ex) |
| | | 117 | | { |
| | 0 | 118 | | _logger.LogWarning(ex, "Unauthorized upload request"); |
| | 0 | 119 | | return StatusCode(403, new { error = ex.Message }); |
| | | 120 | | } |
| | 0 | 121 | | catch (ArgumentException ex) |
| | | 122 | | { |
| | 0 | 123 | | _logger.LogWarning(ex, "Invalid upload request"); |
| | 0 | 124 | | return BadRequest(new { error = ex.Message }); |
| | | 125 | | } |
| | 0 | 126 | | } |
| | | 127 | | |
| | | 128 | | /// <summary> |
| | | 129 | | /// POST /worlds/{worldId}/documents/{documentId}/confirm - Confirm a document upload completed. |
| | | 130 | | /// </summary> |
| | | 131 | | [HttpPost("{documentId:guid}/confirm")] |
| | | 132 | | public async Task<ActionResult<WorldDocumentDto>> ConfirmDocumentUpload(Guid worldId, Guid documentId) |
| | | 133 | | { |
| | 0 | 134 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 135 | | _logger.LogDebug("User {UserId} confirming document upload {DocumentId}", user.Id, documentId); |
| | | 136 | | |
| | | 137 | | try |
| | | 138 | | { |
| | 0 | 139 | | var result = await _documentService.ConfirmUploadAsync(worldId, documentId, user.Id); |
| | 0 | 140 | | return Ok(result); |
| | | 141 | | } |
| | 0 | 142 | | catch (UnauthorizedAccessException ex) |
| | | 143 | | { |
| | 0 | 144 | | _logger.LogWarning(ex, "Unauthorized confirm upload"); |
| | 0 | 145 | | return StatusCode(403, new { error = ex.Message }); |
| | | 146 | | } |
| | 0 | 147 | | catch (InvalidOperationException ex) |
| | | 148 | | { |
| | 0 | 149 | | _logger.LogWarning(ex, "Invalid confirm upload request"); |
| | 0 | 150 | | return NotFound(new { error = ex.Message }); |
| | | 151 | | } |
| | 0 | 152 | | } |
| | | 153 | | |
| | | 154 | | /// <summary> |
| | | 155 | | /// PUT /worlds/{worldId}/documents/{documentId} - Update document metadata. |
| | | 156 | | /// </summary> |
| | | 157 | | [HttpPut("{documentId:guid}")] |
| | | 158 | | public async Task<ActionResult<WorldDocumentDto>> UpdateWorldDocument( |
| | | 159 | | Guid worldId, |
| | | 160 | | Guid documentId, |
| | | 161 | | [FromBody] WorldDocumentUpdateDto update) |
| | | 162 | | { |
| | 0 | 163 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 164 | | _logger.LogDebug("User {UserId} updating document {DocumentId}", user.Id, documentId); |
| | | 165 | | |
| | 0 | 166 | | if (update == null) |
| | | 167 | | { |
| | 0 | 168 | | return BadRequest(new { error = "Invalid request body" }); |
| | | 169 | | } |
| | | 170 | | |
| | | 171 | | try |
| | | 172 | | { |
| | 0 | 173 | | var result = await _documentService.UpdateDocumentAsync(worldId, documentId, user.Id, update); |
| | 0 | 174 | | return Ok(result); |
| | | 175 | | } |
| | 0 | 176 | | catch (UnauthorizedAccessException ex) |
| | | 177 | | { |
| | 0 | 178 | | _logger.LogWarning(ex, "Unauthorized update request"); |
| | 0 | 179 | | return StatusCode(403, new { error = ex.Message }); |
| | | 180 | | } |
| | 0 | 181 | | catch (InvalidOperationException ex) |
| | | 182 | | { |
| | 0 | 183 | | _logger.LogWarning(ex, "Document not found for update"); |
| | 0 | 184 | | return NotFound(new { error = ex.Message }); |
| | | 185 | | } |
| | 0 | 186 | | } |
| | | 187 | | |
| | | 188 | | /// <summary> |
| | | 189 | | /// DELETE /worlds/{worldId}/documents/{documentId} - Delete a document. |
| | | 190 | | /// </summary> |
| | | 191 | | [HttpDelete("{documentId:guid}")] |
| | | 192 | | public async Task<IActionResult> DeleteWorldDocument(Guid worldId, Guid documentId) |
| | | 193 | | { |
| | 0 | 194 | | var user = await _currentUserService.GetRequiredUserAsync(); |
| | 0 | 195 | | _logger.LogDebug("User {UserId} deleting document {DocumentId}", user.Id, documentId); |
| | | 196 | | |
| | | 197 | | try |
| | | 198 | | { |
| | 0 | 199 | | await _documentService.DeleteDocumentAsync(worldId, documentId, user.Id); |
| | 0 | 200 | | return NoContent(); |
| | | 201 | | } |
| | 0 | 202 | | catch (UnauthorizedAccessException ex) |
| | | 203 | | { |
| | 0 | 204 | | _logger.LogWarning(ex, "Unauthorized delete request"); |
| | 0 | 205 | | return StatusCode(403, new { error = ex.Message }); |
| | | 206 | | } |
| | 0 | 207 | | catch (InvalidOperationException ex) |
| | | 208 | | { |
| | 0 | 209 | | _logger.LogWarning(ex, "Document not found for deletion"); |
| | 0 | 210 | | return NotFound(new { error = ex.Message }); |
| | | 211 | | } |
| | 0 | 212 | | } |
| | | 213 | | } |