Update documentation and gate tag creation behind admin role, create default admin on database seeding

This commit is contained in:
2025-10-06 16:35:55 +03:00
parent 25dfb97a5b
commit 8d012f04f1
17 changed files with 149 additions and 33 deletions

View File

@@ -12,8 +12,16 @@ namespace T120B165_ImgBoard.Controllers;
public class AuthController(UserManager<User> userManager, ITokenService tokenService): ControllerBase
{
/// <summary>
/// Creates a new user account.
/// </summary>
/// <param name="dto">Registration data</param>
/// <response code="200">Returns user data</response>
/// <response code="400">If user supplied credentials fail validation</response>
[HttpPost("register")]
public async Task<ActionResult<User>> Register(RegisterDto dto)
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<SlimUserDto>> Register(RegisterDto dto)
{
var user = new User
{
@@ -27,9 +35,15 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
{
return BadRequest(result.Errors);
}
return Ok(user);
return Ok(SlimUserDto.FromUser(user));
}
/// <summary>
/// Authenticates and creates a pair of access and refresh tokens.
/// </summary>
/// <param name="dto">Data with refresh token</param>
/// <response code="200">Returns refresh and access tokens</response>
/// <response code="401">If the credentials are incorrect</response>
[HttpPost("login")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
@@ -44,6 +58,12 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: refreshToken));
}
/// <summary>
/// Consume refresh token to create new access and refresh tokens.
/// </summary>
/// <param name="dto">Data with refresh token</param>
/// <response code="200">Returns new refresh and access tokens</response>
/// <response code="401">If refresh token is missing or is expired</response>
[HttpPost("refresh")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]

View File

@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using T120B165_ImgBoard.Dtos;
using T120B165_ImgBoard.Dtos.Tag;
@@ -11,9 +12,21 @@ namespace T120B165_ImgBoard.Controllers;
[Route("api/tags")]
public class TagController(ITagService tagService) : ControllerBase
{
/// <summary>
/// Creates a new tag.
/// </summary>
/// <param name="dto">New tag data.</param>
/// <response code="201">Returns the newly created tag</response>
/// <response code="400">If request is malformed</response>
/// <response code="401">If authentication is missing</response>
/// <response code="403">If authorization is missing</response>
/// <response code="409">If tag already exists with such name</response>
[HttpPost]
[Authorize(Roles = UserRoles.Admin)]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public async Task<ActionResult<Tag>> Create(CreateTagDto dto)
{
@@ -31,6 +44,12 @@ public class TagController(ITagService tagService) : ControllerBase
return CreatedAtAction(nameof(Get), new { name = createdTag.Name }, createdTag);
}
/// <summary>
/// Get a paginated list of tags.
/// </summary>
/// <param name="pageNumber">The page number</param>
/// <response code="200">Returns paginated list</response>
/// <response code="400">If request is malformed</response>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -39,6 +58,13 @@ public class TagController(ITagService tagService) : ControllerBase
return Ok(await tagService.GetAll(pageNumber));
}
/// <summary>
/// Get specific tag by name.
/// </summary>
/// <param name="name">The tag name</param>
/// <response code="200">The tag data</response>
/// <response code="400">If request is malformed</response>
/// <response code="404">If tag does not exist</response>
[HttpGet("{name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
@@ -53,9 +79,21 @@ public class TagController(ITagService tagService) : ControllerBase
return Ok(tag);
}
/// <summary>
/// Delete specified tag.
/// </summary>
/// <param name="name">The tag name</param>
/// <response code="204">Indicates tag deletion success</response>
/// <response code="400">If request is malformed</response>
/// <response code="401">If authentication is missing</response>
/// <response code="403">If authorization is missing</response>
/// <response code="404">If tag does not exist</response>
[HttpDelete("{name}")]
[Authorize(Roles = UserRoles.Admin)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> Delete(string name)
{
@@ -64,9 +102,22 @@ public class TagController(ITagService tagService) : ControllerBase
return NoContent();
}
/// <summary>
/// Update specified tag.
/// </summary>
/// <param name="name">The tag name</param>
/// <param name="dto">The new tag data</param>
/// <response code="200">Indicates tag update success</response>
/// <response code="400">If request is malformed</response>
/// <response code="401">If authentication is missing</response>
/// <response code="403">If authorization is missing</response>
/// <response code="404">If tag does not exist</response>
[HttpPatch("{name}")]
[Authorize(Roles = UserRoles.Admin)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Tag>> Update(string name, EditTagDto dto)
{

View File

@@ -5,8 +5,9 @@ namespace T120B165_ImgBoard.Data;
public static class DbInitializer
{
public static async Task SeedRolesAsync(IServiceProvider serviceProvider)
public static async Task SeedAuth(IServiceProvider serviceProvider)
{
Console.WriteLine("Seeding Auth...");
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
string[] roleNames = [UserRoles.Admin, UserRoles.Regular];
foreach (var roleName in roleNames)
@@ -16,5 +17,16 @@ public static class DbInitializer
await roleManager.CreateAsync(new IdentityRole(roleName));
}
}
var userManager = serviceProvider.GetRequiredService<UserManager<User>>();
var adminUser = new User
{
UserName = "admin",
Email = "admin@localhost",
};
await userManager.CreateAsync(adminUser, "ChangeMe123#");
await userManager.AddToRoleAsync(adminUser, UserRoles.Regular);
await userManager.AddToRoleAsync(adminUser, UserRoles.Admin);
Console.WriteLine("Auth seeding complete.");
}
}

View File

@@ -17,7 +17,4 @@ public class RefreshToken
public DateTime Expires { get; set; }
}
public class User : IdentityUser
{
//public List<Post> Posts { get; set; }
};
public class User : IdentityUser;

View File

@@ -1,9 +1,12 @@
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Converters;
using NSwag;
using NSwag.Generation.Processors.Security;
using T120B165_ImgBoard.Data;
using T120B165_ImgBoard.Models;
using T120B165_ImgBoard.Services;
@@ -21,8 +24,17 @@ public class Program
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddControllers().AddNewtonsoftJson(
options => options.SerializerSettings.Converters.Add(new StringEnumConverter()));
builder.Services.AddOpenApiDocument();
builder.Services.AddOpenApi();
builder.Services.AddOpenApiDocument(cfg =>
{
cfg.OperationProcessors.Add(new OperationSecurityScopeProcessor("auth"));
cfg.DocumentProcessors.Add(new SecurityDefinitionAppender("auth", new OpenApiSecurityScheme
{
Type = OpenApiSecuritySchemeType.Http,
In = OpenApiSecurityApiKeyLocation.Header,
Scheme = "bearer",
BearerFormat = "jwt"
}));
});
builder.Services.AddIdentity<User, IdentityRole>()
@@ -84,7 +96,7 @@ public class Program
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
//app.MapOpenApi();
app.UseOpenApi();
app.UseSwaggerUi();
}
@@ -95,11 +107,12 @@ public class Program
var context = services.GetRequiredService<ImgBoardContext>();
context.Database.EnsureCreated();
DbInitializer.SeedRolesAsync(services).GetAwaiter().GetResult();
DbInitializer.SeedAuth(services).GetAwaiter().GetResult();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

View File

@@ -1,6 +1,5 @@
using Microsoft.EntityFrameworkCore;
using T120B165_ImgBoard.Data;
using T120B165_ImgBoard.Dtos;
using T120B165_ImgBoard.Models;
using T120B165_ImgBoard.Utils;

View File

@@ -6,6 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>T120B165_ImgBoard</RootNamespace>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>