using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.Options; using Microsoft.OpenApi; using Musify.Api.Authentication; using Musify.Application.Serialization; namespace Musify.Api.OpenApi; public sealed class OpenApiOptionsSetup(IOptions options) : IConfigureNamedOptions { private const string SecuritySchemeId = "OAuth2"; private readonly AuthenticationConfiguration _configuration = options.Value; public void Configure(string? name, OpenApiOptions options) => Configure(options); public void Configure(OpenApiOptions options) { options.AddDocumentTransformer((document, _, _) => { document.Components ??= new OpenApiComponents(); document.Components.SecuritySchemes ??= new Dictionary(); document.Components.SecuritySchemes[SecuritySchemeId] = new OpenApiSecurityScheme { Type = SecuritySchemeType.OAuth2, Flows = new OpenApiOAuthFlows { AuthorizationCode = new OpenApiOAuthFlow { AuthorizationUrl = new Uri(_configuration.AuthorizationEndpoint), TokenUrl = new Uri(_configuration.TokenEndpoint), Scopes = _configuration.Scopes.ToDictionary(scope => scope, scope => scope), }, }, }; return Task.CompletedTask; } ); // Properties annotated with [JsonConverter(typeof(LongAsStringConverter))] (id-shaped // `long` fields — see that converter for why) serialize as a JSON string, but the // schema generator still reflects the declared CLR `long`/`long?` type as a bare // int64 number. Reconcile the declared schema so generated clients (openapi-typescript) // type exactly those properties as `string`, matching what actually goes over the wire — // scoped to the attribute, not the CLR type, so unrelated `long` fields (byte sizes, …) // are left alone. options.AddSchemaTransformer((schema, context, _) => { if (context.JsonPropertyInfo?.CustomConverter is LongAsStringConverter or NullableLongAsStringConverter) { schema.Type = JsonSchemaType.String; schema.Format = null; } return Task.CompletedTask; } ); options.AddOperationTransformer((operation, context, _) => { var requiresAuthorization = context.Description.ActionDescriptor.EndpointMetadata .OfType() .Any(); var allowsAnonymous = context.Description.ActionDescriptor.EndpointMetadata .OfType() .Any(); if (!requiresAuthorization || allowsAnonymous) { return Task.CompletedTask; } operation.Security ??= new List(); operation.Security.Add(new OpenApiSecurityRequirement { [new OpenApiSecuritySchemeReference(SecuritySchemeId, context.Document)] = _configuration.Scopes.ToList(), } ); return Task.CompletedTask; } ); } }