using LiteCharms.Features.Abstractions; using LiteCharms.Features.Api; using LiteCharms.Features.Api.Configuration; using Microsoft.AspNetCore.Authentication.JwtBearer; namespace LiteCharms.Features.Extensions; public static class Api { public const string Books = nameof(Books); public const string Payments = nameof(Payments); public static IServiceCollection AddLiteCharmsUiSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); var authOptions = new LiteCharmsSettings(); 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.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.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always; options.CorrelationCookie.SameSite = SameSiteMode.None; options.CorrelationCookie.HttpOnly = true; options.NonceCookie.SecurePolicy = CookieSecurePolicy.Always; options.NonceCookie.SameSite = SameSiteMode.None; options.NonceCookie.HttpOnly = true; }); return services; } public static IServiceCollection AddLiteCharmsApiSecurity(this IServiceCollection services, IConfiguration configuration) { var configSection = configuration.GetSection(nameof(LiteCharmsSettings)); var authOptions = new LiteCharmsSettings(); configSection.Bind(authOptions); services.Configure(configSection); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = authOptions.Authority; options.Audience = authOptions.Audience; options.TokenValidationParameters = new TokenValidationParameters { ValidIssuer = authOptions.Authority, ValidateAudience = true, ValidateIssuer = true, }; }); 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); }