Add one long hierarchical method, change some return codes

This commit is contained in:
2025-10-09 20:30:39 +03:00
parent 96a5e764c2
commit 30cb0521f6
8 changed files with 108 additions and 11 deletions

View File

@@ -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
}

View File

@@ -21,6 +21,7 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
[HttpPost("register")] [HttpPost("register")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<ActionResult<SlimUserDto>> Register(RegisterDto dto) public async Task<ActionResult<SlimUserDto>> Register(RegisterDto dto)
{ {
var user = new User var user = new User
@@ -31,9 +32,18 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
var result = await userManager.CreateAsync(user, dto.Password); var result = await userManager.CreateAsync(user, dto.Password);
await userManager.AddToRoleAsync(user, UserRoles.Regular); await userManager.AddToRoleAsync(user, UserRoles.Regular);
Dictionary<string, object> idk = new()
{
["errors"] = result.Errors
};
if (!result.Succeeded) if (!result.Succeeded)
{ {
return BadRequest(result.Errors); return Problem(
statusCode: StatusCodes.Status422UnprocessableEntity,
extensions: idk
);
} }
return Ok(SlimUserDto.FromUser(user)); return Ok(SlimUserDto.FromUser(user));
} }
@@ -46,6 +56,7 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
/// <response code="401">If the credentials are incorrect</response> /// <response code="401">If the credentials are incorrect</response>
[HttpPost("login")] [HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<TokenDto>> Login(LoginDto dto) public async Task<ActionResult<TokenDto>> Login(LoginDto dto)
{ {
@@ -66,6 +77,7 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
/// <response code="401">If refresh token is missing or is expired</response> /// <response code="401">If refresh token is missing or is expired</response>
[HttpPost("refresh")] [HttpPost("refresh")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<TokenDto>> Refresh(RefreshDto dto) public async Task<ActionResult<TokenDto>> Refresh(RefreshDto dto)
{ {
@@ -88,6 +100,7 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
/// <response code="401">If refresh token is missing or is expired</response> /// <response code="401">If refresh token is missing or is expired</response>
[HttpPost("revoke")] [HttpPost("revoke")]
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<TokenDto>> Revoke(RefreshDto dto) public async Task<ActionResult<TokenDto>> Revoke(RefreshDto dto)
{ {

View File

@@ -102,7 +102,7 @@ public class PostController(
if (tag == null) if (tag == null)
return Problem( return Problem(
detail: $"'{tagName}' is not a valid tag", detail: $"'{tagName}' is not a valid tag",
statusCode: StatusCodes.Status400BadRequest statusCode: StatusCodes.Status422UnprocessableEntity
); );
tags.Add(tag); tags.Add(tag);
} }
@@ -123,6 +123,7 @@ public class PostController(
[ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<ActionResult<PostDto>> Create(CreatePostDto dto) public async Task<ActionResult<PostDto>> Create(CreatePostDto dto)
{ {
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value; var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
@@ -172,7 +173,9 @@ public class PostController(
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
[ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)] [ProducesResponseType(StatusCodes.Status415UnsupportedMediaType)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<IActionResult> PatchFileContent(int postId, int fileId) public async Task<IActionResult> PatchFileContent(int postId, int fileId)
{ {
var post = await postService.GetById(postId, includeUnfinished: true); var post = await postService.GetById(postId, includeUnfinished: true);
@@ -186,7 +189,7 @@ public class PostController(
if (userId != post.Author.Id) return Forbid(); if (userId != post.Author.Id) return Forbid();
if (fileRecord.FinishedDate != null) if (fileRecord.FinishedDate != null)
return Problem(statusCode: StatusCodes.Status400BadRequest, return Problem(statusCode: StatusCodes.Status409Conflict,
detail: "File was already uploaded."); detail: "File was already uploaded.");
// Parse the Content-Range Header // Parse the Content-Range Header
@@ -204,7 +207,7 @@ public class PostController(
if (totalSizeFromHeader != totalSize) if (totalSizeFromHeader != totalSize)
{ {
return Problem( return Problem(
statusCode: StatusCodes.Status400BadRequest, statusCode: StatusCodes.Status422UnprocessableEntity,
detail: $"Total file size mismatch. Expected: {totalSize} bytes, Received: {totalSizeFromHeader} bytes." detail: $"Total file size mismatch. Expected: {totalSize} bytes, Received: {totalSizeFromHeader} bytes."
); );
} }
@@ -383,6 +386,7 @@ public class PostController(
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<ActionResult<PostDto>> Update(int id, EditPostDto dto) public async Task<ActionResult<PostDto>> Update(int id, EditPostDto dto)
{ {
var post = await postService.GetById(id); var post = await postService.GetById(id);
@@ -550,6 +554,7 @@ public class PostController(
[ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
public async Task<ActionResult<CommentDto>> Update(int postId, int commentId, EditCommentDto dto) public async Task<ActionResult<CommentDto>> Update(int postId, int commentId, EditCommentDto dto)
{ {
var post = await postService.GetById(postId); var post = await postService.GetById(postId);

View File

@@ -1,6 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using T120B165_ImgBoard.Dtos; using T120B165_ImgBoard.Dtos.Comment;
using T120B165_ImgBoard.Dtos.Tag; using T120B165_ImgBoard.Dtos.Tag;
using T120B165_ImgBoard.Models; using T120B165_ImgBoard.Models;
using T120B165_ImgBoard.Services; using T120B165_ImgBoard.Services;
@@ -10,7 +10,10 @@ namespace T120B165_ImgBoard.Controllers;
[ApiController] [ApiController]
[Route("api/tags")] [Route("api/tags")]
public class TagController(ITagService tagService) : ControllerBase public class TagController(
ITagService tagService,
IPostService postService,
ICommentService commentService) : ControllerBase
{ {
/// <summary> /// <summary>
/// Creates a new tag. /// Creates a new tag.
@@ -126,4 +129,34 @@ public class TagController(ITagService tagService) : ControllerBase
var updatedTag = await tagService.Update(tag, dto.Type); var updatedTag = await tagService.Update(tag, dto.Type);
return Ok(updatedTag); return Ok(updatedTag);
} }
/// <summary>
/// Get specific tag, specific post comment.
/// </summary>
/// <param name="tagName">Tag name</param>
/// <param name="postId">Post ID</param>
/// <param name="commentId">Comment ID</param>
/// <response code="200">Comment data</response>
/// <response code="400">If request is malformed</response>
/// <response code="404">If tag or post or comment is not found</response>
[HttpGet("{tagName}/posts/{postId:int}/comments/{commentId:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CommentDto>> 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));
}
} }

View File

@@ -1,4 +1,6 @@
using System.ComponentModel.DataAnnotations;
namespace T120B165_ImgBoard.Dtos.Comment; namespace T120B165_ImgBoard.Dtos.Comment;
public record CreateCommentDto(string Text); public record CreateCommentDto([Required] string Text);
public record EditCommentDto(string Text); public record EditCommentDto([Required] string Text);

View File

@@ -1,3 +1,5 @@
using System.ComponentModel.DataAnnotations;
namespace T120B165_ImgBoard.Dtos; namespace T120B165_ImgBoard.Dtos;
public record LoginDto(string Email, string Password); public record LoginDto([Required] string Email, [Required] string Password);

View File

@@ -3,8 +3,11 @@ using System.ComponentModel.DataAnnotations;
namespace T120B165_ImgBoard.Dtos; namespace T120B165_ImgBoard.Dtos;
public record RegisterDto( public record RegisterDto(
[Required]
string UserName, string UserName,
[EmailAddress] [EmailAddress]
[Required]
string Email, string Email,
[Required]
string Password string Password
); );

View File

@@ -2,6 +2,8 @@ using System.Net;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
@@ -22,8 +24,34 @@ public class Program
builder.Services.AddAuthorization(); builder.Services.AddAuthorization();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddControllers().AddNewtonsoftJson( builder.Services.AddControllers()
options => options.SerializerSettings.Converters.Add(new StringEnumConverter())); .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 => builder.Services.AddOpenApiDocument(cfg =>
{ {
cfg.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth")); cfg.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth"));