using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nuuru.Server.Auth; using Nuuru.Server.Extensions; using Nuuru.Server.Services; using Nuuru.Server.Services.Storage; namespace Nuuru.Server.Controllers { [ApiController] [Route("api/forum/attachments")] public class ForumAttachmentController : ControllerBase { private readonly IForumAttachmentService _attachmentService; private readonly IForumPostService _forumPostService; private readonly IFileStorageService _fileStorageService; private readonly ILogger _logger; public ForumAttachmentController( IForumAttachmentService attachmentService, IForumPostService forumPostService, IFileStorageService fileStorageService, ILogger logger) { _attachmentService = attachmentService; _forumPostService = forumPostService; _fileStorageService = fileStorageService; _logger = logger; } /// /// Upload a new attachment /// [HttpPost] [Authorize(Policy = Permissions.Forum.UploadAttachment)] [RequestSizeLimit(15 * 1024 * 1024)] // 15MB to allow for base64 overhead public async Task UploadAttachment(IFormFile file) { try { if (file == null || file.Length == 0) { return BadRequest(new { error = "No file provided" }); } var attachment = await _attachmentService.UploadAttachmentAsync(file); if (attachment == null) { return BadRequest(new { error = "Failed to upload attachment. File may be too large or of an unsupported type." }); } return Ok(new { id = attachment.Id, fileName = attachment.OriginalFileName, contentType = attachment.ContentType, fileSize = attachment.FileSize, width = attachment.Width, height = attachment.Height, thumbnailUrl = attachment.ThumbnailIdentifier != null ? $"/api/forum/attachments/{attachment.Id}/thumb" : null }); } catch (Exception ex) { _logger.LogError(ex, "Error uploading attachment"); return StatusCode(500, new { error = "Failed to upload attachment" }); } } /// /// Get attachment metadata /// [HttpGet("{id:guid}")] [AllowAnonymous] public async Task GetAttachment(Guid id) { try { var attachment = await _attachmentService.GetAttachmentAsync(id); if (attachment == null) { return NotFound(new { error = "Attachment not found" }); } return Ok(new { id = attachment.Id, postId = attachment.ForumPostId, fileName = attachment.OriginalFileName, contentType = attachment.ContentType, fileSize = attachment.FileSize, width = attachment.Width, height = attachment.Height, thumbnailUrl = attachment.ThumbnailIdentifier != null ? $"/api/forum/attachments/{attachment.Id}/thumb" : null, createdAt = attachment.CreatedAt }); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving attachment {AttachmentId}", id); return StatusCode(500, new { error = "Failed to retrieve attachment" }); } } /// /// Get attachment file content /// [HttpGet("{id:guid}/file")] [AllowAnonymous] public async Task GetAttachmentFile(Guid id) { try { var attachment = await _attachmentService.GetAttachmentAsync(id); if (attachment == null) { return NotFound(new { error = "Attachment not found" }); } var fileResult = await _fileStorageService.GetFileAsync(attachment.FileIdentifier); if (fileResult == null) { _logger.LogWarning("File not found for attachment {AttachmentId}", id); return NotFound(new { error = "File not found" }); } Response.SetFileCacheHeaders(); Response.SetInlineFileHeaders(attachment.OriginalFileName); return File(fileResult.Stream, attachment.ContentType); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving attachment file {AttachmentId}", id); return StatusCode(500, new { error = "Failed to retrieve attachment file" }); } } /// /// Get attachment thumbnail /// [HttpGet("{id:guid}/thumb")] [AllowAnonymous] public async Task GetAttachmentThumbnail(Guid id) { try { var attachment = await _attachmentService.GetAttachmentAsync(id); if (attachment == null) { return NotFound(new { error = "Attachment not found" }); } if (string.IsNullOrEmpty(attachment.ThumbnailIdentifier)) { // No thumbnail - redirect to full file return RedirectToAction(nameof(GetAttachmentFile), new { id }); } var fileResult = await _fileStorageService.GetFileAsync(attachment.ThumbnailIdentifier); if (fileResult == null) { _logger.LogWarning("Thumbnail not found for attachment {AttachmentId}", id); return RedirectToAction(nameof(GetAttachmentFile), new { id }); } Response.SetFileCacheHeaders(); return File(fileResult.Stream, fileResult.Metadata.ContentType); } catch (Exception ex) { _logger.LogError(ex, "Error retrieving attachment thumbnail {AttachmentId}", id); return StatusCode(500, new { error = "Failed to retrieve thumbnail" }); } } /// /// Associate attachments with a post /// [HttpPost("associate")] [Authorize(Policy = Permissions.Forum.UploadAttachment)] public async Task AssociateAttachments([FromBody] AssociateAttachmentsRequest request) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } if (request.AttachmentIds == null || request.AttachmentIds.Count == 0) { return BadRequest(new { error = "No attachment IDs provided" }); } // Verify the user is the author of the post var post = await _forumPostService.GetPostByIdAsync(request.PostId); if (post == null) { return NotFound(new { error = "Post not found" }); } if (post.AuthorId != userId.Value) { return Forbid(); } var success = await _attachmentService.AssociateAttachmentsAsync( request.PostId, request.AttachmentIds); if (!success) { return BadRequest(new { error = "Failed to associate attachments. Some attachments may not exist or may not belong to you." }); } return Ok(new { success = true }); } catch (Exception ex) { _logger.LogError(ex, "Error associating attachments"); return StatusCode(500, new { error = "Failed to associate attachments" }); } } /// /// Delete an attachment (owner or admin only) /// [HttpDelete("{id:guid}")] [Authorize] public async Task DeleteAttachment(Guid id) { try { var userId = User.GetUserId(); if (userId == null) { return Unauthorized(new { error = "User ID not found in token" }); } var attachment = await _attachmentService.GetAttachmentAsync(id); if (attachment == null) { return NotFound(new { error = "Attachment not found" }); } // Check ownership (unless admin) var isAdmin = User.HasPermission(Permissions.Forum.DeletePost); if (!isAdmin && attachment.UploaderId != userId.Value) { return Forbid(); } await _attachmentService.DeleteAttachmentAsync(id); return NoContent(); } catch (Exception ex) { _logger.LogError(ex, "Error deleting attachment {AttachmentId}", id); return StatusCode(500, new { error = "Failed to delete attachment" }); } } } public class AssociateAttachmentsRequest { public int PostId { get; set; } public List AttachmentIds { get; set; } = []; } }