Update documentation and gate tag creation behind admin role, create default admin on database seeding
This commit is contained in:
23
Bruno/Endpoints/Auth/Login as admin.bru
Normal file
23
Bruno/Endpoints/Auth/Login as admin.bru
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ meta {
|
|||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/api/auth/login
|
url: {{baseUrl}}/api/auth/login
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
meta {
|
meta {
|
||||||
name: Refresh token
|
name: Refresh token
|
||||||
type: http
|
type: http
|
||||||
seq: 3
|
seq: 4
|
||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ meta {
|
|||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/api/auth/register
|
url: {{baseUrl}}/api/auth/register
|
||||||
body: json
|
body: json
|
||||||
auth: inherit
|
auth: none
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ meta {
|
|||||||
post {
|
post {
|
||||||
url: {{baseUrl}}/api/posts
|
url: {{baseUrl}}/api/posts
|
||||||
body: json
|
body: json
|
||||||
auth: bearer
|
auth: inherit
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{accessToken}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
|
|||||||
@@ -7,9 +7,5 @@ meta {
|
|||||||
delete {
|
delete {
|
||||||
url: {{baseUrl}}/api/posts/1
|
url: {{baseUrl}}/api/posts/1
|
||||||
body: none
|
body: none
|
||||||
auth: bearer
|
auth: inherit
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{accessToken}}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,7 @@ meta {
|
|||||||
patch {
|
patch {
|
||||||
url: {{baseUrl}}/api/posts/1
|
url: {{baseUrl}}/api/posts/1
|
||||||
body: json
|
body: json
|
||||||
auth: bearer
|
auth: inherit
|
||||||
}
|
|
||||||
|
|
||||||
auth:bearer {
|
|
||||||
token: {{accessToken}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body:json {
|
body:json {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ meta {
|
|||||||
|
|
||||||
patch {
|
patch {
|
||||||
url: {{baseUrl}}/api/tags/:name
|
url: {{baseUrl}}/api/tags/:name
|
||||||
body: none
|
body: json
|
||||||
auth: inherit
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,3 +17,9 @@ params:query {
|
|||||||
params:path {
|
params:path {
|
||||||
name: high-res
|
name: high-res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"type": "Copyright"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ meta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
auth {
|
auth {
|
||||||
mode: none
|
mode: bearer
|
||||||
|
}
|
||||||
|
|
||||||
|
auth:bearer {
|
||||||
|
token: {{accessToken}}
|
||||||
}
|
}
|
||||||
|
|
||||||
vars:pre-request {
|
vars:pre-request {
|
||||||
|
|||||||
@@ -12,8 +12,16 @@ namespace T120B165_ImgBoard.Controllers;
|
|||||||
public class AuthController(UserManager<User> userManager, ITokenService tokenService): ControllerBase
|
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")]
|
[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
|
var user = new User
|
||||||
{
|
{
|
||||||
@@ -27,9 +35,15 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
|
|||||||
{
|
{
|
||||||
return BadRequest(result.Errors);
|
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")]
|
[HttpPost("login")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
@@ -44,6 +58,12 @@ public class AuthController(UserManager<User> userManager, ITokenService tokenSe
|
|||||||
return Ok(new TokenDto(AccessToken: accessToken, RefreshToken: refreshToken));
|
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")]
|
[HttpPost("refresh")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using T120B165_ImgBoard.Dtos;
|
using T120B165_ImgBoard.Dtos;
|
||||||
using T120B165_ImgBoard.Dtos.Tag;
|
using T120B165_ImgBoard.Dtos.Tag;
|
||||||
@@ -11,9 +12,21 @@ namespace T120B165_ImgBoard.Controllers;
|
|||||||
[Route("api/tags")]
|
[Route("api/tags")]
|
||||||
public class TagController(ITagService tagService) : ControllerBase
|
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]
|
[HttpPost]
|
||||||
|
[Authorize(Roles = UserRoles.Admin)]
|
||||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||||
public async Task<ActionResult<Tag>> Create(CreateTagDto dto)
|
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);
|
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]
|
[HttpGet]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
@@ -39,6 +58,13 @@ public class TagController(ITagService tagService) : ControllerBase
|
|||||||
return Ok(await tagService.GetAll(pageNumber));
|
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}")]
|
[HttpGet("{name}")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
@@ -53,9 +79,21 @@ public class TagController(ITagService tagService) : ControllerBase
|
|||||||
return Ok(tag);
|
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}")]
|
[HttpDelete("{name}")]
|
||||||
|
[Authorize(Roles = UserRoles.Admin)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult> Delete(string name)
|
public async Task<ActionResult> Delete(string name)
|
||||||
{
|
{
|
||||||
@@ -64,9 +102,22 @@ public class TagController(ITagService tagService) : ControllerBase
|
|||||||
return NoContent();
|
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}")]
|
[HttpPatch("{name}")]
|
||||||
|
[Authorize(Roles = UserRoles.Admin)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<Tag>> Update(string name, EditTagDto dto)
|
public async Task<ActionResult<Tag>> Update(string name, EditTagDto dto)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ namespace T120B165_ImgBoard.Data;
|
|||||||
|
|
||||||
public static class DbInitializer
|
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>>();
|
var roleManager = serviceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
||||||
string[] roleNames = [UserRoles.Admin, UserRoles.Regular];
|
string[] roleNames = [UserRoles.Admin, UserRoles.Regular];
|
||||||
foreach (var roleName in roleNames)
|
foreach (var roleName in roleNames)
|
||||||
@@ -16,5 +17,16 @@ public static class DbInitializer
|
|||||||
await roleManager.CreateAsync(new IdentityRole(roleName));
|
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.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,4 @@ public class RefreshToken
|
|||||||
public DateTime Expires { get; set; }
|
public DateTime Expires { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class User : IdentityUser
|
public class User : IdentityUser;
|
||||||
{
|
|
||||||
//public List<Post> Posts { get; set; }
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
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.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.IdentityModel.Tokens;
|
using Microsoft.IdentityModel.Tokens;
|
||||||
using Newtonsoft.Json.Converters;
|
using Newtonsoft.Json.Converters;
|
||||||
|
using NSwag;
|
||||||
|
using NSwag.Generation.Processors.Security;
|
||||||
using T120B165_ImgBoard.Data;
|
using T120B165_ImgBoard.Data;
|
||||||
using T120B165_ImgBoard.Models;
|
using T120B165_ImgBoard.Models;
|
||||||
using T120B165_ImgBoard.Services;
|
using T120B165_ImgBoard.Services;
|
||||||
@@ -21,8 +24,17 @@ public class Program
|
|||||||
// 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().AddNewtonsoftJson(
|
||||||
options => options.SerializerSettings.Converters.Add(new StringEnumConverter()));
|
options => options.SerializerSettings.Converters.Add(new StringEnumConverter()));
|
||||||
builder.Services.AddOpenApiDocument();
|
builder.Services.AddOpenApiDocument(cfg =>
|
||||||
builder.Services.AddOpenApi();
|
{
|
||||||
|
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>()
|
builder.Services.AddIdentity<User, IdentityRole>()
|
||||||
@@ -84,7 +96,7 @@ public class Program
|
|||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
if (app.Environment.IsDevelopment())
|
if (app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.MapOpenApi();
|
//app.MapOpenApi();
|
||||||
app.UseOpenApi();
|
app.UseOpenApi();
|
||||||
app.UseSwaggerUi();
|
app.UseSwaggerUi();
|
||||||
}
|
}
|
||||||
@@ -95,11 +107,12 @@ public class Program
|
|||||||
|
|
||||||
var context = services.GetRequiredService<ImgBoardContext>();
|
var context = services.GetRequiredService<ImgBoardContext>();
|
||||||
context.Database.EnsureCreated();
|
context.Database.EnsureCreated();
|
||||||
DbInitializer.SeedRolesAsync(services).GetAwaiter().GetResult();
|
DbInitializer.SeedAuth(services).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
|
|
||||||
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using T120B165_ImgBoard.Data;
|
using T120B165_ImgBoard.Data;
|
||||||
using T120B165_ImgBoard.Dtos;
|
|
||||||
using T120B165_ImgBoard.Models;
|
using T120B165_ImgBoard.Models;
|
||||||
using T120B165_ImgBoard.Utils;
|
using T120B165_ImgBoard.Utils;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>T120B165_ImgBoard</RootNamespace>
|
<RootNamespace>T120B165_ImgBoard</RootNamespace>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -22,8 +22,10 @@
|
|||||||
MARIADB_USER: imgboard
|
MARIADB_USER: imgboard
|
||||||
MARIADB_PASSWORD: supersecretpassword
|
MARIADB_PASSWORD: supersecretpassword
|
||||||
volumes:
|
volumes:
|
||||||
- ./db_data:/var/lib/mysql
|
- db_data:/var/lib/mysql
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
app_data:
|
app_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
db_data:
|
||||||
|
driver: local
|
||||||
|
|||||||
Reference in New Issue
Block a user