diff --git a/Controllers/QuoteController.cs b/Controllers/QuoteController.cs
index 9f1a4cd..35ec1f4 100644
--- a/Controllers/QuoteController.cs
+++ b/Controllers/QuoteController.cs
@@ -1,192 +1,201 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Authorization.Infrastructure;
-using Microsoft.AspNetCore.Cors;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Update.Internal;
-using QuotifyBE.Data;
-using QuotifyBE.DTOs;
-using QuotifyBE.Entities;
-using QuotifyBE.Mapping;
-using System.Reflection.Metadata.Ecma335;
-using System.Security.Claims;
-
-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, an empty list is returned.
- ///
- /// Important!
- /// Has CORS set, unlike e.g. GET /api/v1/quote/{id} or GET /api/v1/quote/random.
- ///
- /// The page number
- /// A page (10 quotes)
- /// Returned on valid request
- /// Returned when requested page is invalid (page_no <= 0)
- [HttpGet("page/{page_no}")]
- [EnableCors]
- [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}
- ///
- /// [AUTHED] Get specified quote summary
- ///
- ///
- /// As per project's guidelines, requires a JWT.
- ///
- /// 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}")]
- [Authorize]
- [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]
- [EnableCors]
- [ProducesResponseType(201)]
- [ProducesResponseType(typeof(ErrorDTO), 400)]
- [ProducesResponseType(typeof(ErrorDTO), 403)]
- public async Task CreateQuote([FromBody] CreateQuoteDTO request)
- {
- // Get user ID from claims
-
- 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 ?? null,
- UserId = userId,
- QuoteCategories = new List()
- };
-
- // Attach categories
- foreach (var categoryId in request.CategoryIds ?? [])
- {
- Category? category = await _db.Categories.FirstOrDefaultAsync(c => c.Id == categoryId);
- if (category == null)
- return BadRequest(new ErrorDTO { Status = "error", Error_msg = $"Category ID {categoryId} not found" });
-
- quote.QuoteCategories.Add(new QuoteCategory
- {
- Category = category,
- Quote = quote
- });
- }
-
- _db.Quotes.Add(quote);
- await _db.SaveChangesAsync();
-
- return CreatedAtAction(nameof(GetQuoteById), new { id = quote.Id }, quote.ToQuoteShortDTO());
- }
-
- // 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)]
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+using Microsoft.AspNetCore.Cors;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Update.Internal;
+using QuotifyBE.Data;
+using QuotifyBE.DTOs;
+using QuotifyBE.Entities;
+using QuotifyBE.Mapping;
+using System.Reflection.Metadata.Ecma335;
+using System.Security.Claims;
+
+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, an empty list is returned.
+ ///
+ /// Important!
+ /// Has CORS set, unlike e.g. GET /api/v1/quote/{id} or GET /api/v1/quote/random.
+ ///
+ /// The page number
+ /// A page (10 quotes)
+ /// Returned on valid request
+ /// Returned when requested page is invalid (page_no <= 0)
+ [HttpGet("page/{page_no}")]
+ [EnableCors]
+ [ProducesResponseType(typeof(List), 200)]
[ProducesResponseType(typeof(ErrorDTO), 404)]
- [ProducesResponseType(204)]
+ public async Task GetQuotePage(int page_no, [FromQuery] int? category_id = null)
+ {
+ const int PageSize = 10;
+
+ if (page_no <= 0)
+ {
+ return NotFound(new ErrorDTO { Status = "error", Error_msg = "Numer strony musi być większy niż 0" });
+ }
+
+ // Paginacja bez filtra
+ var baseQuery = _db.Quotes
+ .Include(q => q.QuoteCategories!)
+ .ThenInclude(qc => qc.Category)
+ .Include(q => q.User)
+ .Include(q => q.Image)
+ .OrderBy(q => q.Id);
+
+ var pageQuotes = await baseQuery
+ .Skip((page_no - 1) * PageSize)
+ .Take(PageSize)
+ .ToListAsync();
+
+ // Filtrowanie dopiero po pobraniu strony
+ if (category_id.HasValue)
+ {
+ pageQuotes = pageQuotes
+ .Where(q => q.QuoteCategories!.Any(qc => qc.CategoryId == category_id.Value))
+ .ToList();
+ }
+
+ var result = pageQuotes
+ .Select(q => q.ToQuoteShortDTO())
+ .ToList();
+
+ return Ok(result);
+ }
+ // GET /api/v1/quotes/{id}
+ ///
+ /// [AUTHED] Get specified quote summary
+ ///
+ ///
+ /// As per project's guidelines, requires a JWT.
+ ///
+ /// 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}")]
+ [Authorize]
+ [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]
+ [EnableCors]
+ [ProducesResponseType(201)]
+ [ProducesResponseType(typeof(ErrorDTO), 400)]
+ [ProducesResponseType(typeof(ErrorDTO), 403)]
+ public async Task CreateQuote([FromBody] CreateQuoteDTO request)
+ {
+ // Get user ID from claims
+
+ 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 ?? null,
+ UserId = userId,
+ QuoteCategories = new List()
+ };
+
+ // Attach categories
+ foreach (var categoryId in request.CategoryIds ?? [])
+ {
+ Category? category = await _db.Categories.FirstOrDefaultAsync(c => c.Id == categoryId);
+ if (category == null)
+ return BadRequest(new ErrorDTO { Status = "error", Error_msg = $"Category ID {categoryId} not found" });
+
+ quote.QuoteCategories.Add(new QuoteCategory
+ {
+ Category = category,
+ Quote = quote
+ });
+ }
+
+ _db.Quotes.Add(quote);
+ await _db.SaveChangesAsync();
+
+ return CreatedAtAction(nameof(GetQuoteById), new { id = quote.Id }, quote.ToQuoteShortDTO());
+ }
+
+ // 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)]
+ [ProducesResponseType(204)]
public async Task GetRandomQuote([FromQuery] int? category_id = null)
{
IQueryable query = _db.Quotes.Include(q => q.QuoteCategories!).ThenInclude(qc => qc.Category);
@@ -239,173 +248,173 @@ public class QuotesController : ControllerBase
return Ok(dto);
- }
-
- // DELETE /api/v1/quotes/{id}
- ///
- /// [AUTHED] Delete a quote
- ///
- ///
- /// Deletes a quote, granted it exists.
- ///
- ///
- /// Is this the best practice? Marking the quote as hidden is also an option.
- /// ~eee4
- ///
- /// Json with status
- /// Quote id which will be deleted
- /// Returned on valid request
- /// Returned when no such quote exists
- [HttpDelete("{id}")]
- [Authorize]
- [EnableCors]
- [ProducesResponseType(200)]
- [ProducesResponseType(typeof(ErrorDTO), 404)]
- public async Task DeleteQuote(int id)
- {
- // (Attempt to) find the quote
- Quote? quote = await _db.Quotes
- .FirstOrDefaultAsync(q => q.Id == id);
- // Failed?
- if (quote == null)
- return NotFound(new { status = "error", error_msg = "Quote not found" });
-
- // If succeded, remove the quote
- _db.Quotes.Remove(quote);
- await _db.SaveChangesAsync();
-
- // ====================================================================== //
- // Important! //
- // Is this the best we can do? Won't marking the quote as "hidden" //
- // be better than explicitly deleting it? ~eee4 //
- // ====================================================================== //
-
- // Return ok
- return Ok(new { Status = "ok" });
- }
-
- // PATCH /api/v1/quotes/{id}
- ///
- /// [AUTHED] Modify an existing quote
- ///
- ///
- /// Modifies an existing quote.
- ///
- /// Warning!
- /// We don't check the user id which created the quote.
- /// In case of single-user instances, this should not be a problem.
- /// This might become one, if we want users with non-admin roles;
- /// that would need some proper ACL checks here (with the help of GUHF).
- ///
- /// Important!
- /// Image handling works the same as with creating new quote.
- /// This means that images not present in the DB will be added automatically.
- ///
- /// Important!
- /// "categories = null" is not the same as "categories = []"!
- /// While "categories = null" will not alter the quote's categories,
- /// "categories = []" will (and in turn, empty each and every present category)!
- /// Be careful when handling user-provided categories!
- ///
- /// Newly modified quote as a DTO
- /// Quote to be modified
- /// Updated quote form data
- /// Returned on valid request
- /// Returned when request text or author is empty (or whitespace)
- /// Returned when no such quote exists
- [HttpPatch("{id}")]
- [Authorize]
- [EnableCors]
- [ProducesResponseType(typeof(QuoteShortDTO), 200)]
- [ProducesResponseType(typeof(ErrorDTO), 400)]
- [ProducesResponseType(typeof(ErrorDTO), 404)]
- public async Task EditQuote(int id, [FromBody] QuoteShortDTO updatedQuote)
- {
- // Try to find the quote in question
- Quote? quote = await _db.Quotes
- .Include(q => q.QuoteCategories)
- .FirstOrDefaultAsync(q => q.Id == id);
-
- // Failed?
- if (quote == null)
- return NotFound(new { status = "error", error_msg = "Quote not found" });
-
- // Is quote contents or author empty?
- if (string.IsNullOrWhiteSpace(updatedQuote.Text) || string.IsNullOrWhiteSpace(updatedQuote.Author))
- return BadRequest(new ErrorDTO { Status = "error", Error_msg = "Text and author are required." });
-
- // Alter the quote's content
- quote.Text = updatedQuote.Text;
- quote.Author = updatedQuote.Author;
- quote.LastUpdatedAt = DateTime.UtcNow;
-
- // Try to find the image inside the DB
- Image? image = null;
- if (!string.IsNullOrEmpty(updatedQuote.ImageUrl))
- {
- image = await _db.Images.FirstOrDefaultAsync(i => i.Url == updatedQuote.ImageUrl);
- // Failed? Just insert it yourself
- if (image == null)
- {
- image = new Image { Url = updatedQuote.ImageUrl };
- _db.Images.Add(image);
- await _db.SaveChangesAsync();
- }
- }
- quote.Image = image;
-
- // Don't touch categories if they are explicitly null
- if (updatedQuote.Categories == null) { }
- // If they aren't
- else if (updatedQuote.Categories.Any())
- {
- // Get all the categories associated with a quote from DB
- List categoriesFromDb = await _db.Categories
- .Where(c => updatedQuote.Categories.Contains(c.Name))
- .ToListAsync();
-
- // Determine which ones are already present, and which to add
- IEnumerable existingNames = categoriesFromDb
- .Select(c => c.Name);
- List newNames = updatedQuote.Categories
- .Except(existingNames)
- .ToList();
-
- // For all the categories not present
- foreach (var name in newNames)
- {
- // Add them to the DB
- var newCat = new Category {
- Name = name,
- Description = string.Empty,
- CreatedAt = DateTime.UtcNow
- };
- _db.Categories.Add(newCat);
- categoriesFromDb.Add(newCat);
- }
-
- // If any categories were added, save changes
- if (newNames.Any())
- await _db.SaveChangesAsync();
-
- // Assign all the new categories to the quote
- quote.QuoteCategories = categoriesFromDb
- .Select(cat => new QuoteCategory {
- CategoryId = cat.Id,
- QuoteId = quote.Id
- })
- .ToList();
- }
- else
- {
- // No categories (empty list) inside DTO?
- // Clear them all!
- quote.QuoteCategories.Clear();
- }
-
- // Save changes, return new quote as a DTO
- await _db.SaveChangesAsync();
- return Ok(quote.ToQuoteShortDTO());
- }
-
-}
+ }
+
+ // DELETE /api/v1/quotes/{id}
+ ///
+ /// [AUTHED] Delete a quote
+ ///
+ ///
+ /// Deletes a quote, granted it exists.
+ ///
+ ///
+ /// Is this the best practice? Marking the quote as hidden is also an option.
+ /// ~eee4
+ ///
+ /// Json with status
+ /// Quote id which will be deleted
+ /// Returned on valid request
+ /// Returned when no such quote exists
+ [HttpDelete("{id}")]
+ [Authorize]
+ [EnableCors]
+ [ProducesResponseType(200)]
+ [ProducesResponseType(typeof(ErrorDTO), 404)]
+ public async Task DeleteQuote(int id)
+ {
+ // (Attempt to) find the quote
+ Quote? quote = await _db.Quotes
+ .FirstOrDefaultAsync(q => q.Id == id);
+ // Failed?
+ if (quote == null)
+ return NotFound(new { status = "error", error_msg = "Quote not found" });
+
+ // If succeded, remove the quote
+ _db.Quotes.Remove(quote);
+ await _db.SaveChangesAsync();
+
+ // ====================================================================== //
+ // Important! //
+ // Is this the best we can do? Won't marking the quote as "hidden" //
+ // be better than explicitly deleting it? ~eee4 //
+ // ====================================================================== //
+
+ // Return ok
+ return Ok(new { Status = "ok" });
+ }
+
+ // PATCH /api/v1/quotes/{id}
+ ///
+ /// [AUTHED] Modify an existing quote
+ ///
+ ///
+ /// Modifies an existing quote.
+ ///
+ /// Warning!
+ /// We don't check the user id which created the quote.
+ /// In case of single-user instances, this should not be a problem.
+ /// This might become one, if we want users with non-admin roles;
+ /// that would need some proper ACL checks here (with the help of GUHF).
+ ///
+ /// Important!
+ /// Image handling works the same as with creating new quote.
+ /// This means that images not present in the DB will be added automatically.
+ ///
+ /// Important!
+ /// "categories = null" is not the same as "categories = []"!
+ /// While "categories = null" will not alter the quote's categories,
+ /// "categories = []" will (and in turn, empty each and every present category)!
+ /// Be careful when handling user-provided categories!
+ ///
+ /// Newly modified quote as a DTO
+ /// Quote to be modified
+ /// Updated quote form data
+ /// Returned on valid request
+ /// Returned when request text or author is empty (or whitespace)
+ /// Returned when no such quote exists
+ [HttpPatch("{id}")]
+ [Authorize]
+ [EnableCors]
+ [ProducesResponseType(typeof(QuoteShortDTO), 200)]
+ [ProducesResponseType(typeof(ErrorDTO), 400)]
+ [ProducesResponseType(typeof(ErrorDTO), 404)]
+ public async Task EditQuote(int id, [FromBody] QuoteShortDTO updatedQuote)
+ {
+ // Try to find the quote in question
+ Quote? quote = await _db.Quotes
+ .Include(q => q.QuoteCategories)
+ .FirstOrDefaultAsync(q => q.Id == id);
+
+ // Failed?
+ if (quote == null)
+ return NotFound(new { status = "error", error_msg = "Quote not found" });
+
+ // Is quote contents or author empty?
+ if (string.IsNullOrWhiteSpace(updatedQuote.Text) || string.IsNullOrWhiteSpace(updatedQuote.Author))
+ return BadRequest(new ErrorDTO { Status = "error", Error_msg = "Text and author are required." });
+
+ // Alter the quote's content
+ quote.Text = updatedQuote.Text;
+ quote.Author = updatedQuote.Author;
+ quote.LastUpdatedAt = DateTime.UtcNow;
+
+ // Try to find the image inside the DB
+ Image? image = null;
+ if (!string.IsNullOrEmpty(updatedQuote.ImageUrl))
+ {
+ image = await _db.Images.FirstOrDefaultAsync(i => i.Url == updatedQuote.ImageUrl);
+ // Failed? Just insert it yourself
+ if (image == null)
+ {
+ image = new Image { Url = updatedQuote.ImageUrl };
+ _db.Images.Add(image);
+ await _db.SaveChangesAsync();
+ }
+ }
+ quote.Image = image;
+
+ // Don't touch categories if they are explicitly null
+ if (updatedQuote.Categories == null) { }
+ // If they aren't
+ else if (updatedQuote.Categories.Any())
+ {
+ // Get all the categories associated with a quote from DB
+ List categoriesFromDb = await _db.Categories
+ .Where(c => updatedQuote.Categories.Contains(c.Name))
+ .ToListAsync();
+
+ // Determine which ones are already present, and which to add
+ IEnumerable existingNames = categoriesFromDb
+ .Select(c => c.Name);
+ List newNames = updatedQuote.Categories
+ .Except(existingNames)
+ .ToList();
+
+ // For all the categories not present
+ foreach (var name in newNames)
+ {
+ // Add them to the DB
+ var newCat = new Category {
+ Name = name,
+ Description = string.Empty,
+ CreatedAt = DateTime.UtcNow
+ };
+ _db.Categories.Add(newCat);
+ categoriesFromDb.Add(newCat);
+ }
+
+ // If any categories were added, save changes
+ if (newNames.Any())
+ await _db.SaveChangesAsync();
+
+ // Assign all the new categories to the quote
+ quote.QuoteCategories = categoriesFromDb
+ .Select(cat => new QuoteCategory {
+ CategoryId = cat.Id,
+ QuoteId = quote.Id
+ })
+ .ToList();
+ }
+ else
+ {
+ // No categories (empty list) inside DTO?
+ // Clear them all!
+ quote.QuoteCategories.Clear();
+ }
+
+ // Save changes, return new quote as a DTO
+ await _db.SaveChangesAsync();
+ return Ok(quote.ToQuoteShortDTO());
+ }
+
+}