diff --git a/Bruno/Seed/Post operations/Get specific tag, specific post, specific comment.bru b/Bruno/Seed/Post operations/Get specific tag, specific post, specific comment.bru new file mode 100644 index 0000000..55b1b5f --- /dev/null +++ b/Bruno/Seed/Post operations/Get specific tag, specific post, specific comment.bru @@ -0,0 +1,11 @@ +meta { + name: Get specific tag, specific post, specific comment + type: http + seq: 7 +} + +get { + url: {{baseUrl}}/api/tags/high-res/posts/1/comments/1 + body: none + auth: inherit +} diff --git a/T120B165-ImgBoard/Controllers/AuthController.cs b/T120B165-ImgBoard/Controllers/AuthController.cs index e0c351b..653fed3 100644 --- a/T120B165-ImgBoard/Controllers/AuthController.cs +++ b/T120B165-ImgBoard/Controllers/AuthController.cs @@ -21,6 +21,7 @@ public class AuthController(UserManager userManager, ITokenService tokenSe [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task> Register(RegisterDto dto) { var user = new User @@ -31,9 +32,18 @@ public class AuthController(UserManager userManager, ITokenService tokenSe var result = await userManager.CreateAsync(user, dto.Password); await userManager.AddToRoleAsync(user, UserRoles.Regular); + + Dictionary idk = new() + { + ["errors"] = result.Errors + }; + if (!result.Succeeded) { - return BadRequest(result.Errors); + return Problem( + statusCode: StatusCodes.Status422UnprocessableEntity, + extensions: idk + ); } return Ok(SlimUserDto.FromUser(user)); } @@ -46,6 +56,7 @@ public class AuthController(UserManager userManager, ITokenService tokenSe /// If the credentials are incorrect [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> Login(LoginDto dto) { @@ -66,6 +77,7 @@ public class AuthController(UserManager userManager, ITokenService tokenSe /// If refresh token is missing or is expired [HttpPost("refresh")] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> Refresh(RefreshDto dto) { @@ -88,6 +100,7 @@ public class AuthController(UserManager userManager, ITokenService tokenSe /// If refresh token is missing or is expired [HttpPost("revoke")] [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] public async Task> Revoke(RefreshDto dto) { diff --git a/T120B165-ImgBoard/Controllers/PostController.cs b/T120B165-ImgBoard/Controllers/PostController.cs index 6e0d032..3e81bed 100644 --- a/T120B165-ImgBoard/Controllers/PostController.cs +++ b/T120B165-ImgBoard/Controllers/PostController.cs @@ -102,7 +102,7 @@ public class PostController( if (tag == null) return Problem( detail: $"'{tagName}' is not a valid tag", - statusCode: StatusCodes.Status400BadRequest + statusCode: StatusCodes.Status422UnprocessableEntity ); tags.Add(tag); } @@ -123,6 +123,7 @@ public class PostController( [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task> Create(CreatePostDto dto) { var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; @@ -172,7 +173,9 @@ public class PostController( [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status409Conflict)] [ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task PatchFileContent(int postId, int fileId) { var post = await postService.GetById(postId, includeUnfinished: true); @@ -186,7 +189,7 @@ public class PostController( if (userId != post.Author.Id) return Forbid(); if (fileRecord.FinishedDate != null) - return Problem(statusCode: StatusCodes.Status400BadRequest, + return Problem(statusCode: StatusCodes.Status409Conflict, detail: "File was already uploaded."); // Parse the Content-Range Header @@ -204,7 +207,7 @@ public class PostController( if (totalSizeFromHeader != totalSize) { return Problem( - statusCode: StatusCodes.Status400BadRequest, + statusCode: StatusCodes.Status422UnprocessableEntity, detail: $"Total file size mismatch. Expected: {totalSize} bytes, Received: {totalSizeFromHeader} bytes." ); } @@ -383,6 +386,7 @@ public class PostController( [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task> Update(int id, EditPostDto dto) { var post = await postService.GetById(id); @@ -550,6 +554,7 @@ public class PostController( [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] public async Task> Update(int postId, int commentId, EditCommentDto dto) { var post = await postService.GetById(postId); diff --git a/T120B165-ImgBoard/Controllers/TagController.cs b/T120B165-ImgBoard/Controllers/TagController.cs index f7683ba..ae9eb82 100644 --- a/T120B165-ImgBoard/Controllers/TagController.cs +++ b/T120B165-ImgBoard/Controllers/TagController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using T120B165_ImgBoard.Dtos; +using T120B165_ImgBoard.Dtos.Comment; using T120B165_ImgBoard.Dtos.Tag; using T120B165_ImgBoard.Models; using T120B165_ImgBoard.Services; @@ -10,7 +10,10 @@ namespace T120B165_ImgBoard.Controllers; [ApiController] [Route("api/tags")] -public class TagController(ITagService tagService) : ControllerBase +public class TagController( + ITagService tagService, + IPostService postService, + ICommentService commentService) : ControllerBase { /// /// Creates a new tag. @@ -126,4 +129,34 @@ public class TagController(ITagService tagService) : ControllerBase var updatedTag = await tagService.Update(tag, dto.Type); return Ok(updatedTag); } + + + /// + /// Get specific tag, specific post comment. + /// + /// Tag name + /// Post ID + /// Comment ID + /// Comment data + /// If request is malformed + /// If tag or post or comment is not found + [HttpGet("{tagName}/posts/{postId:int}/comments/{commentId:int}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> GetComment(string tagName, int postId, int commentId) + { + var tag = await tagService.GetByName(tagName); + if (tag == null) return NotFound(); + + var entry = await postService.GetById(postId); + if (entry == null) return NotFound(); + + if (entry.Tags.All(t => t.Name != tag.Name)) return NotFound(); + + var comment = await commentService.GetById(commentId); + if (comment == null || entry.Id != comment.OriginalPost.Id) return NotFound(); + + return Ok(CommentDto.FromComment(comment)); + } } diff --git a/T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs b/T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs index 40476e7..8bb5f57 100644 --- a/T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs +++ b/T120B165-ImgBoard/Dtos/Comment/CreateCommentDto.cs @@ -1,4 +1,6 @@ +using System.ComponentModel.DataAnnotations; + namespace T120B165_ImgBoard.Dtos.Comment; -public record CreateCommentDto(string Text); -public record EditCommentDto(string Text); \ No newline at end of file +public record CreateCommentDto([Required] string Text); +public record EditCommentDto([Required] string Text); \ No newline at end of file diff --git a/T120B165-ImgBoard/Dtos/LoginDto.cs b/T120B165-ImgBoard/Dtos/LoginDto.cs index d9333af..0371403 100644 --- a/T120B165-ImgBoard/Dtos/LoginDto.cs +++ b/T120B165-ImgBoard/Dtos/LoginDto.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; + namespace T120B165_ImgBoard.Dtos; -public record LoginDto(string Email, string Password); \ No newline at end of file +public record LoginDto([Required] string Email, [Required] string Password); \ No newline at end of file diff --git a/T120B165-ImgBoard/Dtos/RegisterDto.cs b/T120B165-ImgBoard/Dtos/RegisterDto.cs index ac14014..83431ec 100644 --- a/T120B165-ImgBoard/Dtos/RegisterDto.cs +++ b/T120B165-ImgBoard/Dtos/RegisterDto.cs @@ -3,8 +3,11 @@ using System.ComponentModel.DataAnnotations; namespace T120B165_ImgBoard.Dtos; public record RegisterDto( + [Required] string UserName, [EmailAddress] + [Required] string Email, + [Required] string Password ); \ No newline at end of file diff --git a/T120B165-ImgBoard/Program.cs b/T120B165-ImgBoard/Program.cs index ebf0596..7e96c6b 100644 --- a/T120B165-ImgBoard/Program.cs +++ b/T120B165-ImgBoard/Program.cs @@ -2,6 +2,8 @@ using System.Net; using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json.Converters; @@ -22,8 +24,34 @@ public class Program builder.Services.AddAuthorization(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi - builder.Services.AddControllers().AddNewtonsoftJson( - options => options.SerializerSettings.Converters.Add(new StringEnumConverter())); + builder.Services.AddControllers() + .AddNewtonsoftJson( + options => options.SerializerSettings.Converters.Add(new StringEnumConverter())) + /*.ConfigureApiBehaviorOptions(opt => + { + opt.InvalidModelStateResponseFactory = context => + { + var problemDetails = new ValidationProblemDetails(context.ModelState); + + var isBindingError = context.ModelState.Values.Any(v => + v.ValidationState == ModelValidationState.Invalid && v.Errors.Any(e => + e.Exception is not null || (!string.IsNullOrWhiteSpace(e.ErrorMessage) && e.ErrorMessage.Contains("body is required.")) + )); + + if (isBindingError) + { + problemDetails.Status = StatusCodes.Status400BadRequest; + return new BadRequestObjectResult(problemDetails); + } + problemDetails.Status = StatusCodes.Status422UnprocessableEntity; + var result = new ObjectResult(problemDetails) + { + StatusCode = StatusCodes.Status422UnprocessableEntity, + }; + result.ContentTypes.Add("application/json"); + return result; + }; + })*/; builder.Services.AddOpenApiDocument(cfg => { cfg.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth"));