using LiteCharms.Features.Abstractions; using LiteCharms.Features.Api; using LiteCharms.Features.Api.Configuration; namespace LiteCharms.Features.Extensions; public static class Api { public const string Books = nameof(Books); public const string Payments = nameof(Payments); public static IServiceCollection AddAuthentikUiSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(AuthentikSettings)); var authOptions = new AuthentikSettings(); configSection.Bind(authOptions); services.Configure(configSection); services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Authority = authOptions.Authority; options.MetadataAddress = authOptions.MetadataEndpoint; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; options.SignedOutCallbackPath = "/signout-callback-oidc"; options.ResponseType = "code"; options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); options.Events = new OpenIdConnectEvents { OnRedirectToIdentityProvider = context => { var fallbackUri = context.ProtocolMessage.RedirectUri; if (fallbackUri.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) context.ProtocolMessage.RedirectUri = fallbackUri.Replace("http://", "https://", StringComparison.OrdinalIgnoreCase); return Task.CompletedTask; } }; }); return services; } public static IServiceCollection AddAuthentikApiSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(AuthentikSettings)); var authOptions = new AuthentikSettings(); configSection.Bind(authOptions); services.Configure(configSection); services.AddAuthentication(OAuth2IntrospectionDefaults.AuthenticationScheme) .AddOAuth2Introspection(OAuth2IntrospectionDefaults.AuthenticationScheme, options => { options.Authority = authOptions.Authority; options.IntrospectionEndpoint = authOptions.IntrospectionEndpoint; options.ClientId = authOptions.ClientId; options.ClientSecret = authOptions.ClientSecret; options.NameClaimType = "sub"; options.DiscoveryPolicy.RequireHttps = authOptions.RequireHttpsMetadata; options.DiscoveryPolicy.ValidateEndpoints = false; options.EnableCaching = false; }); if (!string.IsNullOrWhiteSpace(authOptions.RequiredClaimName) && !string.IsNullOrWhiteSpace(authOptions.RequiredClaimNameValue)) { services.AddAuthorizationBuilder() .AddPolicy("RequiredScope", policy => policy.RequireClaim(authOptions.RequiredClaimName, authOptions.RequiredClaimNameValue)); } else services.AddAuthorization(); return services; } public static WebApplication AddSecurityEndpoints(this WebApplication app) { app.MapGet("/login", async (HttpContext context, string redirectUri = "/") => { await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = redirectUri, }); }); app.MapGet("/logout", async (HttpContext context, IHttpClientFactory httpClientFactory, IOptions settings) => { await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); string currentBaseUrl = $"https://{context.Request.Host}{context.Request.PathBase}/"; await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = currentBaseUrl }); }); return app; } public static IServiceCollection AddApiServices(this IServiceCollection services, IConfiguration configuration) { services.AddHttpClient(); services.AddApiVersioning(options => { options.DefaultApiVersion = new ApiVersion(1); options.ReportApiVersions = true; options.AssumeDefaultVersionWhenUnspecified = true; options.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), new QueryStringApiVersionReader("version"), new QueryStringApiVersionReader("version"), new MediaTypeApiVersionReader("version")); }) .AddApiExplorer(options => { options.GroupNameFormat = "'v'VVV"; options.SubstituteApiVersionInUrl = true; }); var urls = configuration["ASPNETCORE_URLS"] ?? configuration["Urls"]; var healthUrl = "http://localhost:8080/health"; if (!string.IsNullOrWhiteSpace(urls)) { string firstUrl = urls.Split(';').FirstOrDefault(s => s.Contains("http://"))! .Replace("0.0.0.0", "localhost") .Replace("*", "localhost") .Replace("+", "localhost"); healthUrl = $"{firstUrl.TrimEnd('/')}/health"; } services.AddHealthChecksUI(setup => { setup.SetNotifyUnHealthyOneTimeUntilChange(); setup.AddHealthCheckEndpoint("primary, heal", healthUrl); setup.SetHeaderText("Midrand Books"); }) .AddInMemoryStorage(); services.AddOutputCache(options => { options.AddBasePolicy(builder => builder.Cache()); options.DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); }); services.AddOpenApi(options => options.AddDocumentTransformer()); return services; } public static IApplicationBuilder MapEndpoints(this WebApplication app, IDictionary versionGroups) { var endpoints = app.Services.GetRequiredService>(); foreach (var endpoint in endpoints) { var versionAttributes = endpoint.GetType().GetCustomAttributes().ToList(); if (versionAttributes.Count != 0) { foreach (var attr in versionAttributes) if (versionGroups.TryGetValue(attr.MajorVersion, out var targetGroup)) endpoint.Map(targetGroup); } else endpoint.Map(app); } return app; } public static IServiceCollection AddEndpoints(this IServiceCollection services, Assembly assembly) { ServiceDescriptor[] discriptors = [.. assembly.DefinedTypes .Where(t => t is { IsInterface: false, IsAbstract: false }) .Where(t => t.IsAssignableTo(typeof(IEndpoint))) .Select(t => ServiceDescriptor.Transient(typeof(IEndpoint), t))]; services.TryAddEnumerable(discriptors); return services; } public static string ToEndpointName(this Type target, string? annotation = "") => $"{target.Name.Replace("Endpoint", string.Empty)}{annotation}".ToLower(CultureInfo.CurrentCulture); }