using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.Options; using Microsoft.OpenApi; using Musify.Api.Authentication; namespace Musify.Api.OpenApi; public sealed class OpenApiOptionsSetup(IOptions options) : IConfigureNamedOptions { internal 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; }); options.AddSchemaTransformer((schema, context, _) => { // Every long the API returns is a user id, serialized as a string (JS loses precision above 2^53). // Request DTOs only carry byte sizes, which may be sent as numbers or strings. if (context.JsonPropertyInfo is { } property && (property.PropertyType == typeof(long) || property.PropertyType == typeof(long?))) { var isRequest = property.DeclaringType?.Name.EndsWith("Request", StringComparison.Ordinal) == true; schema.Type = isRequest ? JsonSchemaType.Integer | JsonSchemaType.String : JsonSchemaType.String; if (property.PropertyType == typeof(long?)) schema.Type |= JsonSchemaType.Null; schema.Format = isRequest ? "int64" : null; } return Task.CompletedTask; }); options.AddOperationTransformer((operation, context, _) => { // Route parameters constrained with :long are user ids, which travel as strings like every other long. foreach (var parameter in operation.Parameters?.OfType() ?? []) { if (parameter.Schema is OpenApiSchema { Type: JsonSchemaType.Integer, Format: "int64" } schema) { schema.Type = JsonSchemaType.String; schema.Format = null; } } 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; }); } }