Add validation to RealmCreate
This commit is contained in:
parent
09480eb1e4
commit
ddbb1f42d7
16 changed files with 326 additions and 23 deletions
|
|
@ -1,3 +1,5 @@
|
|||
using FluentResults;
|
||||
using IdentityShroud.Api.Validation;
|
||||
using IdentityShroud.Core.Messages;
|
||||
using IdentityShroud.Core.Messages.Realm;
|
||||
using IdentityShroud.Core.Services;
|
||||
|
|
@ -6,23 +8,36 @@ using Microsoft.AspNetCore.Mvc;
|
|||
|
||||
namespace IdentityShroud.Api;
|
||||
|
||||
public static class RealmController
|
||||
public static class RealmApi
|
||||
{
|
||||
public static void MapRealmEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var realm = app.MapGroup("/realms/{slug}");
|
||||
var realmsGroup = app.MapGroup("/realms");
|
||||
realmsGroup.MapPost("", RealmCreate)
|
||||
.Validate<RealmCreateRequest>()
|
||||
.WithName("Create Realm")
|
||||
.Produces(StatusCodes.Status201Created);
|
||||
|
||||
realm.MapGet("", GetRoot);
|
||||
realm.MapPost("", (RealmCreateRequest request, [FromServices] RealmService service) =>
|
||||
service.Create(request))
|
||||
.WithName("Create Realm");
|
||||
realm.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
||||
var realmSlugGroup = app.MapGroup("{slug}");
|
||||
realmSlugGroup.MapGet("", GetRealmInfo);
|
||||
realmSlugGroup.MapGet(".well-known/openid-configuration", GetOpenIdConfiguration);
|
||||
|
||||
var openidConnect = realm.MapGroup("openid-connect");
|
||||
var openidConnect = realmSlugGroup.MapGroup("openid-connect");
|
||||
openidConnect.MapPost("auth", OpenIdConnectAuth);
|
||||
openidConnect.MapPost("token", OpenIdConnectToken);
|
||||
openidConnect.MapGet("jwks", OpenIdConnectJwks);
|
||||
}
|
||||
|
||||
private static async Task<Results<Created<RealmCreateResponse>, InternalServerError>>
|
||||
RealmCreate(RealmCreateRequest request, [FromServices] IRealmService service)
|
||||
{
|
||||
var response = await service.Create(request);
|
||||
if (response.IsSuccess)
|
||||
return TypedResults.Created($"/realms/{response.Value.Slug}", response.Value);
|
||||
|
||||
// TODO make helper to convert failure response to a proper HTTP result.
|
||||
return TypedResults.InternalServerError();
|
||||
}
|
||||
|
||||
private static Task OpenIdConnectJwks(HttpContext context)
|
||||
{
|
||||
|
|
@ -57,7 +72,7 @@ public static class RealmController
|
|||
}, AppJsonSerializerContext.Default.OpenIdConfiguration);
|
||||
}
|
||||
|
||||
private static string GetRoot()
|
||||
private static string GetRealmInfo()
|
||||
{
|
||||
return "Hello World!";
|
||||
|
||||
|
|
@ -10,7 +10,12 @@
|
|||
<UserSecretsId>6b8ef434-0577-4a3c-8749-6b547d7787c5</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PreserveCompilationContext>true</PreserveCompilationContext>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
|
||||
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
using FluentValidation;
|
||||
using IdentityShroud.Api;
|
||||
using IdentityShroud.Api.Validation;
|
||||
using IdentityShroud.Core;
|
||||
using IdentityShroud.Core.Contracts;
|
||||
using IdentityShroud.Core.Security;
|
||||
using Serilog;
|
||||
using Serilog.Formatting.Json;
|
||||
|
||||
|
||||
// Initial logging until we can set it up from Configuration
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.Enrich.FromLogContext()
|
||||
|
|
@ -34,6 +37,8 @@ void ConfigureBuilder(WebApplicationBuilder builder)
|
|||
services.AddOptions<DbConfiguration>().Bind(configuration.GetSection("db"));
|
||||
services.AddSingleton<ISecretProvider, ConfigurationSecretProvider>();
|
||||
|
||||
services.AddValidatorsFromAssemblyContaining<RealmCreateRequestValidator>();
|
||||
|
||||
builder.Host.UseSerilog((context, services, configuration) => configuration
|
||||
.Enrich.FromLogContext()
|
||||
//.Enrich.With<UserEnricher>()
|
||||
|
|
@ -51,3 +56,5 @@ void ConfigureApplication(WebApplication app)
|
|||
// app.UseRouting();
|
||||
// app.MapControllers();
|
||||
}
|
||||
|
||||
public partial class Program { }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
namespace IdentityShroud.Api.Validation;
|
||||
|
||||
public static class EndpointRouteBuilderExtensions
|
||||
{
|
||||
public static RouteHandlerBuilder Validate<TDto>(this RouteHandlerBuilder builder) where TDto : class
|
||||
=> builder.AddEndpointFilter<ValidateFilter<TDto>>();
|
||||
}
|
||||
19
IdentityShroud.Api/Validation/RealmCreateRequestValidator.cs
Normal file
19
IdentityShroud.Api/Validation/RealmCreateRequestValidator.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using FluentValidation;
|
||||
using IdentityShroud.Core.Messages.Realm;
|
||||
|
||||
namespace IdentityShroud.Api.Validation;
|
||||
|
||||
public class RealmCreateRequestValidator : AbstractValidator<RealmCreateRequest>
|
||||
{
|
||||
private const string SlugPattern = @"^(?=.{1,40}$)[a-z0-9]+(?:-[a-z0-9]+)*$";
|
||||
|
||||
public RealmCreateRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Id)
|
||||
.NotEqual(Guid.Empty).When(x => x.Id.HasValue);
|
||||
RuleFor(x => x.Slug)
|
||||
.Matches(SlugPattern).Unless(x => x.Slug is null);
|
||||
RuleFor(x => x.Name)
|
||||
.NotNull().Length(1, 255);
|
||||
}
|
||||
}
|
||||
33
IdentityShroud.Api/Validation/ValidateFilter.cs
Normal file
33
IdentityShroud.Api/Validation/ValidateFilter.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using FluentValidation;
|
||||
|
||||
namespace IdentityShroud.Api.Validation;
|
||||
|
||||
public class ValidateFilter<T> : IEndpointFilter where T : class
|
||||
{
|
||||
public async ValueTask<object?> InvokeAsync(
|
||||
EndpointFilterInvocationContext context,
|
||||
EndpointFilterDelegate next)
|
||||
{
|
||||
// Grab the deserialized argument (the DTO) from the context.
|
||||
// The order of arguments matches the order of parameters in the endpoint delegate.
|
||||
var dto = context.Arguments
|
||||
.FirstOrDefault(arg => arg is T) as T;
|
||||
|
||||
if (dto == null)
|
||||
return Results.BadRequest("Unable to read request body.");
|
||||
|
||||
// Resolve the matching validator from DI.
|
||||
var validator = context.HttpContext.RequestServices
|
||||
.GetService<IValidator<T>>();
|
||||
|
||||
if (validator != null)
|
||||
{
|
||||
var validationResult = await validator.ValidateAsync(dto);
|
||||
if (!validationResult.IsValid)
|
||||
return Results.ValidationProblem(validationResult.ToDictionary());
|
||||
}
|
||||
|
||||
// Validation passed – continue to the actual handler.
|
||||
return await next(context);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue