using System.Globalization; using System.IdentityModel.Tokens.Jwt; using System.Reflection; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Google.Apis.Util.Store; using IdentityModel; using IdentityServer8; using IdentityServer8.EntityFramework.Entities; using IdentityServer8.EntityFramework.Services; using IdentityServer8.EntityFramework.Stores; using IdentityServer8.Stores; using IdentityServer8.Validation; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.OpenSsl; using Microsoft.IdentityModel.Tokens; using Org.BouncyCastle.Security; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Newtonsoft.Json; using Yavsc.Helpers; using Yavsc.Interface; using Yavsc.Interfaces; using Yavsc.Models; using Yavsc.Server.Helpers; using Yavsc.Services; using Yavsc.Services.Kyc; using Yavsc.Settings; using Yavsc.ViewModels.Auth; using static IdentityServer8.IdentityServerConstants; using IdentityServer8.Models; using IdentityServer8.EntityFramework.Mappers; namespace Yavsc.Extensions; public static class HostingExtensions { private const string InMemoryProviderName = "InMemory"; public static WebApplication ConfigureWebAppServices(this WebApplicationBuilder builder) { builder.Services.AddSwaggerGen(); IServiceCollection services = LoadConfiguration(builder); services.AddSession(); // TODO .AddServerSideSessionStore() // Add the system clock service _ = services.AddSingleton(); _ = services.AddSingleton(); _ = services.AddTransient(); AddIdentityDBAndStores(builder) .AddDefaultTokenProviders(); AddIdentityServer(builder); services.AddSignalR(o => { o.EnableDetailedErrors = true; }); services.AddMvc(config => { /* var policy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); config.Filters.Add(new AuthorizeFilter(policy)); */ config.Filters.Add(new ProducesAttribute("application/json")); // config.ModelBinders.Insert(0,new MyDateTimeModelBinder()); // config.ModelBinders.Insert(0,new MyDecimalModelBinder()); config.EnableEndpointRouting = true; }).AddFormatterMappings( config => config.SetMediaTypeMappingForFormat("text/pdf", new MediaTypeHeaderValue("text/pdf")) ).AddFormatterMappings( config => config.SetMediaTypeMappingForFormat("text/x-tex", new MediaTypeHeaderValue("text/x-tex")) ) .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => { options.ResourcesPath = "Resources"; }).AddDataAnnotationsLocalization(); services.AddTransient() .AddTransient(); services.TryAddSingleton(); services.AddTransient() .AddTransient() .AddTransient((sp) => new FileDataStore("googledatastore", false)) .AddTransient() .AddTransient() .AddTransient>(); // TODO for SMS: services.AddTransient(); _ = services.AddLocalization(options => { options.ResourcesPath = "Resources"; }); var dataDirConfig = builder.Configuration["Site:DataDir"] ?? "DataDir"; var dataDir = new DirectoryInfo(dataDirConfig); // Add session related services. services.AddDataProtection().PersistKeysToFileSystem(dataDir); AddYavscPolicies(services, builder.Configuration); services.AddScoped(); services.AddTransient(); services.AddAuthentication("Bearer") .AddYavscJwtBearer(builder.Configuration, configure: o => o.Audience = builder.Configuration.GetSection("Site")["ExternalUrl"]); services.AddTransient>(); services.AddTransient, RoleStore>(); services.Configure(builder.Configuration.GetSection("Kyc")); services.AddScoped(); return builder.Build(); } public static IdentityBuilder AddIdentityDBAndStores(this WebApplicationBuilder builder) { IServiceCollection services = builder.Services; var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); services.AddDbContext(options => { if (UsesInMemoryProvider(connectionString)) { options.UseInMemoryDatabase(connectionString); } else { options.UseNpgsql(connectionString, options => options.MigrationsAssembly(typeof(Program).Assembly)); } }); var identityBuilder = services.AddIdentity( options => { options.SignIn.RequireConfirmedAccount = builder.Environment.IsEnvironment( builder.Environment.EnvironmentName); options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.PreferredUserName; options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; } ) .AddEntityFrameworkStores(); services.AddScoped, UserClaimsPrincipalFactory>(); // Dev-only: Chromium rejects SameSite=None without Secure on http:// // (e.g. http://localhost:5000). The default Identity cookie policy // sets SameSite=None, which is invalid without Secure. Force Lax in // dev. In production (https://) the default SameSite=None is fine. if (builder.Environment.IsDevelopment()) { services.ConfigureApplicationCookie(options => { options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); services.ConfigureExternalCookie(options => { options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); } return identityBuilder; } private static void AddYavscPolicies(IServiceCollection services, IConfiguration configuration) { services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser() .RequireClaim("scope", "scope2"); }); options.AddPolicy("Performer", policy => { policy .RequireAuthenticatedUser() .RequireClaim(YavscConstants.RoleClaimType, new string[] { YavscConstants.PerformerGroupName, YavscConstants.AdminGroupName }) ; }); options.AddPolicy("AdministratorOnly", policy => { _ = policy .RequireAuthenticatedUser() .RequireClaim(YavscConstants.RoleClaimType, YavscConstants.AdminGroupName); }); options.AddPolicy("FrontOffice", policy => policy.RequireRole(YavscConstants.FrontOfficeGroupName)); // options.AddPolicy("EmployeeId", policy => policy.RequireClaim("EmployeeId", "123", "456")); // options.AddPolicy("BuildingEntry", policy => policy.Requirements.Add(new OfficeEntryRequirement())); options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser()); options.AddPolicy("TheAuthor", policy => policy.Requirements.Add(new EditPermission())); }); services.AddYavscCors(configuration); } public static IServiceCollection LoadConfiguration(this WebApplicationBuilder builder) { var siteSection = builder.Configuration.GetSection("Site"); var smtpSection = builder.Configuration.GetSection("Smtp"); var paypalSection = builder.Configuration.GetSection("Authentication:PayPal"); // OAuth2AppSettings var googleAuthSettings = builder.Configuration.GetSection("Authentication:Google"); LoadGoogleConfig(builder.Configuration); var services = builder.Services; _ = services.AddControllersWithViews() .AddNewtonsoftJson(); services.Configure(siteSection); services.Configure(smtpSection); services.Configure(paypalSection); services.Configure(googleAuthSettings); ConfigureRequestLocalization(services); return services; } private static void AddAuthentication(WebApplicationBuilder builder) { IServiceCollection services = builder.Services; IConfigurationRoot configurationRoot = builder.Configuration; string? googleClientId = configurationRoot["Authentication:Google:ClientId"]; string? googleClientSecret = configurationRoot["Authentication:Google:ClientSecret"]; var authenticationBuilder = services.AddAuthentication(); if (googleClientId != null && googleClientSecret != null) authenticationBuilder.AddGoogle(options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; // register your IdentityServer with Google at https://console.developers.google.com // enable the Google+ API // set the redirect URI to https://localhost:5001/signin-google options.ClientId = googleClientId; options.ClientSecret = googleClientSecret; }); } public static IIdentityServerBuilder AddIdentityServer(WebApplicationBuilder builder) { builder.Services.Configure(options => { options.ClaimsIdentity.UserIdClaimType = JwtClaimTypes.Subject; options.ClaimsIdentity.UserNameClaimType = JwtClaimTypes.Name; options.ClaimsIdentity.RoleClaimType = YavscConstants.RoleClaimType; }); var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; var connectionString = builder.Configuration.GetConnectionString(YavscConstants.YavscConnectionStringName); string sqliteConnectionString = $"Data Source={Path.Combine(Path.GetTempPath(), "yavsc_test.db")}"; var identityServerBuilder = builder.Services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; // see https://IdentityServer8.readthedocs.io/en/latest/topics/resources.html options.EmitStaticAudienceClaim = true; }) .AddAspNetIdentity() .AddClientStore() .AddClientConfigurationValidator() .AddCorsPolicyService() .AddResourceStore() .AddConfigurationStore(options => { options.ConfigureDbContext = b => { if (UsesInMemoryProvider(connectionString)) { b.UseInMemoryDatabase(connectionString); } else { b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); } b.UseSeeding(EnsureDefaultConfiguration(builder.Configuration)); }; }) .AddOperationalStore(options => { options.ConfigureDbContext = b => { if (UsesInMemoryProvider(connectionString)) { b.UseInMemoryDatabase(connectionString); } else { b.UseNpgsql(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); } }; }); // Skip the production signing-cert requirement when running with // an in-memory database (test fixtures) or in the Development // environment. In those cases IdentityServer8 falls back to // AddDeveloperSigningCredential which mints an ephemeral key // at startup; signing real tokens against it would fail, but // the test fixtures only use the discovery/JWKS endpoints. var useDevSigning = builder.Environment.IsDevelopment() || UsesInMemoryProvider(connectionString); if (useDevSigning) { identityServerBuilder.AddDeveloperSigningCredential(); } else { // Production: reuse the Let's Encrypt certificate that Kestrel // already loads for TLS so IdentityServer has a stable signing // key (and a JWKS endpoint). The cert is renewed by the ACME // hook and a service restart picks up the new key automatically. // // The path comes from Kestrel:Endpoints:Https:Certificate to // avoid maintaining a separate setting; fullchain.pem bundles // the leaf + chain, which X509Certificate2 needs for chain // validation by relying parties. var certPath = builder.Configuration["Kestrel:Endpoints:Https:Certificate:Path"]; var keyPath = builder.Configuration["Kestrel:Endpoints:Https:Certificate:KeyPath"]; if (string.IsNullOrWhiteSpace(certPath) || string.IsNullOrWhiteSpace(keyPath)) { throw new InvalidOperationException( "Production IdentityServer requires a signing certificate. " + "Configure Kestrel:Endpoints:Https:Certificate:{Path,KeyPath}."); } // Load the leaf cert and extract its private key for signing. // The previous attempts (X509Certificate2.CreateFromPemFile, // the 3-arg ctor with X509KeyStorageFlags, and BC + CopyWithPrivateKey) // all loaded the cert successfully, but CreateJwkDocumentAsync // still raised NullReferenceException on the first GET /jwks // request: IdentityServer8's key material service reads the // private key off the X509Certificate2 at runtime, and on Linux // the key handle is not retained across that boundary. // // The reliable pattern is to pass a SigningCredentials that // wraps a SecurityKey built directly from the BC-parsed key // parameters. The SecurityKey is a managed object whose Key // property is a live AsymmetricAlgorithm, which survives every // read IdentityServer does (token signing, JWKS publish). var signingCredentials = LoadSigningCredentials(certPath, keyPath); identityServerBuilder.AddSigningCredential(signingCredentials); } // Note: IdentityServer8 does NOT expose the JWKS at /.well-known/jwks. // The default jwks_uri is /.well-known/openid-configuration/jwks, // which is what DiscoveryKeyEndpoint serves. Earlier revisions of // this file tried to override CustomEntries["jwks_uri"], but // IdentityServer8 reserves that key and rejects the override with // "Discovery custom entry jwks_uri cannot be added, because it // already exists." The default endpoint works once the signing // credential's private key is attached (see above). return identityServerBuilder; } /// /// Load the signing credentials (algorithm + private key) from the /// configured PEM files. Returns a /// whose Key is a managed built /// directly from the BouncyCastle-parsed key parameters — which keeps /// the private key alive for every read IdentityServer8 does (token /// signing, JWKS publish), unlike X509Certificate2.CopyWithPrivateKey /// which loses the handle on Linux when IdentityServer8's key material /// service reads it back at runtime. /// private static SigningCredentials LoadSigningCredentials(string certPath, string keyPath) { // Pre-flight read so permission / missing-file errors surface with // the actual path instead of being wrapped as an opaque // InvalidOperationException by the cert / key parsers. try { return LoadSigningCredentialsInner(certPath, keyPath); } catch (Exception ex) { Console.Error.WriteLine( $"[yavsc] Failed to load signing credentials from {certPath} / {keyPath}:"); Console.Error.WriteLine(ex.ToString()); throw new InvalidOperationException( $"Failed to load signing credentials from {certPath} / {keyPath}. " + "See stderr for the underlying managed exception (likely a " + "PEM format mismatch between the cert and private key).", ex); } } private static SigningCredentials LoadSigningCredentialsInner(string certPath, string keyPath) { // Validate the cert is readable (used downstream for token // audience/subject validation; signing itself uses the key). string keyPem = File.ReadAllText(keyPath); // BouncyCastle's PemReader accepts every flavour of unencrypted // private key PEM that ACME clients produce (PKCS#1 with // BEGIN EC/RSA PRIVATE KEY, PKCS#8 with BEGIN PRIVATE KEY, both // EC and RSA), and returns the right AsymmetricKeyParameter // subtype without the SIGABRTs we saw when forcing the // System.Security.Cryptography path on the production EC Let's // Encrypt cert. using var sr = new StringReader(keyPem); var pemReader = new PemReader(sr); var keyObj = pemReader.ReadObject() ?? throw new InvalidOperationException( $"PEM reader returned null for {keyPath}"); AsymmetricKeyParameter bcKey = keyObj switch { AsymmetricCipherKeyPair pair => pair.Private, AsymmetricKeyParameter param => param, _ => throw new InvalidOperationException( $"Unexpected PEM object type '{keyObj.GetType().FullName}' " + $"in {keyPath}; expected a private key."), }; // Build the SecurityKey + SigningCredentials. RsaSecurityKey / // ECDsaSecurityKey wrap managed AsymmetricAlgorithm objects whose // Key is the live private key — IdentityServer8 can call Sign on // these repeatedly without losing the key handle. switch (bcKey) { case RsaPrivateCrtKeyParameters rsa: { #pragma warning disable CA1416 // Valider la compatibilité de la plateforme var rsaDotNet = DotNetUtilities.ToRSA(rsa); #pragma warning restore CA1416 // Valider la compatibilité de la plateforme var key = new RsaSecurityKey(rsaDotNet); return new SigningCredentials(key, SecurityAlgorithms.RsaSha256); } case ECPrivateKeyParameters ec: { var ecParams = new ECParameters { Curve = LoadEcCurve(ec.Parameters), D = ec.D.ToByteArrayUnsigned(), }; var ecdsa = ECDsa.Create(); ecdsa.ImportParameters(ecParams); var key = new ECDsaSecurityKey(ecdsa); return new SigningCredentials(key, SecurityAlgorithms.EcdsaSha256); } default: throw new InvalidOperationException( $"Unsupported private key algorithm '{bcKey.GetType().Name}' " + $"in {keyPath}; expected RSA or EC."); } } /// /// Map a BouncyCastle to a /// that /// understands. Handles the curves Let's Encrypt issues (P-256, /// P-384, P-521); other curves throw. /// private static ECCurve LoadEcCurve(ECDomainParameters bcCurve) { // bcCurve.N is the order of the generator; its bit length is the // canonical fingerprint for NIST curves (256, 384, 521 bits). var orderBits = bcCurve.N.BitLength; return orderBits switch { 256 => ECCurve.NamedCurves.nistP256, 384 => ECCurve.NamedCurves.nistP384, 521 => ECCurve.NamedCurves.nistP521, _ => throw new InvalidOperationException( $"Unsupported EC curve with order bit length {orderBits}; " + "expected P-256, P-384 or P-521."), }; } private static bool UsesInMemoryProvider(string connectionString) { return string.Equals(connectionString, InMemoryProviderName, StringComparison.OrdinalIgnoreCase); } private static Action EnsureDefaultApplicationScopes() { return (context, _) => { foreach (String scope in Constants.BuildInApiScopes) { var existentScope = context.Set().FirstOrDefault(b => b.Name == scope); if (existentScope == null) { context.Set().Add(new IdentityServer8.EntityFramework.Entities.ApiScope { Name = scope }); context.SaveChanges(); } } var identityResources = context.Set(); var apiScopes = context.Set(); // IdentityResources standards if (!identityResources.Any(r => r.Name == "openid")) { var openid = new IdentityResources.OpenId().ToEntity(); identityResources.Add(openid); } if (!identityResources.Any(r => r.Name == "profile")) { var profile = new IdentityResources.Profile().ToEntity(); identityResources.Add(profile); } foreach (var scope in Constants.ApiResourcesScopes) { if (!identityResources.Any(s => s.Name == scope.ScopeName)) { identityResources.Add( new IdentityResources.Profile() { Name = scope.ScopeName, DisplayName = scope.Description, Enabled = true }.ToEntity()); } } context.SaveChanges(); }; } private const string PostItClientId = "postit"; private static readonly string[] PostItRedirectUris = new[] { // Loopback URI for desktop / browser-based PKCE flows. "postit://callback", "android://postit-signin", "https://blogs.pschneider.fr" }; private static readonly string[] PostItGrantTypes = new[] { "authorization_code", "client_credentials", }; private static readonly string[] PostItScopes = new[] { // Scopes the PostIt client is allowed to ask for. Must match // what postit-settings.json (and Constants.BuildInApiScopes on // the server) actually defines. Notably: // - "blogs" (plural) is the API scope that gates access to the // Yavsc.Blogs deployment at https://blogs.pschneider.fr. // - "offline_access" is required for the YavscApiClient's // silent refresh path to work; without it IdentityServer // refuses to issue a refresh_token. "blogs", IdentityServer8.IdentityServerConstants.StandardScopes.OpenId, IdentityServer8.IdentityServerConstants.StandardScopes.Profile, IdentityServer8.IdentityServerConstants.StandardScopes.OfflineAccess, }; private static Action EnsureDefaultConfiguration( IConfiguration configuration ) { return (context, _) => { EnsureDefaultApplicationScopes()(context, _); var clients = context.Set(); var existingClient = clients.FirstOrDefault(c => c.ClientId == PostItClientId); if (existingClient is null) { SeedNewPostItClient(configuration, context); return; } }; } /// /// Insert a brand new postit client configured as a public OIDC /// client using Authorization Code + PKCE. Used the first time the /// ConfigurationDb is seeded. /// private static void SeedNewPostItClient(IConfiguration configuration, DbContext context) { // PostIt is a public client (Authorization Code + PKCE). // No client secret is stored or transmitted; PKCE binds the // authorization code to the requesting device. var client = new IdentityServer8.EntityFramework.Entities.Client { ClientId = PostItClientId, Enabled = true, RequireClientSecret = false, RequirePkce = true, ProtocolType = "oidc", RequireConsent = false, }; context.Set().Add(client); foreach (var grantType in PostItGrantTypes) { context.Set().Add(new IdentityServer8.EntityFramework.Entities.ClientGrantType { Client = client, GrantType = grantType }); } foreach (var scope in PostItScopes) { context.Set().Add(new IdentityServer8.EntityFramework.Entities.ClientScope { Client = client, Scope = scope }); } foreach (var redirectUri in BuildPostItRedirectUris(configuration)) { context.Set().Add(new IdentityServer8.EntityFramework.Entities.ClientRedirectUri { Client = client, RedirectUri = redirectUri }); } // No ClientSecret row: PKCE-only clients don't need one. context.SaveChanges(); } /// /// Compose the full set of redirect URIs for the PostIt client. The base /// URIs cover the standalone desktop/mobile flows; the value of /// Site:ExternalUrl is appended so PostIt can also be embedded in /// a Yavsc.Org web page (e.g. an iframe-launched launcher). /// private static IEnumerable BuildPostItRedirectUris(IConfiguration configuration) { foreach (var uri in PostItRedirectUris) yield return uri; var externalUrl = configuration["Site:ExternalUrl"]; if (!string.IsNullOrWhiteSpace(externalUrl)) yield return externalUrl; } private static void ConfigureRequestLocalization(IServiceCollection services) { services.Configure(options => { CultureInfo[] supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("fr"), new CultureInfo("pt") }; CultureInfo[] supportedUICultures = new[] { new CultureInfo("fr"), new CultureInfo("en"), new CultureInfo("pt") }; // You must explicitly state which cultures your application supports. // These are the cultures the app supports for formatting numbers, dates, etc. options.SupportedCultures = supportedCultures; // These are the cultures the app supports for UI strings, i.e. we have localized resources for. options.SupportedUICultures = supportedUICultures; options.RequestCultureProviders = new List { new QueryStringRequestCultureProvider { Options = options }, new CookieRequestCultureProvider { Options = options, CookieName="ASPNET_CULTURE" }, new AcceptLanguageHeaderRequestCultureProvider { Options = options } }; }); } public async static Task ConfigurePipeline(this WebApplication app, string staticAssetsManifestPath = null) { ILoggerFactory loggerFactory = app.Services.GetRequiredService(); var logger = loggerFactory.CreateLogger(); AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true); JwtSecurityTokenHandler.DefaultMapInboundClaims = true; if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Home/Error"); app.MigrateDatabase(); } app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/robots.txt")) { var robotsTxtPath = System.IO.Path.Combine(app.Environment.WebRootPath, $"robots.txt"); string output = "User-agent: * \nDisallow: /"; if (File.Exists(robotsTxtPath)) { output = await File.ReadAllTextAsync(robotsTxtPath); } context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(output); } else await next(); }); app.UseSwagger(); app.UseSwaggerUI(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.UseCors("default"); app.MapDefaultControllerRoute(); app.MapStaticAssets(staticAssetsManifestPath); //app.MapRazorPages(); app.MapHub("/chatHub"); WorkflowHelpers.ConfigureBillingService(); var services = app.Services; var siteSettings = services.GetRequiredService>(); var smtpSettings = services.GetRequiredService>(); var payPalSettings = services.GetRequiredService>(); var googleAuthSettings = services.GetRequiredService>(); var localization = services.GetRequiredService>(); Startup.Configure(app, siteSettings, smtpSettings, payPalSettings, googleAuthSettings, localization, loggerFactory, app.Environment.EnvironmentName); app.ConfigureFileServerApp(); app.UseSession(); return app; } private static void MigrateDatabase(this IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetRequiredService().CreateScope()) { try { foreach (Type contextType in new Type[] { typeof(ApplicationDbContext) }) { ((DbContext)serviceScope.ServiceProvider .GetRequiredService(contextType)) .Database.Migrate(); } } catch (InvalidOperationException ex) { app.Properties["DegradedDBContext"] = ex.Message; } } } static void LoadGoogleConfig(IConfigurationRoot configuration) { string? googleClientFile = configuration["Authentication:Google:GoogleWebClientJson"]; string? googleServiceAccountJsonFile = configuration["Authentication:Google:GoogleServiceAccountJson"]; if (googleClientFile != null) { Config.GoogleWebClientConfiguration = new ConfigurationBuilder().AddJsonFile(googleClientFile).Build(); } if (googleServiceAccountJsonFile != null) { FileInfo safile = new FileInfo(googleServiceAccountJsonFile); Config.GServiceAccount = JsonConvert.DeserializeObject(safile.OpenText().ReadToEnd()); } } public static IApplicationBuilder ConfigureFileServerApp(this IApplicationBuilder app, bool enableDirectoryBrowsing = false) { var userFilesDirInfo = new DirectoryInfo(Config.SiteSetup.Blog); AbstractFileSystemHelpers.UserFilesDirName = userFilesDirInfo.FullName; if (!userFilesDirInfo.Exists) userFilesDirInfo.Create(); Config.UserFilesOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(AbstractFileSystemHelpers.UserFilesDirName), RequestPath = PathString.FromUriComponent(YavscConstants.UserFilesPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.UserFilesOptions.EnableDefaultFiles = true; Config.UserFilesOptions.StaticFileOptions.ServeUnknownFileTypes = true; var avatarsDirInfo = new DirectoryInfo(Config.SiteSetup.Avatars); if (!avatarsDirInfo.Exists) avatarsDirInfo.Create(); Config.AvatarsDirName = avatarsDirInfo.FullName; Config.AvatarsOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.AvatarsDirName), RequestPath = PathString.FromUriComponent(YavscConstants.AvatarsPath), EnableDirectoryBrowsing = enableDirectoryBrowsing }; var gitdirinfo = new DirectoryInfo(Config.SiteSetup.GitRepository); Config.GitDirName = gitdirinfo.FullName; if (!gitdirinfo.Exists) gitdirinfo.Create(); Config.GitOptions = new FileServerOptions() { FileProvider = new PhysicalFileProvider(Config.GitDirName), RequestPath = PathString.FromUriComponent(YavscConstants.GitPath), EnableDirectoryBrowsing = enableDirectoryBrowsing, }; Config.GitOptions.DefaultFilesOptions.DefaultFileNames.Add("index.md"); Config.GitOptions.StaticFileOptions.ServeUnknownFileTypes = true; app.UseFileServer(Config.UserFilesOptions); app.UseFileServer(Config.AvatarsOptions); app.UseFileServer(Config.GitOptions); app.UseStaticFiles(); return app; } }