using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using QuotifyBE.Data; using QuotifyBE.DTOs; using QuotifyBE.Entities; using QuotifyBE.Mapping; using System.Security.Claims; using Microsoft.EntityFrameworkCore; namespace QuotifyBE.Controllers; [ApiController] [Route("api/v1/quotes")] [Produces("application/json")] public class QuotesController : ControllerBase { private readonly ApplicationDbContext _db; private readonly GeneralUseHelpers guhf; public QuotesController(ApplicationDbContext db, GeneralUseHelpers GUHF) { _db = db; guhf = GUHF; } // GET /api/v1/quotes /// /// Get a page of quotes /// /// A page of quotes consists of 10 quotes or less. If a page does not contain any quotes, 404 is returned. /// The page number /// A page (10 quotes) /// Returned on valid request /// Returned when requested page is invalid [HttpGet("page/{page_no}")] [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(ErrorDTO), 404)] public async Task GetQuotePage(int page_no) { var totalQuotes = await _db.Quotes.CountAsync(); const int PageSize = 10; if (page_no <= 0) { return NotFound(new ErrorDTO { Status = "error", Error_msg = "Numer strony musi być większy niż 0" }); } var quotes = await _db.Quotes .Include(q => q.QuoteCategories) .ThenInclude(qc => qc.Category) .Include(q => q.User) .Include(q => q.Image) .OrderBy(q => q.Id) .Skip((page_no - 1) * PageSize) .Take(PageSize) .ToListAsync(); var result = quotes .Select(q => q.ToQuoteShortDTO()) .ToList(); return Ok(result); } // GET /api/v1/quotes/{id} /// /// Get specified quote summary /// /// The quote id in question /// A quote: id, quote content and author, imageUrl and categories if successful, otherwise: error message /// Returned on valid request /// Returned when quote id is invalid or simply doesn't exist [HttpGet("{id}")] [ProducesResponseType(typeof(QuoteShortDTO), 200)] [ProducesResponseType(typeof(ErrorDTO), 404)] public async Task GetQuoteById(int id) { var quote = await _db.Quotes .Include(q => q.QuoteCategories!) .ThenInclude(qc => qc.Category) .Include(q => q.User) .Include(q => q.Image) .FirstOrDefaultAsync(q => q.Id == id); if (quote == null) return NotFound(new { status = "error", error_msg = "Quote not found" }); return Ok(quote.ToQuoteShortDTO()); } // POST /api/v1/quotes/new /// /// [AUTHED] Add a new quote /// /// Newly created quote's id /// Form data containing required quote information /// Returned on valid request /// Returned when any of the categories does not exist /// Returned when user's id does not match the creator's id [HttpPost("new")] [Authorize] [ProducesResponseType(201)] // ? FIXME [ProducesResponseType(typeof(ErrorDTO), 400)] [ProducesResponseType(typeof(ErrorDTO), 403)] public async Task CreateQuote([FromBody] CreateQuoteDTO request) { // Get user ID from claims // FIXME var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; if (userIdClaim == null || !int.TryParse(userIdClaim, out int userId)) // https://stackoverflow.com/a/47708867 return StatusCode(403, new ErrorDTO { Status = "error", Error_msg = "Invalid user ID" }); // Find or create image Image? image = null; if (!string.IsNullOrEmpty(request.ImageUrl)) { image = await _db.Images.FirstOrDefaultAsync(i => i.Url == request.ImageUrl); if (image == null) { image = new Image { Url = request.ImageUrl }; _db.Images.Add(image); await _db.SaveChangesAsync(); } } // Create quote var quote = new Quote { Text = request.Text, Author = request.Author, CreatedAt = DateTime.UtcNow, LastUpdatedAt = DateTime.UtcNow, ImageId = image?.Id ?? 0, UserId = userId, QuoteCategories = new List() }; // Attach categories foreach (var categoryId in request.CategoryIds) { var categoryExists = await _db.Categories.AnyAsync(c => c.Id == categoryId); if (!categoryExists) return BadRequest(new ErrorDTO { Status = "error", Error_msg = $"Category ID {categoryId} not found"}); quote.QuoteCategories.Add(new QuoteCategory { CategoryId = categoryId, Quote = quote }); } _db.Quotes.Add(quote); await _db.SaveChangesAsync(); return CreatedAtAction(nameof(GetQuoteById), new { id = quote.Id }, quote); } // GET /api/v1/quotes/random /// /// Get a random quote summary /// /// A quote: id, quote content and author, imageUrl and categories if successful, otherwise: error message /// Returned on valid request /// Returned when no quotes exist [HttpGet("random")] [AllowAnonymous] [ProducesResponseType(typeof(QuoteShortDTO), 200)] [ProducesResponseType(typeof(ErrorDTO), 404)] public async Task GetRandomQuote() { var totalQuotes = await _db.Quotes.CountAsync(); if (totalQuotes == 0) return NotFound(new ErrorDTO { Status = "error", Error_msg = "No quotes to choose from" }); var random = new Random(); var skip = random.Next(0, totalQuotes); var quote = await _db.Quotes .Include(q => q.QuoteCategories!) .ThenInclude(qc => qc.Category) .Skip(skip) .Take(1) .FirstOrDefaultAsync(); if (quote == null) return NotFound(new ErrorDTO { Status = "error", Error_msg = "Unknown error - couldn't get quote"}); Image? image = null; if (quote.ImageId != 0) { image = await _db.Images.FirstOrDefaultAsync(i => i.Id == quote.ImageId); } var dto = new QuoteShortDTO { Id = quote.Id, Text = quote.Text, Author = quote.Author, ImageUrl = image?.Url, Categories = quote.QuoteCategories? .Select(qc => qc.Category?.Name ?? "") .Where(name => !string.IsNullOrEmpty(name)) .ToList() ?? new List() }; return Ok(dto); } }