From 8d012f04f1cb0dfb9be7cce7a61e2cfebb7b2e90 Mon Sep 17 00:00:00 2001 From: JustAnyone Date: Mon, 6 Oct 2025 16:35:55 +0300 Subject: [PATCH] Update documentation and gate tag creation behind admin role, create default admin on database seeding --- Bruno/Endpoints/Auth/Login as admin.bru | 23 +++++++++ Bruno/Endpoints/Auth/Login as normal.bru | 2 +- Bruno/Endpoints/Auth/Refresh token.bru | 2 +- Bruno/Endpoints/Auth/Register normal user.bru | 2 +- Bruno/Endpoints/Posts/Create a post.bru | 6 +-- Bruno/Endpoints/Posts/Delete post 1.bru | 6 +-- Bruno/Endpoints/Posts/Update post 1.bru | 6 +-- Bruno/Endpoints/Tags/Update a tag.bru | 8 ++- Bruno/collection.bru | 6 ++- .../Controllers/AuthController.cs | 24 ++++++++- .../Controllers/TagController.cs | 51 +++++++++++++++++++ T120B165-ImgBoard/Data/DbInitializer.cs | 14 ++++- T120B165-ImgBoard/Models/User.cs | 5 +- T120B165-ImgBoard/Program.cs | 21 ++++++-- T120B165-ImgBoard/Services/TagService.cs | 1 - T120B165-ImgBoard/T120B165-ImgBoard.csproj | 1 + compose.yaml | 4 +- 17 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 Bruno/Endpoints/Auth/Login as admin.bru diff --git a/Bruno/Endpoints/Auth/Login as admin.bru b/Bruno/Endpoints/Auth/Login as admin.bru new file mode 100644 index 0000000..041838d --- /dev/null +++ b/Bruno/Endpoints/Auth/Login as admin.bru @@ -0,0 +1,23 @@ +meta { + name: Login as admin + type: http + seq: 3 +} + +post { + url: {{baseUrl}}/api/auth/login + body: json + auth: none +} + +body:json { + { + "email": "admin@localhost", + "password": "ChangeMe123#" + } +} + +vars:post-response { + accessToken: res.body.accessToken + refreshToken: res.body.refreshToken +} diff --git a/Bruno/Endpoints/Auth/Login as normal.bru b/Bruno/Endpoints/Auth/Login as normal.bru index bd20487..3ba4854 100644 --- a/Bruno/Endpoints/Auth/Login as normal.bru +++ b/Bruno/Endpoints/Auth/Login as normal.bru @@ -7,7 +7,7 @@ meta { post { url: {{baseUrl}}/api/auth/login body: json - auth: inherit + auth: none } body:json { diff --git a/Bruno/Endpoints/Auth/Refresh token.bru b/Bruno/Endpoints/Auth/Refresh token.bru index d6a39a3..056fda4 100644 --- a/Bruno/Endpoints/Auth/Refresh token.bru +++ b/Bruno/Endpoints/Auth/Refresh token.bru @@ -1,7 +1,7 @@ meta { name: Refresh token type: http - seq: 3 + seq: 4 } post { diff --git a/Bruno/Endpoints/Auth/Register normal user.bru b/Bruno/Endpoints/Auth/Register normal user.bru index 60e1870..b9c3061 100644 --- a/Bruno/Endpoints/Auth/Register normal user.bru +++ b/Bruno/Endpoints/Auth/Register normal user.bru @@ -7,7 +7,7 @@ meta { post { url: {{baseUrl}}/api/auth/register body: json - auth: inherit + auth: none } body:json { diff --git a/Bruno/Endpoints/Posts/Create a post.bru b/Bruno/Endpoints/Posts/Create a post.bru index 26d93cf..82c217b 100644 --- a/Bruno/Endpoints/Posts/Create a post.bru +++ b/Bruno/Endpoints/Posts/Create a post.bru @@ -7,11 +7,7 @@ meta { post { url: {{baseUrl}}/api/posts body: json - auth: bearer -} - -auth:bearer { - token: {{accessToken}} + auth: inherit } body:json { diff --git a/Bruno/Endpoints/Posts/Delete post 1.bru b/Bruno/Endpoints/Posts/Delete post 1.bru index 36f15e9..6b5183d 100644 --- a/Bruno/Endpoints/Posts/Delete post 1.bru +++ b/Bruno/Endpoints/Posts/Delete post 1.bru @@ -7,9 +7,5 @@ meta { delete { url: {{baseUrl}}/api/posts/1 body: none - auth: bearer -} - -auth:bearer { - token: {{accessToken}} + auth: inherit } diff --git a/Bruno/Endpoints/Posts/Update post 1.bru b/Bruno/Endpoints/Posts/Update post 1.bru index c451fe5..023b3ef 100644 --- a/Bruno/Endpoints/Posts/Update post 1.bru +++ b/Bruno/Endpoints/Posts/Update post 1.bru @@ -7,11 +7,7 @@ meta { patch { url: {{baseUrl}}/api/posts/1 body: json - auth: bearer -} - -auth:bearer { - token: {{accessToken}} + auth: inherit } body:json { diff --git a/Bruno/Endpoints/Tags/Update a tag.bru b/Bruno/Endpoints/Tags/Update a tag.bru index 96351f7..5c8f9e7 100644 --- a/Bruno/Endpoints/Tags/Update a tag.bru +++ b/Bruno/Endpoints/Tags/Update a tag.bru @@ -6,7 +6,7 @@ meta { patch { url: {{baseUrl}}/api/tags/:name - body: none + body: json auth: inherit } @@ -17,3 +17,9 @@ params:query { params:path { name: high-res } + +body:json { + { + "type": "Copyright" + } +} diff --git a/Bruno/collection.bru b/Bruno/collection.bru index f7f3c81..867cec3 100644 --- a/Bruno/collection.bru +++ b/Bruno/collection.bru @@ -3,7 +3,11 @@ meta { } auth { - mode: none + mode: bearer +} + +auth:bearer { + token: {{accessToken}} } vars:pre-request { diff --git a/T120B165-ImgBoard/Controllers/AuthController.cs b/T120B165-ImgBoard/Controllers/AuthController.cs index 09d3b64..3200b2b 100644 --- a/T120B165-ImgBoard/Controllers/AuthController.cs +++ b/T120B165-ImgBoard/Controllers/AuthController.cs @@ -12,8 +12,16 @@ namespace T120B165_ImgBoard.Controllers; public class AuthController(UserManager userManager, ITokenService tokenService): ControllerBase { + /// + /// Creates a new user account. + /// + /// Registration data + /// Returns user data + /// If user supplied credentials fail validation [HttpPost("register")] - public async Task> Register(RegisterDto dto) + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Register(RegisterDto dto) { var user = new User { @@ -27,9 +35,15 @@ public class AuthController(UserManager userManager, ITokenService tokenSe { return BadRequest(result.Errors); } - return Ok(user); + return Ok(SlimUserDto.FromUser(user)); } + /// + /// Authenticates and creates a pair of access and refresh tokens. + /// + /// Data with refresh token + /// Returns refresh and access tokens + /// If the credentials are incorrect [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -44,6 +58,12 @@ public class AuthController(UserManager userManager, ITokenService tokenSe return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: refreshToken)); } + /// + /// Consume refresh token to create new access and refresh tokens. + /// + /// Data with refresh token + /// Returns new refresh and access tokens + /// If refresh token is missing or is expired [HttpPost("refresh")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] diff --git a/T120B165-ImgBoard/Controllers/TagController.cs b/T120B165-ImgBoard/Controllers/TagController.cs index ca0457a..f7683ba 100644 --- a/T120B165-ImgBoard/Controllers/TagController.cs +++ b/T120B165-ImgBoard/Controllers/TagController.cs @@ -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 { + /// + /// Creates a new tag. + /// + /// New tag data. + /// Returns the newly created tag + /// If request is malformed + /// If authentication is missing + /// If authorization is missing + /// If tag already exists with such name [HttpPost] + [Authorize(Roles = UserRoles.Admin)] [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task> Create(CreateTagDto dto) { @@ -31,6 +44,12 @@ public class TagController(ITagService tagService) : ControllerBase return CreatedAtAction(nameof(Get), new { name = createdTag.Name }, createdTag); } + /// + /// Get a paginated list of tags. + /// + /// The page number + /// Returns paginated list + /// If request is malformed [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -39,6 +58,13 @@ public class TagController(ITagService tagService) : ControllerBase return Ok(await tagService.GetAll(pageNumber)); } + /// + /// Get specific tag by name. + /// + /// The tag name + /// The tag data + /// If request is malformed + /// If tag does not exist [HttpGet("{name}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -53,9 +79,21 @@ public class TagController(ITagService tagService) : ControllerBase return Ok(tag); } + /// + /// Delete specified tag. + /// + /// The tag name + /// Indicates tag deletion success + /// If request is malformed + /// If authentication is missing + /// If authorization is missing + /// If tag does not exist [HttpDelete("{name}")] + [Authorize(Roles = UserRoles.Admin)] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task Delete(string name) { @@ -64,9 +102,22 @@ public class TagController(ITagService tagService) : ControllerBase return NoContent(); } + /// + /// Update specified tag. + /// + /// The tag name + /// The new tag data + /// Indicates tag update success + /// If request is malformed + /// If authentication is missing + /// If authorization is missing + /// If tag does not exist [HttpPatch("{name}")] + [Authorize(Roles = UserRoles.Admin)] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> Update(string name, EditTagDto dto) { diff --git a/T120B165-ImgBoard/Data/DbInitializer.cs b/T120B165-ImgBoard/Data/DbInitializer.cs index c414b26..da005f7 100644 --- a/T120B165-ImgBoard/Data/DbInitializer.cs +++ b/T120B165-ImgBoard/Data/DbInitializer.cs @@ -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>(); 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>(); + 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."); } } diff --git a/T120B165-ImgBoard/Models/User.cs b/T120B165-ImgBoard/Models/User.cs index 542f19c..7c136c8 100644 --- a/T120B165-ImgBoard/Models/User.cs +++ b/T120B165-ImgBoard/Models/User.cs @@ -17,7 +17,4 @@ public class RefreshToken public DateTime Expires { get; set; } } -public class User : IdentityUser -{ - //public List Posts { get; set; } -}; \ No newline at end of file +public class User : IdentityUser; diff --git a/T120B165-ImgBoard/Program.cs b/T120B165-ImgBoard/Program.cs index 0276aac..ebf0596 100644 --- a/T120B165-ImgBoard/Program.cs +++ b/T120B165-ImgBoard/Program.cs @@ -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() @@ -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(); context.Database.EnsureCreated(); - DbInitializer.SeedRolesAsync(services).GetAwaiter().GetResult(); + DbInitializer.SeedAuth(services).GetAwaiter().GetResult(); } app.UseHttpsRedirection(); + app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); diff --git a/T120B165-ImgBoard/Services/TagService.cs b/T120B165-ImgBoard/Services/TagService.cs index 2cfd507..b424b59 100644 --- a/T120B165-ImgBoard/Services/TagService.cs +++ b/T120B165-ImgBoard/Services/TagService.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using T120B165_ImgBoard.Data; -using T120B165_ImgBoard.Dtos; using T120B165_ImgBoard.Models; using T120B165_ImgBoard.Utils; diff --git a/T120B165-ImgBoard/T120B165-ImgBoard.csproj b/T120B165-ImgBoard/T120B165-ImgBoard.csproj index 4ffa873..fbc6a2e 100644 --- a/T120B165-ImgBoard/T120B165-ImgBoard.csproj +++ b/T120B165-ImgBoard/T120B165-ImgBoard.csproj @@ -6,6 +6,7 @@ enable T120B165_ImgBoard Linux + true diff --git a/compose.yaml b/compose.yaml index b344d5b..c90ce41 100644 --- a/compose.yaml +++ b/compose.yaml @@ -22,8 +22,10 @@ MARIADB_USER: imgboard MARIADB_PASSWORD: supersecretpassword volumes: - - ./db_data:/var/lib/mysql + - db_data:/var/lib/mysql volumes: app_data: driver: local + db_data: + driver: local