using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using QuotifyBE.Data; using QuotifyBE.Entities; using QuotifyBE.DTOs; using QuotifyBE.Mapping; using Microsoft.AspNetCore.Cors; using Microsoft.EntityFrameworkCore; namespace QuotifyBE.Controllers; [ApiController] [EnableCors] [Route("api/v1/uc")] [Produces("application/json")] public class UserContentController : ControllerBase { private readonly IConfiguration _appsettings; private readonly ApplicationDbContext _db; private readonly GeneralUseHelpers guhf; List _allowedExtensions = new List() { ".jpg", ".jpeg", ".jfif", ".png", ".gif", ".avif", ".webp" }; public UserContentController(IConfiguration appsettings, ApplicationDbContext db, GeneralUseHelpers GUHF) { _appsettings = appsettings; _db = db; guhf = GUHF; } // GET /api/v1/uc/images /// /// [AUTHED] Get every image /// /// /// Can (and will) return an empty list if no images are found in DB.
/// Requires authorization with a JWT, has CORS set. ///
/// Returned on valid request [HttpGet("images")] [Authorize] [EnableCors] [ProducesResponseType(typeof(List), 200)] public async Task GetImages() { // Get all the images List images = await _db.Images .ToListAsync(); // Return to user return Ok(images); } // POST /api/v1/uc/images /// /// [AUTHED] Upload an image and get an its URI /// /// /// Allows authorized users to publish images. /// A user-reachable path and image id is returned on success.
///
/// Returned on valid request /// Returned when request does not contain a file or the file is blank /// Returned when image size is too large /// Returned when file extension/mimetype is unknown [HttpPost("images")] [Authorize] [EnableCors] [ProducesResponseType(200)] [ProducesResponseType(typeof(ErrorDTO), 400)] [ProducesResponseType(typeof(ErrorDTO), 413)] [ProducesResponseType(typeof(ErrorDTO), 415)] public IActionResult PostNewImage(IFormFile file) { // Obsługa braku pliku if (file == null || file.Length == 0) { return BadRequest(new ErrorDTO { Status = "error", Error_msg = "No file was uploaded." }); } // Dozwolone rozszerzenia string fileExtension = Path.GetExtension(file.FileName).ToLower(); if (!_allowedExtensions.Contains(fileExtension)) { return StatusCode(415, new ErrorDTO { Status = "error", Error_msg = $"Unknown file extension. Allowed: {string.Join(", ", _allowedExtensions)}" }); } // Sprawdzenie typu MIME (opcjonalnie dokładniejsze) if (!file.ContentType.StartsWith("image/")) { return StatusCode(415, new ErrorDTO { Status = "error", Error_msg = "Uploaded file is not an image." }); } // Ograniczenie rozmiaru pliku do tego, ustawionego przez użytkownika int MaxFileSize = int.TryParse(_appsettings.GetSection("UserContent")["MaxFileSize"], out int r) ? r : 5 * 1024 * 1024; if (file.Length > MaxFileSize) { return StatusCode(413, new ErrorDTO { Status = "error", Error_msg = $"File size exceeds {MaxFileSize / 1024 / 1024} MB." }); } // Generowanie unikalnej nazwy string uniqueFileName = $"{Guid.NewGuid()}{fileExtension}"; string relativePath = $"/uploads/images/{uniqueFileName}"; string absolutePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "images", uniqueFileName); // Upewnij się, że katalog istnieje Directory.CreateDirectory(Path.GetDirectoryName(absolutePath)!); // Zapis pliku na dysk using (var stream = new FileStream(absolutePath, FileMode.Create)) { file.CopyTo(stream); } // Dodaj do bazy Image image = new Image { Url = relativePath }; _db.Images.Add(image); _db.SaveChanges(); // Zwracany adres URL (np. do użytku w cytacie) return Ok(new { Status = "ok", Filepath = relativePath, ImageId = image.Id }); } // GET /api/v1/uc/restrictions /// /// [AUTHED] Get server restrictions for file upload /// /// /// Returns a list of allowed file extensions and mimetypes for upload. /// /// Returned on valid request [HttpGet("restrictions")] [Authorize] [EnableCors] [ProducesResponseType(200)] public IActionResult GetFileUploadRestrictions() { return Ok(new { Status = "ok", AllowedMimeTypes = new List { "image/" // this could be done dynamically ~eee4 }, AllowedExtensions = _allowedExtensions, MaxFileSize = int.TryParse(_appsettings.GetSection("UserContent")["MaxFileSize"], out int r) ? r : 5 * 1024 * 1024 }); } // DELETE /api/v1/uc/images/{id} /// /// [AUTHED] Delete an image /// /// /// Deletes an image, granted it exists. ///

/// Note: /// If the image is a file on disk, it's also deleted. ///

/// Warning: /// Any reference to deleted image in Quotes table will also be deleted (nullified). ///
/// Json with status /// Image id which will be deleted /// Returned on valid request /// Returned when no such image exists [HttpDelete("images/{id}")] [Authorize] [EnableCors] [ProducesResponseType(200)] [ProducesResponseType(typeof(ErrorDTO), 404)] public async Task DeleteImage(int id) { // (Attempt to) find the image Image? image = await _db.Images .FirstOrDefaultAsync(q => q.Id == id); // Failed? if (image == null) return NotFound(new { status = "error", error_msg = "Image not found" }); // If succeded, remove the image: // - from disk - if saved locally if (!string.IsNullOrEmpty(image.Url)) { if (image.Url.StartsWith("/uploads/images/")) { // delete from disk int fileNameStart = image.Url.LastIndexOf('/'); string uniqueFileName = image.Url.Substring(fileNameStart + 1); string absolutePath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "uploads", "images", uniqueFileName); System.IO.File.Delete(absolutePath); } } // - from db // - first, from any quotes that reference it List quotesToModify = await _db.Quotes .Include(q => q.Image) .Where(q => q.Image == image) .ToListAsync(); foreach (Quote quote in quotesToModify) { quote.Image = null; } // - finally, from images table _db.Images.Remove(image); await _db.SaveChangesAsync(); // Return ok return Ok(new { Status = "ok" }); } }