Add project files
This commit is contained in:
62
T120B165-ImgBoard/Controllers/AuthController.cs
Normal file
62
T120B165-ImgBoard/Controllers/AuthController.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
|
||||
[ApiController]
|
||||
[Route("api/auth")]
|
||||
public class AuthController(UserManager<User> userManager, ITokenService tokenService): ControllerBase
|
||||
{
|
||||
|
||||
[HttpPost("register")]
|
||||
public async Task<ActionResult<User>> Register(RegisterDto dto)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
UserName = dto.UserName,
|
||||
Email = dto.Email,
|
||||
};
|
||||
|
||||
var result = await userManager.CreateAsync(user, dto.Password);
|
||||
await userManager.AddToRoleAsync(user, UserRoles.Regular);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return BadRequest(result.Errors);
|
||||
}
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<TokenDto>> Login(LoginDto dto)
|
||||
{
|
||||
var user = await userManager.FindByEmailAsync(dto.Email);
|
||||
if (user == null || !await userManager.CheckPasswordAsync(user, dto.Password))
|
||||
return Unauthorized();
|
||||
|
||||
var accessToken = await tokenService.GenerateJwtToken(user);
|
||||
var refreshToken = await tokenService.GenerateRefreshToken(user);
|
||||
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: refreshToken));
|
||||
}
|
||||
|
||||
[HttpPost("refresh")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
public async Task<ActionResult<TokenDto>> Refresh(RefreshDto dto)
|
||||
{
|
||||
var token = await tokenService.GetRefreshTokenByValue(dto.RefreshToken);
|
||||
if (token == null) return Unauthorized();
|
||||
var user = token.User;
|
||||
|
||||
await tokenService.InvalidateRefreshToken(token);
|
||||
|
||||
var accessToken = await tokenService.GenerateJwtToken(user);
|
||||
var newRefreshToken = await tokenService.GenerateRefreshToken(user);
|
||||
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: newRefreshToken));
|
||||
}
|
||||
}
|
||||
456
T120B165-ImgBoard/Controllers/PostController.cs
Normal file
456
T120B165-ImgBoard/Controllers/PostController.cs
Normal file
@@ -0,0 +1,456 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using System.Text.RegularExpressions;
|
||||
using FileSignatures;
|
||||
using FileSignatures.Formats;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos.Comment;
|
||||
using T120B165_ImgBoard.Dtos.Post;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/posts")]
|
||||
public class PostController(
|
||||
IPostService postService,
|
||||
IUserService userService,
|
||||
ITagService tagService,
|
||||
ICommentService commentService,
|
||||
IFileService fileService,
|
||||
IWebHostEnvironment env
|
||||
): ControllerBase
|
||||
{
|
||||
|
||||
private static async Task<string> InspectFileContent(string filePath)
|
||||
{
|
||||
// Define the types we support
|
||||
var inspector = new FileFormatInspector(new List<FileFormat>
|
||||
{
|
||||
new Jpeg(),
|
||||
new Png(),
|
||||
});
|
||||
|
||||
// Open the file and inspect
|
||||
await using var stream = System.IO.File.OpenRead(filePath);
|
||||
var format = inspector.DetermineFileFormat(stream);
|
||||
|
||||
// If the file matches a known format, return its MIME type
|
||||
if (format != null) return format.MediaType;
|
||||
|
||||
// If no signature matches, assume generic binary type
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private string GetFinalFilePath(Models.File fileRecord)
|
||||
{
|
||||
var baseDir = Path.Combine(env.ContentRootPath, "Storage", "Attachments");
|
||||
|
||||
// Create a clean, organized subdirectory (e.g., /Storage/Attachments/2025/10/)
|
||||
var dateDir = Path.Combine(baseDir, DateTime.UtcNow.Year.ToString(), DateTime.UtcNow.Month.ToString("D2"));
|
||||
|
||||
// Ensure the directory exists before attempting to save the file
|
||||
if (!Directory.Exists(dateDir))
|
||||
{
|
||||
Directory.CreateDirectory(dateDir);
|
||||
}
|
||||
|
||||
// Create a unique file name to avoid collisions
|
||||
// Use the file ID or a GUID + a sanitized version of the original file name
|
||||
var uniqueFileName = $"{Guid.NewGuid()}-{Path.GetFileName(fileRecord.OriginalFileName)}";
|
||||
|
||||
return Path.Combine(dateDir, uniqueFileName);
|
||||
}
|
||||
|
||||
private static bool TryParseContentRange(string header, out long start, out long end, out long totalSize)
|
||||
{
|
||||
start = end = totalSize = 0;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(header))
|
||||
return false;
|
||||
|
||||
// We expect a format like "bytes 0-1048575/5242880"
|
||||
// Regex pattern to capture the parts
|
||||
var match = Regex.Match(header, @"bytes\s+(?<start>\d+)-(?<end>\d+)/(?<total>\d+)");
|
||||
|
||||
if (!match.Success)
|
||||
return false;
|
||||
|
||||
// Safely parse the captured groups
|
||||
if (!long.TryParse(match.Groups["start"].Value, out start) ||
|
||||
!long.TryParse(match.Groups["end"].Value, out end) ||
|
||||
!long.TryParse(match.Groups["total"].Value, out totalSize))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// The length of the chunk must match the difference between end and start
|
||||
// Note: 'end' is inclusive, so the number of bytes is (end - start + 1)
|
||||
return start >= 0 && end >= start && totalSize > 0 && end < totalSize;
|
||||
}
|
||||
|
||||
private async Task<ActionResult<List<Tag>>> TagNamesToTags(List<string> tagNames)
|
||||
{
|
||||
var tags = new List<Tag>();
|
||||
foreach (var tagName in tagNames)
|
||||
{
|
||||
var tag = await tagService.GetByName(tagName);
|
||||
if (tag == null)
|
||||
return Problem(
|
||||
detail: $"'{tagName}' is not a valid tag",
|
||||
statusCode: StatusCodes.Status400BadRequest
|
||||
);
|
||||
tags.Add(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PostDto>> Create(CreatePostDto dto)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var user = await userService.GetUserById(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var maybeTags = await TagNamesToTags(dto.Tags);
|
||||
List<Tag> tags;
|
||||
if (maybeTags.Value != null)
|
||||
{
|
||||
tags = maybeTags.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(maybeTags.Result != null);
|
||||
return maybeTags.Result;
|
||||
}
|
||||
var created = await postService.Create(
|
||||
dto.Title, dto.Description, tags, user,
|
||||
dto.FileName, dto.FileMimeType, dto.FileSize.Value
|
||||
);
|
||||
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = created.Post.Id, fileId = created.File.Id }, Request.Scheme
|
||||
);
|
||||
return CreatedAtAction(nameof(Get), new { id = created.Post.Id },
|
||||
PostDto.FromPost(created.Post, fileUrl));
|
||||
}
|
||||
|
||||
[HttpPatch("{postId:int}/files/{fileId:int}")]
|
||||
public async Task<IActionResult> PatchFileContent(int postId, int fileId)
|
||||
{
|
||||
var post = await postService.GetById(postId, includeUnfinished: true);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var fileRecord = await fileService.GetFileById(fileId);
|
||||
if (fileRecord == null) return NotFound();
|
||||
|
||||
if (fileRecord.FinishedDate != null)
|
||||
return Problem(statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: "File was already uploaded.");
|
||||
|
||||
|
||||
// Parse the Content-Range Header
|
||||
// Example: Content-Range: bytes 0-1048575/5242880
|
||||
var rangeHeader = Request.Headers.ContentRange.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(rangeHeader) || !TryParseContentRange(rangeHeader, out long start, out long end, out long totalSizeFromHeader))
|
||||
{
|
||||
return Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: "Missing or invalid Content-Range header."
|
||||
);
|
||||
}
|
||||
|
||||
var totalSize = fileRecord.Size;
|
||||
if (totalSizeFromHeader != totalSize)
|
||||
{
|
||||
return Problem(
|
||||
statusCode: StatusCodes.Status400BadRequest,
|
||||
detail: $"Total file size mismatch. Expected: {totalSize} bytes, Received: {totalSizeFromHeader} bytes."
|
||||
);
|
||||
}
|
||||
|
||||
// Append the chunk to the temporary file
|
||||
var tempFilePath = fileRecord.FilePath;
|
||||
await using (var stream = new FileStream(tempFilePath, FileMode.Append, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
|
||||
{
|
||||
stream.Seek(start, SeekOrigin.Begin);
|
||||
await Request.Body.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
// Check if the upload is complete
|
||||
// Return 202 Accepted for a successful intermediate chunk
|
||||
if (end + 1 != totalSize) return Accepted();
|
||||
|
||||
// Otherwise, all chunks received. Finalize the file.
|
||||
var finalFilePath = GetFinalFilePath(fileRecord);
|
||||
System.IO.File.Move(tempFilePath, finalFilePath, overwrite: true);
|
||||
|
||||
// Compare the actual type to the expected type
|
||||
var actualContentType = await InspectFileContent(finalFilePath);
|
||||
if (!string.Equals(actualContentType, fileRecord.ContentType, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the file's content doesn't match its declared MIME type, clean up
|
||||
System.IO.File.Delete(finalFilePath);
|
||||
await fileService.Delete(fileRecord);
|
||||
//await postService.Delete(post);
|
||||
|
||||
return Problem(statusCode: StatusCodes.Status415UnsupportedMediaType,
|
||||
detail: $"Uploaded file type '{actualContentType}' does not match declared type '{fileRecord.ContentType}'.");
|
||||
}
|
||||
|
||||
// Update the database record with the final path
|
||||
fileRecord.FilePath = finalFilePath;
|
||||
fileRecord.FinishedDate = DateTime.Now;
|
||||
await fileService.Update(fileRecord);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/files/{fileId:int}")]
|
||||
public async Task<IActionResult> GetFileContent(int postId, int fileId)
|
||||
{
|
||||
var post = await postService.GetById(postId, includeUnfinished: true);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var fileRecord = await fileService.GetFileById(fileId);
|
||||
if (fileRecord == null) return NotFound();
|
||||
|
||||
// Ensure the file has been successfully uploaded and finalized
|
||||
if (string.IsNullOrEmpty(fileRecord.FilePath) || fileRecord.FinishedDate == null)
|
||||
{
|
||||
return Problem(statusCode: StatusCodes.Status404NotFound,
|
||||
detail: "File content is not yet available or was corrupted.");
|
||||
}
|
||||
var fullPath = Path.Combine(env.ContentRootPath, fileRecord.FilePath);
|
||||
|
||||
// Check if the file actually exists
|
||||
if (!System.IO.File.Exists(fullPath))
|
||||
{
|
||||
return Problem(statusCode: StatusCodes.Status404NotFound,
|
||||
detail: "The physical file could not be found.");
|
||||
}
|
||||
|
||||
var stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
return File(stream, fileRecord.ContentType, fileRecord.OriginalFileName);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PostDto>> Get(int id)
|
||||
{
|
||||
var entry = await postService.GetById(id);
|
||||
if (entry == null) return NotFound();
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = entry.Id, fileId = entry.File.Id }, Request.Scheme
|
||||
);
|
||||
return Ok(PostDto.FromPost(entry, fileUrl));
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedList<PostDto>>> GetAll([Range(1, int.MaxValue)] int pageNumber = 1)
|
||||
{
|
||||
var list = await postService.GetAll(pageNumber);
|
||||
var newItems = list.Items.Select(i =>
|
||||
{
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = i.Id, fileId = i.File.Id }, Request.Scheme
|
||||
);
|
||||
return PostDto.FromPost(i, fileUrl);
|
||||
}).ToList();
|
||||
return Ok(new PagedList<PostDto>(newItems, list.CurrentPage, list.PageSize, list.TotalCount));
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Delete(int id)
|
||||
{
|
||||
var post = await postService.GetById(id);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != post.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
// Clean up the file record first
|
||||
var fullPath = Path.Combine(env.ContentRootPath, post.File.FilePath);
|
||||
if (System.IO.File.Exists(fullPath)) System.IO.File.Delete(fullPath);
|
||||
await fileService.Delete(post.File);
|
||||
|
||||
var deleted = await postService.Delete(post);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPatch("{id:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PostDto>> Update(int id, EditPostDto dto)
|
||||
{
|
||||
var post = await postService.GetById(id);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != post.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Title))
|
||||
post.Title = dto.Title;
|
||||
if (!string.IsNullOrEmpty(dto.Description))
|
||||
post.Description = dto.Description;
|
||||
if (dto.Tags != null)
|
||||
{
|
||||
var maybeTags = await TagNamesToTags(dto.Tags);
|
||||
if (maybeTags.Value != null)
|
||||
{
|
||||
post.Tags = maybeTags.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Assert(maybeTags.Result != null);
|
||||
return maybeTags.Result;
|
||||
}
|
||||
}
|
||||
|
||||
var updated = await postService.Update(post);
|
||||
var fileUrl = Url.Action(nameof(PatchFileContent), "Post",
|
||||
new { postId = updated.Id, fileId = updated.File.Id }, Request.Scheme
|
||||
);
|
||||
return Ok(PostDto.FromPost(updated, fileUrl));
|
||||
}
|
||||
|
||||
|
||||
[HttpPost("{postId:int}/comments")]
|
||||
[Authorize]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<PostDto>> CreateComment(int postId, CreateCommentDto dto)
|
||||
{
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var user = await userService.GetUserById(userId);
|
||||
if (user == null) return Unauthorized();
|
||||
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
|
||||
var created = await commentService.Create(dto.Text, user, post);
|
||||
return CreatedAtAction(nameof(GetComment), new {postId = postId, commentId = created.Id}, CommentDto.FromComment(created));
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CommentDto>> GetComment(int postId, int commentId)
|
||||
{
|
||||
var entry = await postService.GetById(postId);
|
||||
if (entry == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
return Ok(CommentDto.FromComment(comment));
|
||||
}
|
||||
|
||||
[HttpGet("{postId:int}/comments")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<PagedList<CommentDto>>> GetAllComments(
|
||||
int postId,
|
||||
[Range(1, int.MaxValue)] int pageNumber = 1
|
||||
)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var list = await commentService.GetAll(pageNumber);
|
||||
var newItems = list.Items.Select(CommentDto.FromComment).ToList();
|
||||
return Ok(new PagedList<CommentDto>(newItems, list.CurrentPage, list.PageSize, list.TotalCount));
|
||||
}
|
||||
|
||||
|
||||
[Authorize]
|
||||
[HttpDelete("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> DeleteComment(int postId, int commentId)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != comment.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
var deleted = await commentService.Delete(comment);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpPatch("{postId:int}/comments/{commentId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CommentDto>> Update(int postId, int commentId, EditCommentDto dto)
|
||||
{
|
||||
var post = await postService.GetById(postId);
|
||||
if (post == null) return NotFound();
|
||||
|
||||
var comment = await commentService.GetById(commentId);
|
||||
if (comment == null) return NotFound();
|
||||
|
||||
var userId = HttpContext.User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;
|
||||
var isAdmin = HttpContext.User.IsInRole(UserRoles.Admin);
|
||||
|
||||
// If neither the admin nor the resource owner
|
||||
if (!isAdmin && userId != comment.Author.Id)
|
||||
{
|
||||
Forbid();
|
||||
}
|
||||
|
||||
comment.Text = dto.Text;
|
||||
|
||||
var updated = await commentService.Update(comment);
|
||||
return Ok(CommentDto.FromComment(updated));
|
||||
}
|
||||
}
|
||||
78
T120B165-ImgBoard/Controllers/TagController.cs
Normal file
78
T120B165-ImgBoard/Controllers/TagController.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using T120B165_ImgBoard.Dtos;
|
||||
using T120B165_ImgBoard.Dtos.Tag;
|
||||
using T120B165_ImgBoard.Models;
|
||||
using T120B165_ImgBoard.Services;
|
||||
using T120B165_ImgBoard.Utils;
|
||||
|
||||
namespace T120B165_ImgBoard.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/tags")]
|
||||
public class TagController(ITagService tagService) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<Tag>> Create(CreateTagDto dto)
|
||||
{
|
||||
// Check if tag exists, if it does, throw a conflict
|
||||
var existingTag = await tagService.GetByName(dto.Name);
|
||||
if (existingTag != null)
|
||||
{
|
||||
return Problem(
|
||||
detail: "Tag with such name already exists.",
|
||||
statusCode: StatusCodes.Status409Conflict
|
||||
);
|
||||
}
|
||||
|
||||
var createdTag = await tagService.Create(dto.Type, dto.Name);
|
||||
return CreatedAtAction(nameof(Get), new { name = createdTag.Name }, createdTag);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public async Task<ActionResult<PagedList<Tag>>> GetAll(int pageNumber = 1)
|
||||
{
|
||||
return Ok(await tagService.GetAll(pageNumber));
|
||||
}
|
||||
|
||||
[HttpGet("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<Tag>> Get(string name)
|
||||
{
|
||||
var tag = await tagService.GetByName(name);
|
||||
if (tag == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
return Ok(tag);
|
||||
}
|
||||
|
||||
[HttpDelete("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult> Delete(string name)
|
||||
{
|
||||
var deleted = await tagService.DeleteByName(name);
|
||||
if (!deleted) return NotFound();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPatch("{name}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<Tag>> Update(string name, EditTagDto dto)
|
||||
{
|
||||
var tag = await tagService.GetByName(name);
|
||||
if (tag == null) return NotFound();
|
||||
var updatedTag = await tagService.Update(tag, dto.Type);
|
||||
return Ok(updatedTag);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user