From cfc975dade428bfa56630c390639f425659d9238 Mon Sep 17 00:00:00 2001 From: Stuart Ferguson Date: Mon, 3 Nov 2025 11:28:15 +0000 Subject: [PATCH] more useful shared functions --- .../Authorisation/AuthorizationExtensions.cs | 78 +++++++++++++++++++ .../EndpointConventionBuilderExtensions.cs | 21 ++++- Shared/General/SnakeCaseNamingPolicy.cs | 22 ++++++ 3 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 Shared/Authorisation/AuthorizationExtensions.cs create mode 100644 Shared/General/SnakeCaseNamingPolicy.cs diff --git a/Shared/Authorisation/AuthorizationExtensions.cs b/Shared/Authorisation/AuthorizationExtensions.cs new file mode 100644 index 00000000..ba0b8009 --- /dev/null +++ b/Shared/Authorisation/AuthorizationExtensions.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; + +namespace Shared.Authorisation +{ + public static class AuthorizationExtensions { + public static IServiceCollection AddClientCredentialsHandler(this IServiceCollection services) { + services.AddSingleton(); + return services; + } + + public static IServiceCollection AddClientCredentialsOnlyPolicy(this IServiceCollection services) { + services.AddAuthorization(options => { + options.AddPolicy("ClientCredentialsOnly", policy => + policy.Requirements.Add(new ClientCredentialsRequirement())); + }); + + return services; + } + + public static IServiceCollection AddPasswordTokenHandler(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } + + public static IServiceCollection AddPasswordTokenPolicy(this IServiceCollection services) + { + services.AddAuthorization(options => + { + options.AddPolicy("PasswordToken", policy => + policy.Requirements.Add(new PasswordTokenRequirement())); + }); + + return services; + } + + + private sealed class PasswordTokenRequirement : IAuthorizationRequirement; + + private sealed class ClientCredentialsRequirement : IAuthorizationRequirement; + + private sealed class PasswordTokenHandler : AuthorizationHandler { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + PasswordTokenRequirement requirement) { + // Your logic from IsPasswordToken() + Claim? userIdClaim = context.User.Claims.SingleOrDefault(c => c.Type == ClaimTypes.NameIdentifier); + + if (userIdClaim != null) { + context.Succeed(requirement); + } + + return Task.CompletedTask; + } + } + + private sealed class ClientCredentialsHandler : AuthorizationHandler { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, + ClientCredentialsRequirement requirement) { + // A client credentials token *does not* include a NameIdentifier (user) claim. + bool isUserToken = context.User.HasClaim(c => c.Type == ClaimTypes.NameIdentifier); + + if (!isUserToken) { + context.Succeed(requirement); // Valid client credentials token + } + + return Task.CompletedTask; + } + } + + } +} diff --git a/Shared/Extensions/EndpointConventionBuilderExtensions.cs b/Shared/Extensions/EndpointConventionBuilderExtensions.cs index 687fa6d4..dc1e4de1 100644 --- a/Shared/Extensions/EndpointConventionBuilderExtensions.cs +++ b/Shared/Extensions/EndpointConventionBuilderExtensions.cs @@ -1,13 +1,28 @@ -using Microsoft.AspNetCore.Builder; +using System; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; namespace Shared.Extensions; public static class EndpointConventionBuilderExtensions { - public static RouteHandlerBuilder WithStandardProduces(this RouteHandlerBuilder builder) + public static RouteHandlerBuilder WithStandardProduces(this RouteHandlerBuilder builder, Int32 successCode = StatusCodes.Status200OK) { - builder.Produces(StatusCodes.Status200OK) + builder.Produces(successCode) + .Produces(StatusCodes.Status400BadRequest) + .Produces(StatusCodes.Status404NotFound) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status409Conflict) + .Produces(StatusCodes.Status500InternalServerError) + .Produces(StatusCodes.Status403Forbidden) + .Produces(StatusCodes.Status501NotImplemented); + + return builder; + } + + public static RouteHandlerBuilder WithStandardProduces(this RouteHandlerBuilder builder, Int32 successCode = StatusCodes.Status200OK) + { + builder.Produces(successCode) .Produces(StatusCodes.Status400BadRequest) .Produces(StatusCodes.Status404NotFound) .Produces(StatusCodes.Status401Unauthorized) diff --git a/Shared/General/SnakeCaseNamingPolicy.cs b/Shared/General/SnakeCaseNamingPolicy.cs new file mode 100644 index 00000000..6441f626 --- /dev/null +++ b/Shared/General/SnakeCaseNamingPolicy.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Shared.General +{ + public class SnakeCaseNamingPolicy : JsonNamingPolicy + { + public override string ConvertName(string name) + { + // simple PascalCase to snake_case + return string.Concat( + name.Select((c, i) => + i > 0 && char.IsUpper(c) ? "_" + char.ToLower(c) : char.ToLower(c).ToString() + ) + ); + } + } +}