mirror of
https://github.com/azaion/admin.git
synced 2026-04-22 03:46:34 +00:00
[AZ-199] [AZ-200] [AZ-201] [AZ-202] Fix API bugs
Made-with: Cursor
This commit is contained in:
@@ -10,19 +10,36 @@ public class BusinessExceptionHandler(ILogger<BusinessExceptionHandler> logger)
|
||||
{
|
||||
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
|
||||
{
|
||||
if (exception is not BusinessException ex)
|
||||
return false;
|
||||
|
||||
logger.LogWarning(exception, ex.Message);
|
||||
httpContext.Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
|
||||
var err = JsonConvert.SerializeObject(new
|
||||
if (exception is BusinessException ex)
|
||||
{
|
||||
ErrorCode = ex.ExceptionEnum,
|
||||
ex.Message
|
||||
});
|
||||
await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
logger.LogWarning(exception, ex.Message);
|
||||
httpContext.Response.StatusCode = StatusCodes.Status409Conflict;
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
|
||||
var err = JsonConvert.SerializeObject(new
|
||||
{
|
||||
ErrorCode = ex.ExceptionEnum,
|
||||
ex.Message
|
||||
});
|
||||
await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (exception is BadHttpRequestException badReq)
|
||||
{
|
||||
logger.LogWarning(exception, badReq.Message);
|
||||
httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||
httpContext.Response.ContentType = "application/json";
|
||||
|
||||
var err = JsonConvert.SerializeObject(new
|
||||
{
|
||||
ErrorCode = 0,
|
||||
badReq.Message
|
||||
});
|
||||
await httpContext.Response.WriteAsync(err, cancellationToken).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,9 @@ Log.Logger = new LoggerConfiguration()
|
||||
.CreateLogger();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 209715200); //increase upload limit up to 200mb
|
||||
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 209715200);
|
||||
builder.Services.Configure<Microsoft.AspNetCore.Http.Features.FormOptions>(o =>
|
||||
o.MultipartBodyLengthLimit = 209715200);
|
||||
|
||||
var jwtConfig = builder.Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>();
|
||||
if (jwtConfig == null || string.IsNullOrEmpty(jwtConfig.Secret))
|
||||
@@ -139,8 +141,15 @@ app.MapPost("/login",
|
||||
.WithSummary("Login");
|
||||
|
||||
app.MapPost("/users",
|
||||
async (RegisterUserRequest registerUserRequest, IUserService userService, CancellationToken cancellationToken)
|
||||
=> await userService.RegisterUser(registerUserRequest, cancellationToken))
|
||||
async (RegisterUserRequest registerUserRequest, IValidator<RegisterUserRequest> validator,
|
||||
IUserService userService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var validation = await validator.ValidateAsync(registerUserRequest, cancellationToken);
|
||||
if (!validation.IsValid)
|
||||
return Results.ValidationProblem(validation.ToDictionary());
|
||||
await userService.RegisterUser(registerUserRequest, cancellationToken);
|
||||
return Results.Ok();
|
||||
})
|
||||
.RequireAuthorization(apiAdminPolicy)
|
||||
.WithSummary("Creates a new user");
|
||||
|
||||
@@ -188,8 +197,12 @@ app.MapDelete("/users/{email}", async (string email, IUserService userService, C
|
||||
.WithSummary("Remove user");
|
||||
|
||||
app.MapPost("/resources/{dataFolder?}",
|
||||
async ([FromRoute]string? dataFolder, IFormFile data, IResourcesService resourceService, CancellationToken ct)
|
||||
=> await resourceService.SaveResource(dataFolder, data, ct))
|
||||
async ([FromRoute]string? dataFolder, IFormFile? data, IResourcesService resourceService, CancellationToken ct) =>
|
||||
{
|
||||
if (data is null)
|
||||
throw new BusinessException(ExceptionEnum.NoFileProvided);
|
||||
await resourceService.SaveResource(dataFolder, data, ct);
|
||||
})
|
||||
.Accepts<IFormFile>("multipart/form-data")
|
||||
.RequireAuthorization()
|
||||
.WithSummary("Upload resource")
|
||||
|
||||
@@ -28,7 +28,7 @@ public enum ExceptionEnum
|
||||
[Description("Passwords do not match.")]
|
||||
WrongPassword = 30,
|
||||
|
||||
[Description("Password should be at least 8 characters.")]
|
||||
[Description("Password should be at least 12 characters.")]
|
||||
PasswordLengthIncorrect = 32,
|
||||
|
||||
[Description("Email is empty or invalid.")]
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
namespace Azaion.Common.Entities;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Azaion.Common.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; } = null!;
|
||||
[JsonIgnore]
|
||||
public string PasswordHash { get; set; } = null!;
|
||||
public string? Hardware { get; set; }
|
||||
public RoleEnum Role { get; set; }
|
||||
|
||||
@@ -19,5 +19,5 @@ public class RegisterUserValidator : AbstractValidator<RegisterUserRequest>
|
||||
.EmailAddress().WithErrorCode(ExceptionEnum.WrongEmail.ToString()).WithMessage("Email address is not valid.");
|
||||
|
||||
RuleFor(r => r.Password)
|
||||
.MinimumLength(8).WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()).WithMessage("Password should be at least 8 characters.");
|
||||
.MinimumLength(12).WithErrorCode(ExceptionEnum.PasswordLengthIncorrect.ToString()).WithMessage("Password should be at least 12 characters.");
|
||||
} }
|
||||
@@ -165,7 +165,7 @@ public sealed class ResilienceTests
|
||||
p95.Should().BeLessThan(500);
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: MultipartBodyLengthLimit defaults to 128MB while Kestrel MaxRequestBodySize is 200MB — FormOptions not configured")]
|
||||
[Fact]
|
||||
[Trait("Category", "ResourceLimit")]
|
||||
public async Task Max_file_upload_200_mb_accepted()
|
||||
{
|
||||
|
||||
@@ -119,7 +119,7 @@ public sealed class ResourceTests
|
||||
var folder = $"restest-{Guid.NewGuid():N}";
|
||||
const string fileName = "roundtrip.bin";
|
||||
var original = Enumerable.Range(0, 128).Select(i => (byte)i).ToArray();
|
||||
const string password = "RoundTrip1!";
|
||||
const string password = "RoundTrip123";
|
||||
const string hardware = "RT-HW-CPU-001-GPU-002";
|
||||
string? email = null;
|
||||
|
||||
@@ -174,7 +174,7 @@ public sealed class ResourceTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: missing file upload returns 500 instead of 400/409 — unhandled BadHttpRequestException")]
|
||||
[Fact]
|
||||
public async Task Upload_without_file_is_rejected_with_400_or_409_and_60_on_conflict()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
@@ -76,7 +76,7 @@ public sealed class SecurityTests
|
||||
|
||||
// Act & Assert
|
||||
using (var r = await client.PostAsync("/users",
|
||||
new { email = targetEmail, password = "TestPwd1234", role = 10 }))
|
||||
new { email = targetEmail, password = "TestPwd12345", role = 10 }))
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
|
||||
using (var r = await client.GetAsync("/users"))
|
||||
@@ -89,7 +89,7 @@ public sealed class SecurityTests
|
||||
r.StatusCode.Should().Be(HttpStatusCode.Forbidden);
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: GET /users exposes passwordHash field with actual hash values")]
|
||||
[Fact]
|
||||
public async Task Users_list_must_not_expose_non_empty_password_hash_in_json()
|
||||
{
|
||||
// Arrange
|
||||
@@ -150,7 +150,7 @@ public sealed class SecurityTests
|
||||
var payload = Encoding.UTF8.GetBytes($"secret-{Guid.NewGuid()}");
|
||||
var email1 = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
var email2 = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
const string password = "TestPwd1234";
|
||||
const string password = "TestPwd12345";
|
||||
var hw1 = $"hw-{Guid.NewGuid():N}";
|
||||
var hw2 = $"hw-{Guid.NewGuid():N}";
|
||||
|
||||
@@ -207,7 +207,7 @@ public sealed class SecurityTests
|
||||
{
|
||||
// Arrange
|
||||
var email = $"{Guid.NewGuid():N}@sectest.example.com";
|
||||
const string password = "TestPwd1234";
|
||||
const string password = "TestPwd12345";
|
||||
try
|
||||
{
|
||||
var reg = JsonSerializer.Serialize(new { email, password, role = 10 }, JsonOptions);
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class UserManagementTests
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
var body = new { email, password = "SecurePass1", role = 10 };
|
||||
var body = new { email, password = "SecurePass1!", role = 10 };
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users", body);
|
||||
@@ -87,7 +87,7 @@ public sealed class UserManagementTests
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1!", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public sealed class UserManagementTests
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1!", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public sealed class UserManagementTests
|
||||
try
|
||||
{
|
||||
// Arrange
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1", role = 10 }))
|
||||
using (var createResp = await client.PostAsync("/users", new { email, password = "SecurePass1!", role = 10 }))
|
||||
{
|
||||
createResp.StatusCode.Should().BeOneOf(HttpStatusCode.OK, HttpStatusCode.NoContent);
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public sealed class UserManagementTests
|
||||
}
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: no email length validation — returns 200 instead of 400")]
|
||||
[Fact]
|
||||
public async Task Registration_rejects_short_email_with_400()
|
||||
{
|
||||
// Arrange
|
||||
@@ -168,13 +168,13 @@ public sealed class UserManagementTests
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = "ab@c.de", password = "ValidPass1", role = 10 });
|
||||
new { email = "ab@c.de", password = "ValidPass123", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: no email format validation — returns 200 instead of 400")]
|
||||
[Fact]
|
||||
public async Task Registration_rejects_invalid_email_format_with_400()
|
||||
{
|
||||
// Arrange
|
||||
@@ -182,13 +182,13 @@ public sealed class UserManagementTests
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = "notavalidemail", password = "ValidPass1", role = 10 });
|
||||
new { email = "notavalidemail", password = "ValidPass123", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
[Fact(Skip = "API bug: no password length validation — returns 200 instead of 400")]
|
||||
[Fact]
|
||||
public async Task Registration_rejects_short_password_with_400()
|
||||
{
|
||||
// Arrange
|
||||
@@ -210,7 +210,7 @@ public sealed class UserManagementTests
|
||||
|
||||
// Act
|
||||
using var response = await client.PostAsync("/users",
|
||||
new { email = _fixture.AdminEmail, password = "DuplicateP1", role = 10 });
|
||||
new { email = _fixture.AdminEmail, password = "DuplicateP1!", role = 10 });
|
||||
|
||||
// Assert
|
||||
response.StatusCode.Should().Be(HttpStatusCode.Conflict);
|
||||
|
||||
Reference in New Issue
Block a user