From 919fb5012f33a6517f3217f10c6f4c4b3364d514 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 26 Aug 2025 18:13:09 +0800 Subject: [PATCH 01/25] Fix #5532: add require nonce setting. --- Oqtane.Client/Modules/Admin/Users/Index.razor | 15 +++++++++++++++ .../Resources/Modules/Admin/Users/Index.resx | 6 ++++++ .../OqtaneSiteAuthenticationBuilderExtensions.cs | 1 + 3 files changed, 22 insertions(+) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 53218b3c..97bbdebe 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -413,6 +413,18 @@ else + @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) + { +
+ +
+ +
+
+ }
@@ -557,6 +569,7 @@ else private string _synchronizeroles; private string _profileclaimtypes; private string _savetokens; + private string _requirenonce; private string _domainfilter; private string _createusers; private string _verifyusers; @@ -643,6 +656,7 @@ else _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); _savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false"); + _requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "false"); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); @@ -762,6 +776,7 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:SaveTokens", _savetokens, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:RequireNonce", _requirenonce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 16e0d40e..e6d07a27 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -513,6 +513,12 @@ OpenID Connect (OIDC) + + Require Nonce? + + + Specify the RequireNonce property for OpenID Connect Authentication. + Save Tokens? diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index f142c602..6f6651e3 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -63,6 +63,7 @@ namespace Oqtane.Extensions options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); + options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "false")); ; if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); From 91e55aeb9b15da948e154a301ac6e8982c5b67ea Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 26 Aug 2025 20:26:11 +0800 Subject: [PATCH 02/25] Fix #5532: change the default value to true. --- Oqtane.Client/Modules/Admin/Users/Index.razor | 2 +- .../Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 97bbdebe..fa3dccf3 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -656,7 +656,7 @@ else _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); _savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false"); - _requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "false"); + _requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "true"); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 6f6651e3..3bdc49c4 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -63,7 +63,7 @@ namespace Oqtane.Extensions options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); - options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "false")); ; + options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "true")); ; if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); From f451cfce090fe2c235958effa4f2ee5d5addae48 Mon Sep 17 00:00:00 2001 From: Ben Date: Tue, 26 Aug 2025 20:27:41 +0800 Subject: [PATCH 03/25] Fix #5532: remove duplicated semi colon. --- .../Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 3bdc49c4..544ad068 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -63,7 +63,7 @@ namespace Oqtane.Extensions options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); - options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "true")); ; + options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "true")); if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); From ec06c1cdf1bf2f903c3607d95762c095ccb45328 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 26 Aug 2025 15:27:35 -0400 Subject: [PATCH 04/25] optimize startup --- .../ApplicationBuilderExtensions.cs | 91 +++++++ .../OqtaneServiceCollectionExtensions.cs | 141 +++++++++- .../Infrastructure/LocalizationManager.cs | 6 + Oqtane.Server/Startup.cs | 241 +----------------- 4 files changed, 241 insertions(+), 238 deletions(-) diff --git a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs index 3e31ac60..fa3835ea 100644 --- a/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/ApplicationBuilderExtensions.cs @@ -2,14 +2,105 @@ using System; using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Net.Http.Headers; +using Oqtane.Components; using Oqtane.Infrastructure; +using Oqtane.Shared; +using Oqtane.UI; +using OqtaneSSR.Extensions; namespace Oqtane.Extensions { public static class ApplicationBuilderExtensions { + public static IApplicationBuilder UseOqtane(this IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) + { + ServiceActivator.Configure(app.ApplicationServices); + + if (environment.IsDevelopment()) + { + app.UseWebAssemblyDebugging(); + app.UseForwardedHeaders(); + } + else + { + app.UseForwardedHeaders(); + app.UseExceptionHandler("/Error", createScopeForErrors: true); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + // allow oqtane localization middleware + app.UseOqtaneLocalization(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(new StaticFileOptions + { + OnPrepareResponse = (ctx) => + { + // static asset caching + var cachecontrol = configuration.GetSection("CacheControl"); + if (!string.IsNullOrEmpty(cachecontrol.Value)) + { + ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); + } + // CORS headers for .NET MAUI clients + var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) + .ConfigureAwait(false).GetAwaiter().GetResult(); + corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); + } + }); + app.UseExceptionMiddleWare(); + app.UseTenantResolution(); + app.UseJwtAuthorization(); + app.UseRouting(); + app.UseCors(); + app.UseOutputCache(); + app.UseAuthentication(); + app.UseAuthorization(); + app.UseAntiforgery(); + + // execute any IServerStartup logic + app.ConfigureOqtaneAssemblies(environment); + + if (configuration.GetSection("UseSwagger").Value != "false") + { + app.UseSwagger(); + app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/" + Constants.Version + "/swagger.json", Constants.PackageId + " " + Constants.Version); }); + } + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapRazorPages(); + }); + + app.UseEndpoints(endpoints => + { + endpoints.MapRazorComponents() + .AddInteractiveServerRenderMode() + .AddInteractiveWebAssemblyRenderMode() + .AddAdditionalAssemblies(typeof(SiteRouter).Assembly); + }); + + // simulate the fallback routing approach of traditional Blazor - allowing the custom SiteRouter to handle all routing concerns + app.UseEndpoints(endpoints => + { + endpoints.MapFallback(); + }); + + // create a global sync event to identify server application startup + sync.AddSyncEvent(-1, -1, EntityNames.Host, -1, SyncEventActions.Reload); + + return app; + } + public static IApplicationBuilder ConfigureOqtaneAssemblies(this IApplicationBuilder app, IWebHostEnvironment env) { var startUps = AppDomain.CurrentDomain diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 20a11ced..ccd9f3a3 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.IO; @@ -11,12 +12,17 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication.OAuth; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; +using Oqtane.Extensions; using Oqtane.Infrastructure; using Oqtane.Interfaces; using Oqtane.Managers; @@ -31,10 +37,126 @@ namespace Microsoft.Extensions.DependencyInjection { public static class OqtaneServiceCollectionExtensions { - public static IServiceCollection AddOqtane(this IServiceCollection services, string[] installedCultures) + public static IServiceCollection AddOqtane(this IServiceCollection services, IConfigurationRoot configuration, IWebHostEnvironment environment) + { + // process forwarded headers on load balancers and proxy servers + services.Configure(options => + { + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + }); + + // register localization services + services.AddLocalization(options => options.ResourcesPath = "Resources"); + + services.AddOptions>().Bind(configuration.GetSection(SettingKeys.AvailableDatabasesSection)); + + // register scoped core services + services.AddScoped() + .AddOqtaneServerScopedServices(); + + services.AddSingleton(); + + // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) + services.AddHttpClients(); + + // register singleton scoped core services + services.AddSingleton(configuration) + .AddOqtaneSingletonServices(); + + // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) + InstallationManager.InstallPackages(environment.WebRootPath, environment.ContentRootPath); + + // register transient scoped core services + services.AddOqtaneTransientServices(); + + // load the external assemblies into the app domain, install services + services.AddOqtaneAssemblies(); + services.AddOqtaneDbContext(); + + services.AddAntiforgery(options => + { + options.HeaderName = Constants.AntiForgeryTokenHeaderName; + options.Cookie.Name = Constants.AntiForgeryTokenCookieName; + options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; + options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; + options.Cookie.HttpOnly = true; + }); + + services.AddIdentityCore(options => { }) + .AddEntityFrameworkStores() + .AddSignInManager() + .AddDefaultTokenProviders() + .AddClaimsPrincipalFactory>(); // role claims + + services.ConfigureOqtaneIdentityOptions(configuration); + + services.AddCascadingAuthenticationState(); + services.AddScoped(); + services.AddAuthorization(); + + services.AddAuthentication(options => + { + options.DefaultScheme = Constants.AuthenticationScheme; + }) + .AddCookie(Constants.AuthenticationScheme) + .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { }) + .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { }); + + services.ConfigureOqtaneCookieOptions(); + services.ConfigureOqtaneAuthenticationOptions(configuration); + + services.AddOqtaneSiteOptions() + .WithSiteIdentity() + .WithSiteAuthentication(); + + services.AddCors(options => + { + options.AddPolicy(Constants.MauiCorsPolicy, + policy => + { + // allow .NET MAUI client cross origin calls + policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") + .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); + }); + }); + + services.AddOutputCache(); + + services.AddMvc(options => + { + options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); + }) + .AddOqtaneApplicationParts() // register any Controllers from custom modules + .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes + + services.AddRazorPages(); + + services.AddRazorComponents() + .AddInteractiveServerComponents(options => + { + if (environment.IsDevelopment()) + { + options.DetailedErrors = true; + } + }).AddHubOptions(options => + { + options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) + }) + .AddInteractiveWebAssemblyComponents(); + + services.AddSwaggerGen(options => + { + options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type + }); + services.TryAddSwagger(configuration); + + return services; + } + + public static IServiceCollection AddOqtaneAssemblies(this IServiceCollection services) { LoadAssemblies(); - LoadSatelliteAssemblies(installedCultures); + LoadSatelliteAssemblies(); services.AddOqtaneServices(); return services; @@ -53,7 +175,7 @@ namespace Microsoft.Extensions.DependencyInjection return new OqtaneSiteOptionsBuilder(services); } - internal static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneSingletonServices(this IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); @@ -66,7 +188,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneServerScopedServices(this IServiceCollection services) { services.AddScoped(); services.AddScoped(); @@ -112,7 +234,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) + public static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) { // services services.AddTransient(); @@ -242,7 +364,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection AddHttpClients(this IServiceCollection services) + public static IServiceCollection AddHttpClients(this IServiceCollection services) { if (!services.Any(x => x.ServiceType == typeof(HttpClient))) { @@ -285,9 +407,9 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - internal static IServiceCollection TryAddSwagger(this IServiceCollection services, bool useSwagger) + public static IServiceCollection TryAddSwagger(this IServiceCollection services, IConfigurationRoot configuration) { - if (useSwagger) + if (configuration.GetSection("UseSwagger").Value != "false") { services.AddSwaggerGen(c => { @@ -386,10 +508,11 @@ namespace Microsoft.Extensions.DependencyInjection } } - private static void LoadSatelliteAssemblies(string[] installedCultures) + private static void LoadSatelliteAssemblies() { AssemblyLoadContext.Default.Resolving += ResolveDependencies; + var installedCultures = LocalizationManager.GetSatelliteAssemblyCultures(); foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) { var code = Path.GetFileName(Path.GetDirectoryName(file)); diff --git a/Oqtane.Server/Infrastructure/LocalizationManager.cs b/Oqtane.Server/Infrastructure/LocalizationManager.cs index 0d083c4f..3dfc0b54 100644 --- a/Oqtane.Server/Infrastructure/LocalizationManager.cs +++ b/Oqtane.Server/Infrastructure/LocalizationManager.cs @@ -45,6 +45,12 @@ namespace Oqtane.Infrastructure } public string[] GetInstalledCultures() + { + return GetSatelliteAssemblyCultures(); + } + + // method is static as it is called during startup + public static string[] GetSatelliteAssemblyCultures() { var cultures = new List(); foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 3642addd..979c57e4 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -1,260 +1,43 @@ using System; -using System.Collections.Generic; using System.IO; -using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Oqtane.Extensions; using Oqtane.Infrastructure; -using Oqtane.Repository; -using Oqtane.Security; using Oqtane.Shared; -using Microsoft.AspNetCore.HttpOverrides; -using Microsoft.Extensions.Logging; -using Oqtane.Components; -using Oqtane.UI; -using OqtaneSSR.Extensions; -using Microsoft.AspNetCore.Components.Authorization; -using Oqtane.Providers; using Microsoft.AspNetCore.Cors.Infrastructure; -using Microsoft.Net.Http.Headers; namespace Oqtane { public class Startup { - private readonly bool _useSwagger; - private readonly IWebHostEnvironment _env; - private readonly string[] _installedCultures; - private string _configureServicesErrors; + private readonly IConfigurationRoot _configuration; + private readonly IWebHostEnvironment _environment; - public IConfigurationRoot Configuration { get; } - - public Startup(IWebHostEnvironment env, ILocalizationManager localizationManager) + public Startup(IWebHostEnvironment environment) { + AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data")); + var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) + .SetBasePath(environment.ContentRootPath) .AddJsonFile("appsettings.json", false, true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true) + .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true) .AddEnvironmentVariables(); - Configuration = builder.Build(); - - _installedCultures = localizationManager.GetInstalledCultures(); - - //add possibility to switch off swagger on production. - _useSwagger = Configuration.GetSection("UseSwagger").Value != "false"; - - AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(env.ContentRootPath, "Data")); - - _env = env; + _configuration = builder.Build(); + _environment = environment; } - // This method gets called by the runtime. Use this method to add services to the container. - // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { - // process forwarded headers on load balancers and proxy servers - services.Configure(options => - { - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - }); - - // register localization services - services.AddLocalization(options => options.ResourcesPath = "Resources"); - - services.AddOptions>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection)); - - // register scoped core services - services.AddScoped() - .AddOqtaneServerScopedServices(); - - services.AddSingleton(); - - // setup HttpClient for server side in a client side compatible fashion ( with auth cookie ) - services.AddHttpClients(); - - // register singleton scoped core services - services.AddSingleton(Configuration) - .AddOqtaneSingletonServices(); - - // install any modules or themes ( this needs to occur BEFORE the assemblies are loaded into the app domain ) - _configureServicesErrors += InstallationManager.InstallPackages(_env.WebRootPath, _env.ContentRootPath); - - // register transient scoped core services - services.AddOqtaneTransientServices(); - - // load the external assemblies into the app domain, install services - services.AddOqtane(_installedCultures); - services.AddOqtaneDbContext(); - - services.AddAntiforgery(options => - { - options.HeaderName = Constants.AntiForgeryTokenHeaderName; - options.Cookie.Name = Constants.AntiForgeryTokenCookieName; - options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict; - options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; - options.Cookie.HttpOnly = true; - }); - - services.AddIdentityCore(options => { }) - .AddEntityFrameworkStores() - .AddSignInManager() - .AddDefaultTokenProviders() - .AddClaimsPrincipalFactory>(); // role claims - - services.ConfigureOqtaneIdentityOptions(Configuration); - - services.AddCascadingAuthenticationState(); - services.AddScoped(); - services.AddAuthorization(); - - services.AddAuthentication(options => - { - options.DefaultScheme = Constants.AuthenticationScheme; - }) - .AddCookie(Constants.AuthenticationScheme) - .AddOpenIdConnect(AuthenticationProviderTypes.OpenIDConnect, options => { }) - .AddOAuth(AuthenticationProviderTypes.OAuth2, options => { }); - - services.ConfigureOqtaneCookieOptions(); - services.ConfigureOqtaneAuthenticationOptions(Configuration); - - services.AddOqtaneSiteOptions() - .WithSiteIdentity() - .WithSiteAuthentication(); - - services.AddCors(options => - { - options.AddPolicy(Constants.MauiCorsPolicy, - policy => - { - // allow .NET MAUI client cross origin calls - policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") - .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); - }); - }); - - services.AddOutputCache(); - - services.AddMvc(options => - { - options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); - }) - .AddOqtaneApplicationParts() // register any Controllers from custom modules - .ConfigureOqtaneMvc(); // any additional configuration from IStartup classes - - services.AddRazorPages(); - - services.AddRazorComponents() - .AddInteractiveServerComponents(options => - { - if (_env.IsDevelopment()) - { - options.DetailedErrors = true; - } - }).AddHubOptions(options => - { - options.MaximumReceiveMessageSize = null; // no limit (for large amounts of data ie. textarea components) - }) - .AddInteractiveWebAssemblyComponents(); - - services.AddSwaggerGen(options => - { - options.CustomSchemaIds(type => type.ToString()); // Handle SchemaId already used for different type - }); - services.TryAddSwagger(_useSwagger); + services.AddOqtane(_configuration, _environment); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ISyncManager sync, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ILogger logger) + public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) { - if (!string.IsNullOrEmpty(_configureServicesErrors)) - { - logger.LogError(_configureServicesErrors); - } - - ServiceActivator.Configure(app.ApplicationServices); - - if (env.IsDevelopment()) - { - app.UseWebAssemblyDebugging(); - app.UseForwardedHeaders(); - } - else - { - app.UseForwardedHeaders(); - app.UseExceptionHandler("/Error", createScopeForErrors: true); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - - // allow oqtane localization middleware - app.UseOqtaneLocalization(); - - app.UseHttpsRedirection(); - app.UseStaticFiles(new StaticFileOptions - { - OnPrepareResponse = (ctx) => - { - // static asset caching - var cachecontrol = Configuration.GetSection("CacheControl"); - if (!string.IsNullOrEmpty(cachecontrol.Value)) - { - ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value); - } - // CORS headers for .NET MAUI clients - var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) - .ConfigureAwait(false).GetAwaiter().GetResult(); - corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); - } - }); - app.UseExceptionMiddleWare(); - app.UseTenantResolution(); - app.UseJwtAuthorization(); - app.UseRouting(); - app.UseCors(); - app.UseOutputCache(); - app.UseAuthentication(); - app.UseAuthorization(); - app.UseAntiforgery(); - - // execute any IServerStartup logic - app.ConfigureOqtaneAssemblies(env); - - if (_useSwagger) - { - app.UseSwagger(); - app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/" + Constants.Version + "/swagger.json", Constants.PackageId + " " + Constants.Version); }); - } - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - endpoints.MapRazorPages(); - }); - - app.UseEndpoints(endpoints => - { - endpoints.MapRazorComponents() - .AddInteractiveServerRenderMode() - .AddInteractiveWebAssemblyRenderMode() - .AddAdditionalAssemblies(typeof(SiteRouter).Assembly); - }); - - // simulate the fallback routing approach of traditional Blazor - allowing the custom SiteRouter to handle all routing concerns - app.UseEndpoints(endpoints => - { - endpoints.MapFallback(); - }); - - // create a global sync event to identify server application startup - sync.AddSyncEvent(-1, -1, EntityNames.Host, -1, SyncEventActions.Reload); + app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync); } } } From 4d5168c99889478073ac0db66eb763a2f4f15658 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 26 Aug 2025 17:15:46 -0400 Subject: [PATCH 05/25] application template changes --- .../AppHost/Oqtane.Application.AppHost.csproj | 12 ----- .../AppHost/Properties/launchSettings.json | 29 ------------ .../AppHost/wwwroot/_content/Placeholder.txt | 11 ----- .../AppHost/wwwroot/resources.txt | 1 - .../Client/Modules/MyModule/Edit.razor | 2 +- .../Client/Modules/MyModule/Index.razor | 4 +- .../Client/Oqtane.Application.Client.csproj | 34 +++++++------- Oqtane.Application/Client/Program.cs | 12 +++++ .../Client/Themes/MyTheme/ThemeInfo.cs | 4 +- Oqtane.Application/Oqtane.Application.sln | 19 ++------ .../Server/Oqtane.Application.Server.csproj | 35 ++++++-------- .../{AppHost => Server}/Program.cs | 14 +++--- .../Server/Properties/launchSettings.json | 25 ++++++++++ Oqtane.Application/Server/Startup.cs | 43 ++++++++++++++++++ .../{AppHost => Server}/appsettings.json | 5 +- .../Oqtane.Modules.Admin.Login/Module.css | 0 .../Oqtane.Modules.HtmlText/Module.css | 0 .../wwwroot/Oqtane.Server.lib.module.js | 0 .../Oqtane.Themes.BlazorTheme/Theme.css | 0 .../Oqtane.Themes.OqtaneTheme/Theme.css | 0 .../Server/wwwroot/_content/Placeholder.txt | 11 ----- .../wwwroot/app_offline.bak | 0 .../{AppHost => Server}/wwwroot/css/app.css | 0 .../wwwroot/css/open-iconic/FONT-LICENSE | 0 .../wwwroot/css/open-iconic/ICON-LICENSE | 0 .../wwwroot/css/open-iconic/README.md | 0 .../font/css/open-iconic-bootstrap.min.css | 0 .../open-iconic/font/fonts/open-iconic.eot | Bin .../open-iconic/font/fonts/open-iconic.otf | Bin .../open-iconic/font/fonts/open-iconic.svg | 0 .../open-iconic/font/fonts/open-iconic.ttf | Bin .../open-iconic/font/fonts/open-iconic.woff | Bin .../wwwroot/css/quill/quill.bubble.css | 0 .../wwwroot/css/quill/quill.snow.css | 0 .../wwwroot/css/quill/quill1.3.7.bubble.css | 0 .../wwwroot/css/quill/quill1.3.7.snow.css | 0 .../{AppHost => Server}/wwwroot/favicon.ico | Bin .../{AppHost => Server}/wwwroot/icon.png | Bin .../wwwroot/images/checked.png | Bin .../wwwroot/images/disabled.png | Bin .../wwwroot/images/error.png | Bin .../wwwroot/images/help.png | Bin .../wwwroot/images/logo-black.png | Bin .../wwwroot/images/logo-white.png | Bin .../wwwroot/images/null.png | Bin .../wwwroot/images/unchecked.png | Bin .../{AppHost => Server}/wwwroot/js/app.js | 0 .../{AppHost => Server}/wwwroot/js/interop.js | 0 .../wwwroot/js/loadjs.min.js | 0 .../wwwroot/js/quill-blot-formatter.min.js | 0 .../wwwroot/js/quill-interop.js | 0 .../wwwroot/js/quill.min.js | 0 .../wwwroot/js/quill.min.js.map | 0 .../wwwroot/js/quill1.3.7.min.js | 0 .../{AppHost => Server}/wwwroot/js/reload.js | 0 .../{AppHost => Server}/wwwroot/loading.gif | Bin .../wwwroot/oqtane-black.png | Bin .../wwwroot/oqtane-glow.png | Bin .../wwwroot/oqtane-white.png | Bin .../{AppHost => Server}/wwwroot/oqtane.ico | Bin .../{AppHost => Server}/wwwroot/oqtane.png | Bin .../{AppHost => Server}/wwwroot/package.png | Bin .../wwwroot/service-worker.js | 0 .../{AppHost => Server}/wwwroot/users.txt | 0 .../Shared/Oqtane.Application.Shared.csproj | 16 +++---- 65 files changed, 136 insertions(+), 141 deletions(-) delete mode 100644 Oqtane.Application/AppHost/Oqtane.Application.AppHost.csproj delete mode 100644 Oqtane.Application/AppHost/Properties/launchSettings.json delete mode 100644 Oqtane.Application/AppHost/wwwroot/_content/Placeholder.txt delete mode 100644 Oqtane.Application/AppHost/wwwroot/resources.txt create mode 100644 Oqtane.Application/Client/Program.cs rename Oqtane.Application/{AppHost => Server}/Program.cs (84%) create mode 100644 Oqtane.Application/Server/Properties/launchSettings.json create mode 100644 Oqtane.Application/Server/Startup.cs rename Oqtane.Application/{AppHost => Server}/appsettings.json (92%) rename Oqtane.Application/{AppHost => Server}/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/Modules/Oqtane.Modules.HtmlText/Module.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/Oqtane.Server.lib.module.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css (100%) delete mode 100644 Oqtane.Application/Server/wwwroot/_content/Placeholder.txt rename Oqtane.Application/{AppHost => Server}/wwwroot/app_offline.bak (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/app.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/FONT-LICENSE (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/ICON-LICENSE (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/README.md (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/fonts/open-iconic.eot (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/fonts/open-iconic.otf (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/fonts/open-iconic.svg (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/open-iconic/font/fonts/open-iconic.woff (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/quill/quill.bubble.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/quill/quill.snow.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/quill/quill1.3.7.bubble.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/css/quill/quill1.3.7.snow.css (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/favicon.ico (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/icon.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/checked.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/disabled.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/error.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/help.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/logo-black.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/logo-white.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/null.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/images/unchecked.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/app.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/interop.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/loadjs.min.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/quill-blot-formatter.min.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/quill-interop.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/quill.min.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/quill.min.js.map (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/quill1.3.7.min.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/js/reload.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/loading.gif (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/oqtane-black.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/oqtane-glow.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/oqtane-white.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/oqtane.ico (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/oqtane.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/package.png (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/service-worker.js (100%) rename Oqtane.Application/{AppHost => Server}/wwwroot/users.txt (100%) diff --git a/Oqtane.Application/AppHost/Oqtane.Application.AppHost.csproj b/Oqtane.Application/AppHost/Oqtane.Application.AppHost.csproj deleted file mode 100644 index 9a9cb9fa..00000000 --- a/Oqtane.Application/AppHost/Oqtane.Application.AppHost.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - - net9.0 - 1.0.0 - Oqtane.Application.AppHost - - - - - - diff --git a/Oqtane.Application/AppHost/Properties/launchSettings.json b/Oqtane.Application/AppHost/Properties/launchSettings.json deleted file mode 100644 index 11cab4d9..00000000 --- a/Oqtane.Application/AppHost/Properties/launchSettings.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:44358/", - "sslPort": 0 - } - }, - "profiles": { - "Oqtane.Application": { - "commandName": "Project", - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:44358/" - }, - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/Oqtane.Application/AppHost/wwwroot/_content/Placeholder.txt b/Oqtane.Application/AppHost/wwwroot/_content/Placeholder.txt deleted file mode 100644 index 5a324d79..00000000 --- a/Oqtane.Application/AppHost/wwwroot/_content/Placeholder.txt +++ /dev/null @@ -1,11 +0,0 @@ -The _content folder should only contain static resources from shared razor component libraries (RCLs). Static resources can be extracted from shared RCL Nuget packages by executing a Publish task on the module's Server project to a local folder and copying the files from the _content folder which is created. Each shared RCL would have its own appropriately named subfolder within the module's _content folder. - -ie. - -/_content - /Radzen.Blazor - /css - /fonts - /syncfusion.blazor - /scripts - /styles diff --git a/Oqtane.Application/AppHost/wwwroot/resources.txt b/Oqtane.Application/AppHost/wwwroot/resources.txt deleted file mode 100644 index 2542de03..00000000 --- a/Oqtane.Application/AppHost/wwwroot/resources.txt +++ /dev/null @@ -1 +0,0 @@ -This is the location where static resources such as images or style sheets should be located \ No newline at end of file diff --git a/Oqtane.Application/Client/Modules/MyModule/Edit.razor b/Oqtane.Application/Client/Modules/MyModule/Edit.razor index f84c4daf..3e9caa2a 100644 --- a/Oqtane.Application/Client/Modules/MyModule/Edit.razor +++ b/Oqtane.Application/Client/Modules/MyModule/Edit.razor @@ -35,7 +35,7 @@ public override List Resources => new List() { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } + new Stylesheet(ModulePath() + "Module.css") }; private ElementReference form; diff --git a/Oqtane.Application/Client/Modules/MyModule/Index.razor b/Oqtane.Application/Client/Modules/MyModule/Index.razor index 637ba533..1987a19a 100644 --- a/Oqtane.Application/Client/Modules/MyModule/Index.razor +++ b/Oqtane.Application/Client/Modules/MyModule/Index.razor @@ -42,8 +42,8 @@ else public override List Resources => new List() { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }, - new Resource { ResourceType = ResourceType.Script, Url = ModulePath() + "Module.js" } + new Stylesheet(ModulePath() + "Module.css"), + new Script(ModulePath() + "Module.js") }; List _MyModules; diff --git a/Oqtane.Application/Client/Oqtane.Application.Client.csproj b/Oqtane.Application/Client/Oqtane.Application.Client.csproj index 6d83fd5c..638acb68 100644 --- a/Oqtane.Application/Client/Oqtane.Application.Client.csproj +++ b/Oqtane.Application/Client/Oqtane.Application.Client.csproj @@ -1,23 +1,21 @@ - + - - net9.0 - 1.0.0 - Oqtane.Application.Client.Oqtane - + + net9.0 + 1.0.0 + Oqtane.Application.Client.Oqtane + true + Default + false + true + - - - + + + - - - - - - - false - false - + + + diff --git a/Oqtane.Application/Client/Program.cs b/Oqtane.Application/Client/Program.cs new file mode 100644 index 00000000..3d0820e2 --- /dev/null +++ b/Oqtane.Application/Client/Program.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Oqtane.Application.Client +{ + internal class Program + { + static async Task Main(string[] args) + { + await Oqtane.Client.Program.Main(args); + } + } +} diff --git a/Oqtane.Application/Client/Themes/MyTheme/ThemeInfo.cs b/Oqtane.Application/Client/Themes/MyTheme/ThemeInfo.cs index 55047946..f46efe9a 100644 --- a/Oqtane.Application/Client/Themes/MyTheme/ThemeInfo.cs +++ b/Oqtane.Application/Client/Themes/MyTheme/ThemeInfo.cs @@ -16,8 +16,8 @@ namespace Oqtane.Application.MyTheme ContainerSettingsType = "Oqtane.Application.MyTheme.ContainerSettings, Oqtane.Application.Client.Oqtane", Resources = new List() { - new Script(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), - new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, + new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), + new Stylesheet("~/Theme.css"), new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") } }; diff --git a/Oqtane.Application/Oqtane.Application.sln b/Oqtane.Application/Oqtane.Application.sln index a1054772..ea06ef57 100644 --- a/Oqtane.Application/Oqtane.Application.sln +++ b/Oqtane.Application/Oqtane.Application.sln @@ -2,12 +2,10 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.12.35506.116 d17.12 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Oqtane.Application.AppHost", "AppHost\Oqtane.Application.AppHost.csproj", "{5BDDA15B-05CF-41B2-BF12-D532D1A561D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Server", "Server\Oqtane.Application.Server.csproj", "{04B05448-788F-433D-92C0-FED35122D45A}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Client", "Client\Oqtane.Application.Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Server", "Server\Oqtane.Application.Server.csproj", "{04B05448-788F-433D-92C0-FED35122D45A}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Application.Shared", "Shared\Oqtane.Application.Shared.csproj", "{18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}" EndProject Global @@ -16,18 +14,14 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {5BDDA15B-05CF-41B2-BF12-D532D1A561D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5BDDA15B-05CF-41B2-BF12-D532D1A561D1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5BDDA15B-05CF-41B2-BF12-D532D1A561D1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5BDDA15B-05CF-41B2-BF12-D532D1A561D1}.Release|Any CPU.Build.0 = Release|Any CPU - {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU {04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {04B05448-788F-433D-92C0-FED35122D45A}.Debug|Any CPU.Build.0 = Debug|Any CPU {04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.ActiveCfg = Release|Any CPU {04B05448-788F-433D-92C0-FED35122D45A}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU {18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {18D73F73-D7BE-4388-85BA-FBD9AC96FCA2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -36,7 +30,4 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {1D016F15-46FE-4726-8DFD-2E4FD4DC7668} - EndGlobalSection EndGlobal diff --git a/Oqtane.Application/Server/Oqtane.Application.Server.csproj b/Oqtane.Application/Server/Oqtane.Application.Server.csproj index 7e9ad243..5259f3b5 100644 --- a/Oqtane.Application/Server/Oqtane.Application.Server.csproj +++ b/Oqtane.Application/Server/Oqtane.Application.Server.csproj @@ -1,25 +1,18 @@ - + - - net9.0 - true - 1.0.0 - Oqtane.Application.Server.Oqtane - + + net9.0 + 1.0.0 + Oqtane.Application.Server.Oqtane + - - - - + + + + - - - - - - - - - - + + + + diff --git a/Oqtane.Application/AppHost/Program.cs b/Oqtane.Application/Server/Program.cs similarity index 84% rename from Oqtane.Application/AppHost/Program.cs rename to Oqtane.Application/Server/Program.cs index e2a7714c..9cbd89e5 100644 --- a/Oqtane.Application/AppHost/Program.cs +++ b/Oqtane.Application/Server/Program.cs @@ -1,12 +1,11 @@ using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore; -using Microsoft.Extensions.DependencyInjection; -using Oqtane.Infrastructure; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using Oqtane.Infrastructure; +using Microsoft.Extensions.DependencyInjection; -namespace Oqtane.Application.AppHost +namespace Oqtane.Application.Server { public class Program { @@ -20,7 +19,7 @@ namespace Oqtane.Application.AppHost var filelogger = host.Services.GetRequiredService>(); if (filelogger != null) { - filelogger.LogError($"[Oqtane.Application.AppHost.Program.Main] {install.Message}"); + filelogger.LogError($"[Oqtane.Application.Server.Program.Main] {install.Message}"); } } else @@ -35,9 +34,8 @@ namespace Oqtane.Application.AppHost .AddCommandLine(args) .AddEnvironmentVariables() .Build()) - .UseStartup() + .UseStartup() .ConfigureLocalizationSettings() .Build(); } } - diff --git a/Oqtane.Application/Server/Properties/launchSettings.json b/Oqtane.Application/Server/Properties/launchSettings.json new file mode 100644 index 00000000..80a48970 --- /dev/null +++ b/Oqtane.Application/Server/Properties/launchSettings.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", + "applicationUrl": "https://localhost:7035;http://localhost:5084", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } + } diff --git a/Oqtane.Application/Server/Startup.cs b/Oqtane.Application/Server/Startup.cs new file mode 100644 index 00000000..da1bc828 --- /dev/null +++ b/Oqtane.Application/Server/Startup.cs @@ -0,0 +1,43 @@ +using System; +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Extensions; +using Oqtane.Infrastructure; +using Oqtane.Shared; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Oqtane.Application.Server +{ + public class Startup + { + private readonly IConfigurationRoot _configuration; + private readonly IWebHostEnvironment _environment; + + public Startup(IWebHostEnvironment environment) + { + AppDomain.CurrentDomain.SetData(Constants.DataDirectory, Path.Combine(environment.ContentRootPath, "Data")); + + var builder = new ConfigurationBuilder() + .SetBasePath(environment.ContentRootPath) + .AddJsonFile("appsettings.json", false, true) + .AddJsonFile($"appsettings.{environment.EnvironmentName}.json", true, true) + .AddEnvironmentVariables(); + + _configuration = builder.Build(); + _environment = environment; + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddOqtane(_configuration, _environment); + } + + public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) + { + app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync); + } + } +} diff --git a/Oqtane.Application/AppHost/appsettings.json b/Oqtane.Application/Server/appsettings.json similarity index 92% rename from Oqtane.Application/AppHost/appsettings.json rename to Oqtane.Application/Server/appsettings.json index cbf901bd..4da5bbc7 100644 --- a/Oqtane.Application/AppHost/appsettings.json +++ b/Oqtane.Application/Server/appsettings.json @@ -2,7 +2,7 @@ "RenderMode": "Interactive", "Runtime": "Server", "Database": { - "DefaultDBType": "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Server" + "DefaultDBType": "" }, "ConnectionStrings": { "DefaultConnection": "" @@ -57,8 +57,7 @@ } }, "LogLevel": { - "Default": "Information", - "Notify": "Error" + "Default": "Information" } } } \ No newline at end of file diff --git a/Oqtane.Application/AppHost/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css b/Oqtane.Application/Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css rename to Oqtane.Application/Server/wwwroot/Modules/Oqtane.Modules.Admin.Login/Module.css diff --git a/Oqtane.Application/AppHost/wwwroot/Modules/Oqtane.Modules.HtmlText/Module.css b/Oqtane.Application/Server/wwwroot/Modules/Oqtane.Modules.HtmlText/Module.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/Modules/Oqtane.Modules.HtmlText/Module.css rename to Oqtane.Application/Server/wwwroot/Modules/Oqtane.Modules.HtmlText/Module.css diff --git a/Oqtane.Application/AppHost/wwwroot/Oqtane.Server.lib.module.js b/Oqtane.Application/Server/wwwroot/Oqtane.Server.lib.module.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/Oqtane.Server.lib.module.js rename to Oqtane.Application/Server/wwwroot/Oqtane.Server.lib.module.js diff --git a/Oqtane.Application/AppHost/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css b/Oqtane.Application/Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css rename to Oqtane.Application/Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css diff --git a/Oqtane.Application/AppHost/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css b/Oqtane.Application/Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css rename to Oqtane.Application/Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css diff --git a/Oqtane.Application/Server/wwwroot/_content/Placeholder.txt b/Oqtane.Application/Server/wwwroot/_content/Placeholder.txt deleted file mode 100644 index 5a324d79..00000000 --- a/Oqtane.Application/Server/wwwroot/_content/Placeholder.txt +++ /dev/null @@ -1,11 +0,0 @@ -The _content folder should only contain static resources from shared razor component libraries (RCLs). Static resources can be extracted from shared RCL Nuget packages by executing a Publish task on the module's Server project to a local folder and copying the files from the _content folder which is created. Each shared RCL would have its own appropriately named subfolder within the module's _content folder. - -ie. - -/_content - /Radzen.Blazor - /css - /fonts - /syncfusion.blazor - /scripts - /styles diff --git a/Oqtane.Application/AppHost/wwwroot/app_offline.bak b/Oqtane.Application/Server/wwwroot/app_offline.bak similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/app_offline.bak rename to Oqtane.Application/Server/wwwroot/app_offline.bak diff --git a/Oqtane.Application/AppHost/wwwroot/css/app.css b/Oqtane.Application/Server/wwwroot/css/app.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/app.css rename to Oqtane.Application/Server/wwwroot/css/app.css diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/FONT-LICENSE b/Oqtane.Application/Server/wwwroot/css/open-iconic/FONT-LICENSE similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/FONT-LICENSE rename to Oqtane.Application/Server/wwwroot/css/open-iconic/FONT-LICENSE diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/ICON-LICENSE b/Oqtane.Application/Server/wwwroot/css/open-iconic/ICON-LICENSE similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/ICON-LICENSE rename to Oqtane.Application/Server/wwwroot/css/open-iconic/ICON-LICENSE diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/README.md b/Oqtane.Application/Server/wwwroot/css/open-iconic/README.md similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/README.md rename to Oqtane.Application/Server/wwwroot/css/open-iconic/README.md diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.eot b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.eot rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.eot diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.otf b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.otf rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.otf diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.svg b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.svg rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.svg diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf diff --git a/Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.woff b/Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/open-iconic/font/fonts/open-iconic.woff rename to Oqtane.Application/Server/wwwroot/css/open-iconic/font/fonts/open-iconic.woff diff --git a/Oqtane.Application/AppHost/wwwroot/css/quill/quill.bubble.css b/Oqtane.Application/Server/wwwroot/css/quill/quill.bubble.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/quill/quill.bubble.css rename to Oqtane.Application/Server/wwwroot/css/quill/quill.bubble.css diff --git a/Oqtane.Application/AppHost/wwwroot/css/quill/quill.snow.css b/Oqtane.Application/Server/wwwroot/css/quill/quill.snow.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/quill/quill.snow.css rename to Oqtane.Application/Server/wwwroot/css/quill/quill.snow.css diff --git a/Oqtane.Application/AppHost/wwwroot/css/quill/quill1.3.7.bubble.css b/Oqtane.Application/Server/wwwroot/css/quill/quill1.3.7.bubble.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/quill/quill1.3.7.bubble.css rename to Oqtane.Application/Server/wwwroot/css/quill/quill1.3.7.bubble.css diff --git a/Oqtane.Application/AppHost/wwwroot/css/quill/quill1.3.7.snow.css b/Oqtane.Application/Server/wwwroot/css/quill/quill1.3.7.snow.css similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/css/quill/quill1.3.7.snow.css rename to Oqtane.Application/Server/wwwroot/css/quill/quill1.3.7.snow.css diff --git a/Oqtane.Application/AppHost/wwwroot/favicon.ico b/Oqtane.Application/Server/wwwroot/favicon.ico similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/favicon.ico rename to Oqtane.Application/Server/wwwroot/favicon.ico diff --git a/Oqtane.Application/AppHost/wwwroot/icon.png b/Oqtane.Application/Server/wwwroot/icon.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/icon.png rename to Oqtane.Application/Server/wwwroot/icon.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/checked.png b/Oqtane.Application/Server/wwwroot/images/checked.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/checked.png rename to Oqtane.Application/Server/wwwroot/images/checked.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/disabled.png b/Oqtane.Application/Server/wwwroot/images/disabled.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/disabled.png rename to Oqtane.Application/Server/wwwroot/images/disabled.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/error.png b/Oqtane.Application/Server/wwwroot/images/error.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/error.png rename to Oqtane.Application/Server/wwwroot/images/error.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/help.png b/Oqtane.Application/Server/wwwroot/images/help.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/help.png rename to Oqtane.Application/Server/wwwroot/images/help.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/logo-black.png b/Oqtane.Application/Server/wwwroot/images/logo-black.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/logo-black.png rename to Oqtane.Application/Server/wwwroot/images/logo-black.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/logo-white.png b/Oqtane.Application/Server/wwwroot/images/logo-white.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/logo-white.png rename to Oqtane.Application/Server/wwwroot/images/logo-white.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/null.png b/Oqtane.Application/Server/wwwroot/images/null.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/null.png rename to Oqtane.Application/Server/wwwroot/images/null.png diff --git a/Oqtane.Application/AppHost/wwwroot/images/unchecked.png b/Oqtane.Application/Server/wwwroot/images/unchecked.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/images/unchecked.png rename to Oqtane.Application/Server/wwwroot/images/unchecked.png diff --git a/Oqtane.Application/AppHost/wwwroot/js/app.js b/Oqtane.Application/Server/wwwroot/js/app.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/app.js rename to Oqtane.Application/Server/wwwroot/js/app.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/interop.js b/Oqtane.Application/Server/wwwroot/js/interop.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/interop.js rename to Oqtane.Application/Server/wwwroot/js/interop.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/loadjs.min.js b/Oqtane.Application/Server/wwwroot/js/loadjs.min.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/loadjs.min.js rename to Oqtane.Application/Server/wwwroot/js/loadjs.min.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/quill-blot-formatter.min.js b/Oqtane.Application/Server/wwwroot/js/quill-blot-formatter.min.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/quill-blot-formatter.min.js rename to Oqtane.Application/Server/wwwroot/js/quill-blot-formatter.min.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/quill-interop.js b/Oqtane.Application/Server/wwwroot/js/quill-interop.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/quill-interop.js rename to Oqtane.Application/Server/wwwroot/js/quill-interop.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/quill.min.js b/Oqtane.Application/Server/wwwroot/js/quill.min.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/quill.min.js rename to Oqtane.Application/Server/wwwroot/js/quill.min.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/quill.min.js.map b/Oqtane.Application/Server/wwwroot/js/quill.min.js.map similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/quill.min.js.map rename to Oqtane.Application/Server/wwwroot/js/quill.min.js.map diff --git a/Oqtane.Application/AppHost/wwwroot/js/quill1.3.7.min.js b/Oqtane.Application/Server/wwwroot/js/quill1.3.7.min.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/quill1.3.7.min.js rename to Oqtane.Application/Server/wwwroot/js/quill1.3.7.min.js diff --git a/Oqtane.Application/AppHost/wwwroot/js/reload.js b/Oqtane.Application/Server/wwwroot/js/reload.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/js/reload.js rename to Oqtane.Application/Server/wwwroot/js/reload.js diff --git a/Oqtane.Application/AppHost/wwwroot/loading.gif b/Oqtane.Application/Server/wwwroot/loading.gif similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/loading.gif rename to Oqtane.Application/Server/wwwroot/loading.gif diff --git a/Oqtane.Application/AppHost/wwwroot/oqtane-black.png b/Oqtane.Application/Server/wwwroot/oqtane-black.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/oqtane-black.png rename to Oqtane.Application/Server/wwwroot/oqtane-black.png diff --git a/Oqtane.Application/AppHost/wwwroot/oqtane-glow.png b/Oqtane.Application/Server/wwwroot/oqtane-glow.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/oqtane-glow.png rename to Oqtane.Application/Server/wwwroot/oqtane-glow.png diff --git a/Oqtane.Application/AppHost/wwwroot/oqtane-white.png b/Oqtane.Application/Server/wwwroot/oqtane-white.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/oqtane-white.png rename to Oqtane.Application/Server/wwwroot/oqtane-white.png diff --git a/Oqtane.Application/AppHost/wwwroot/oqtane.ico b/Oqtane.Application/Server/wwwroot/oqtane.ico similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/oqtane.ico rename to Oqtane.Application/Server/wwwroot/oqtane.ico diff --git a/Oqtane.Application/AppHost/wwwroot/oqtane.png b/Oqtane.Application/Server/wwwroot/oqtane.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/oqtane.png rename to Oqtane.Application/Server/wwwroot/oqtane.png diff --git a/Oqtane.Application/AppHost/wwwroot/package.png b/Oqtane.Application/Server/wwwroot/package.png similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/package.png rename to Oqtane.Application/Server/wwwroot/package.png diff --git a/Oqtane.Application/AppHost/wwwroot/service-worker.js b/Oqtane.Application/Server/wwwroot/service-worker.js similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/service-worker.js rename to Oqtane.Application/Server/wwwroot/service-worker.js diff --git a/Oqtane.Application/AppHost/wwwroot/users.txt b/Oqtane.Application/Server/wwwroot/users.txt similarity index 100% rename from Oqtane.Application/AppHost/wwwroot/users.txt rename to Oqtane.Application/Server/wwwroot/users.txt diff --git a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj index 865461b1..90fcda5d 100644 --- a/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj +++ b/Oqtane.Application/Shared/Oqtane.Application.Shared.csproj @@ -1,13 +1,13 @@ - - net9.0 - 1.0.0 - Oqtane.Application.Shared.Oqtane - + + net9.0 + 1.0.0 + Oqtane.Application.Shared.Oqtane + - - - + + + From 9e85b35498592a0d1ebb9a931b4ed158dd968011 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Tue, 26 Aug 2025 17:22:10 -0400 Subject: [PATCH 06/25] fix naming --- Oqtane.Application/Oqtane.Application.Template.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Application/Oqtane.Application.Template.nuspec b/Oqtane.Application/Oqtane.Application.Template.nuspec index 964286a2..31d80f81 100644 --- a/Oqtane.Application/Oqtane.Application.Template.nuspec +++ b/Oqtane.Application/Oqtane.Application.Template.nuspec @@ -3,7 +3,7 @@ Oqtane.Application.Template 6.1.6 - Oqtane Application Solution For Blazor + Oqtane Application Template For Blazor Shaun Walker false MIT From 581f14e66163750227ac5d54b322599520714aaf Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 09:21:19 -0400 Subject: [PATCH 07/25] default index component to interactive --- Oqtane.Application/Client/Modules/MyModule/Index.razor | 2 -- 1 file changed, 2 deletions(-) diff --git a/Oqtane.Application/Client/Modules/MyModule/Index.razor b/Oqtane.Application/Client/Modules/MyModule/Index.razor index 1987a19a..fc3744d5 100644 --- a/Oqtane.Application/Client/Modules/MyModule/Index.razor +++ b/Oqtane.Application/Client/Modules/MyModule/Index.razor @@ -38,8 +38,6 @@ else } @code { - public override string RenderMode => RenderModes.Static; - public override List Resources => new List() { new Stylesheet(ModulePath() + "Module.css"), From 7cf9d9ad6573d867c16284d2dc07307d0cbf019a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 12:20:59 -0400 Subject: [PATCH 08/25] optimize client startup in templates --- Oqtane.Application/Client/Startup/ClientStartup.cs | 5 ++++- .../Templates/External/Client/Startup/ClientStartup.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Oqtane.Application/Client/Startup/ClientStartup.cs b/Oqtane.Application/Client/Startup/ClientStartup.cs index 1cdbd905..cf29f81d 100644 --- a/Oqtane.Application/Client/Startup/ClientStartup.cs +++ b/Oqtane.Application/Client/Startup/ClientStartup.cs @@ -8,7 +8,10 @@ namespace Oqtane.Application.Startup { public void ConfigureServices(IServiceCollection services) { - services.AddScoped(); + if (!services.Any(s => s.ServiceType == typeof(IMyModuleService))) + { + services.AddScoped(); + } } } } diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs index 611b5a8e..5050abee 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs @@ -8,7 +8,10 @@ namespace [Owner].Module.[Module].Startup { public void ConfigureServices(IServiceCollection services) { - services.AddScoped(); + if (!services.Any(s => s.ServiceType == typeof(I[Module]Service))) + { + services.AddScoped(); + } } } } From 9a6195edf1ef4278f7f852814af95b5e1fb13c62 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 12:28:51 -0400 Subject: [PATCH 09/25] fix resources in default theme template --- .../wwwroot/Themes/Templates/External/Client/ThemeInfo.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs index 4dea5a8f..6ceaa574 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -16,11 +16,10 @@ namespace [Owner].Theme.[Theme] ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane", Resources = new List() { - // obtained from https://cdnjs.com/libraries - new StyleSheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), - new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, + // obtained from https://cdnjs.com/libraries + new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), + new Stylesheet("~/Theme.css"), new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") - } }; From edad9e6b3c07bfea45577b4b07325b1cc22728f0 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 13:54:30 -0400 Subject: [PATCH 10/25] fix #5531 - external login single sign-on for multiple sites --- ...taneSiteAuthenticationBuilderExtensions.cs | 67 ++++++++++++------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index f142c602..9bbe4e41 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -476,8 +476,26 @@ namespace Oqtane.Extensions else { var logins = await _identityUserManager.GetLoginsAsync(identityuser); - var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString())); - if (login == null) + // check if any logins exist for this user and provider type for any site + var login = logins.FirstOrDefault(item => item.LoginProvider.StartsWith(providerType)); + if (login != null || !bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true"))) + { + // external login using existing user account - link automatically + user = _users.GetUser(identityuser.UserName); + user.SiteId = alias.SiteId; + + var _notifications = httpContext.RequestServices.GetRequiredService(); + string url = httpContext.Request.Scheme + "://" + alias.Name; + string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Account Notification", body); + _notifications.AddNotification(notification); + + // add user login + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); + + _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName); + } + else { if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:VerifyUsers", "true"))) { @@ -496,28 +514,11 @@ namespace Oqtane.Extensions } else { - // external login using existing user account - link automatically - user = _users.GetUser(identityuser.UserName); - user.SiteId = alias.SiteId; - - var _notifications = httpContext.RequestServices.GetRequiredService(); - string url = httpContext.Request.Scheme + "://" + alias.Name; - string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!"; - var notification = new Notification(user.SiteId, user, "User Account Notification", body); - _notifications.AddNotification(notification); - - // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + user.SiteId.ToString(), id, providerName)); - - _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Created For User {Username} And Provider {Provider}", user.Username, providerName); + // provider keys do not match + identity.Label = ExternalLoginStatus.ProviderKeyMismatch; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); } } - else - { - // provider keys do not match - identity.Label = ExternalLoginStatus.ProviderKeyMismatch; - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); - } } } @@ -525,14 +526,34 @@ namespace Oqtane.Extensions if (user != null) { // manage roles + var _roles = httpContext.RequestServices.GetRequiredService(); var _userRoles = httpContext.RequestServices.GetRequiredService(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); + + // if user is signing in to a new site + if (userRoles.Count == 0) + { + // add auto assigned roles to user for site + var roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList(); + foreach (var role in roles) + { + var userrole = new UserRole(); + userrole.UserId = user.UserId; + userrole.RoleId = role.RoleId; + userrole.EffectiveDate = null; + userrole.ExpiryDate = null; + userrole.IgnoreSecurityStamp = true; + _userRoles.AddUserRole(userrole); + } + userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); + } + + // process any role claims if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) { // external roles if (claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) { - var _roles = httpContext.RequestServices.GetRequiredService(); var allowhostrole = bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:AllowHostRole", "false")); var roles = _roles.GetRoles(user.SiteId, allowhostrole).ToList(); From 3b16ae8cc061b864f2ff7a1a740ae973db6e8232 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 14:07:51 -0400 Subject: [PATCH 11/25] improve help text --- Oqtane.Client/Modules/Admin/Users/Index.razor | 27 +++++++++---------- .../Resources/Modules/Admin/Users/Index.resx | 2 +- ...taneSiteAuthenticationBuilderExtensions.cs | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index fa3dccf3..b3a7d623 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -314,6 +314,15 @@ else
+
+ +
+ +
+
}
@@ -413,18 +422,6 @@ else
- @if (_providertype == AuthenticationProviderTypes.OpenIDConnect) - { -
- -
- -
-
- }
@@ -555,6 +552,7 @@ else private string _clientsecrettype = "password"; private string _toggleclientsecret = string.Empty; private string _authresponsetype; + private string _requirenonce; private string _scopes; private string _parameters; private string _pkce; @@ -569,7 +567,6 @@ else private string _synchronizeroles; private string _profileclaimtypes; private string _savetokens; - private string _requirenonce; private string _domainfilter; private string _createusers; private string _verifyusers; @@ -642,6 +639,7 @@ else _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _toggleclientsecret = SharedLocalizer["ShowPassword"]; _authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code"); + _requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "true"); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", ""); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); @@ -656,7 +654,6 @@ else _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); _savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false"); - _requirenonce = SettingService.GetSetting(settings, "ExternalLogin:RequireNonce", "true"); _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); @@ -764,6 +761,7 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:RequireNonce", _requirenonce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); @@ -776,7 +774,6 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:SaveTokens", _savetokens, true); - settings = SettingService.SetSetting(settings, "ExternalLogin:RequireNonce", _requirenonce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index e6d07a27..8b2be8e7 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -517,7 +517,7 @@ Require Nonce? - Specify the RequireNonce property for OpenID Connect Authentication. + Specify if Nonce validation is required for the ID token (the default is true) Save Tokens? diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 143874ba..1a1a3687 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -61,9 +61,9 @@ namespace Oqtane.Extensions options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", ""); options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow + options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "true")); options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false")); - options.ProtocolValidator.RequireNonce = bool.Parse(sitesettings.GetValue("ExternalLogin:RequireNonce", "true")); if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) { options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); From 68a7571741370f4d750cae8a892628b2ad13bc83 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 27 Aug 2025 14:26:47 -0400 Subject: [PATCH 12/25] Change RenderMode from Interactive to Static --- Oqtane.Server/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/appsettings.json b/Oqtane.Server/appsettings.json index 46ae54ac..9f96daeb 100644 --- a/Oqtane.Server/appsettings.json +++ b/Oqtane.Server/appsettings.json @@ -1,5 +1,5 @@ { - "RenderMode": "Interactive", + "RenderMode": "Static", "Runtime": "Server", "Database": { "DefaultDBType": "" From 23f29ca55d086e5284709f43ae991ae4bafc5322 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 27 Aug 2025 14:27:01 -0400 Subject: [PATCH 13/25] Change RenderMode from Interactive to Static --- Oqtane.Server/appsettings.release.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Oqtane.Server/appsettings.release.json b/Oqtane.Server/appsettings.release.json index 9269136e..307008c8 100644 --- a/Oqtane.Server/appsettings.release.json +++ b/Oqtane.Server/appsettings.release.json @@ -1,5 +1,5 @@ { - "RenderMode": "Interactive", + "RenderMode": "Static", "Runtime": "Server", "Database": { "DefaultDBType": "" From 006423e32e3e9e360d66cfdacc571a596f837bb9 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Wed, 27 Aug 2025 14:27:28 -0400 Subject: [PATCH 14/25] Change RenderMode from Interactive to Static --- Oqtane.Application/Server/appsettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Application/Server/appsettings.json b/Oqtane.Application/Server/appsettings.json index 4da5bbc7..9f96daeb 100644 --- a/Oqtane.Application/Server/appsettings.json +++ b/Oqtane.Application/Server/appsettings.json @@ -1,5 +1,5 @@ { - "RenderMode": "Interactive", + "RenderMode": "Static", "Runtime": "Server", "Database": { "DefaultDBType": "" @@ -60,4 +60,4 @@ "Default": "Information" } } -} \ No newline at end of file +} From 2af02fae95e2253db0d2eb4185dc358a424ae2cf Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 27 Aug 2025 14:28:23 -0400 Subject: [PATCH 15/25] install wizard should use RenderMode and Runtime values from appsettings.json when creating site --- Oqtane.Client/Installer/Installer.razor | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 5d2a487a..56756a5a 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -269,8 +269,8 @@ SiteName = Constants.DefaultSite, Register = _register, SiteTemplate = _template, - RenderMode = RenderModes.Static, - Runtime = Runtimes.Server + RenderMode = "", // provided by appsettings.json + Runtime = "" // provided by appsettings.json }; var installation = await InstallationService.Install(config); From 6f7a18674e38c29480b52b37d9cc2b119c0cf85e Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 14:30:49 -0400 Subject: [PATCH 16/25] add support for packageType in nuspec files for minimum Oqtane version --- Oqtane.Server/Infrastructure/InstallationManager.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index 883997e6..8da91ec6 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -93,12 +93,21 @@ namespace Oqtane.Infrastructure { id = node.InnerText; } - // get framework dependency - node = doc.SelectSingleNode("/package/metadata/dependencies/dependency[@id='Oqtane.Framework']"); + // get minimum framework version using packageType + node = doc.SelectSingleNode("/package/metadata/packageTypes/packageType[@name='Oqtane.Framework']"); if (node != null) { frameworkversion = node.Attributes["version"].Value; } + if (string.IsNullOrEmpty(frameworkversion)) + { + // legacy packages used the dependency metadata + node = doc.SelectSingleNode("/package/metadata/dependencies/dependency[@id='Oqtane.Framework']"); + if (node != null) + { + frameworkversion = node.Attributes["version"].Value; + } + } reader.Close(); break; } From ab534d07f34cb4b53f0b179fdabb64df95263d74 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 15:14:48 -0400 Subject: [PATCH 17/25] update default module/theme templates to use projectType rather than dependency in nuspec file --- .../External/Package/[Owner].Module.[Module].nuspec | 7 ++++--- .../External/Package/[Owner].Theme.[Theme].nuspec | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec index 67af3899..9df56e87 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec @@ -15,9 +15,10 @@ oqtane module - - - + + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec index 40f5f3b3..16777464 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec @@ -15,9 +15,10 @@ oqtane theme - - - + + + + From 0067cc426648fc68eb10fb8a7f6213e365ac9365 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 29 Aug 2025 15:19:07 -0400 Subject: [PATCH 18/25] Added FixProps command line utility --- Oqtane.Package/FixProps.exe | Bin 0 -> 126943 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 Oqtane.Package/FixProps.exe diff --git a/Oqtane.Package/FixProps.exe b/Oqtane.Package/FixProps.exe new file mode 100644 index 0000000000000000000000000000000000000000..58d79183ebabc2ea2d6a605c681ba19350d08048 GIT binary patch literal 126943 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 CHidBj literal 0 HcmV?d00001 From d2d88d4b5e30fa5cd099b1798cb70935e61903aa Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 16:20:16 -0400 Subject: [PATCH 19/25] improve default module template to follow RCL/Nuget standards --- .../Client/Modules/[Owner].Module.[Module]/Edit.razor | 2 +- .../Client/Modules/[Owner].Module.[Module]/Index.razor | 4 ++-- .../External/Package/[Owner].Module.[Module].nuspec | 7 ++++++- .../wwwroot/Modules/Templates/External/Package/debug.cmd | 2 +- .../wwwroot/Modules/Templates/External/Package/debug.sh | 2 +- .../wwwroot/Modules/Templates/External/Package/release.cmd | 1 + .../wwwroot/Modules/Templates/External/Package/release.sh | 2 ++ 7 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor index 23ca8f4f..a3d25ab5 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Edit.razor @@ -35,7 +35,7 @@ public override List Resources => new List() { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" } + new Stylesheet("_content/[Owner].Module.[Module]/Module.css") }; private ElementReference form; diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor index 4c68ada6..6f43dcb3 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Index.razor @@ -42,8 +42,8 @@ else public override List Resources => new List() { - new Resource { ResourceType = ResourceType.Stylesheet, Url = ModulePath() + "Module.css" }, - new Resource { ResourceType = ResourceType.Script, Url = ModulePath() + "Module.js" } + new Stylesheet("_content/[Owner].Module.[Module]/Module.css"), + new Script("_content/[Owner].Module.[Module]/Module.js") }; List<[Module]> _[Module]s; diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec index 9df56e87..bf919a3d 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/[Owner].Module.[Module].nuspec @@ -27,7 +27,12 @@ - + + + + + + \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.cmd b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.cmd index af7654c7..35093663 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.cmd +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.cmd @@ -8,4 +8,4 @@ XCOPY "..\Server\bin\Debug\%TargetFramework%\%ProjectName%.Server.Oqtane.dll" ". XCOPY "..\Server\bin\Debug\%TargetFramework%\%ProjectName%.Server.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\%TargetFramework%\" /Y XCOPY "..\Shared\bin\Debug\%TargetFramework%\%ProjectName%.Shared.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\%TargetFramework%\" /Y XCOPY "..\Shared\bin\Debug\%TargetFramework%\%ProjectName%.Shared.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\%TargetFramework%\" /Y -XCOPY "..\Server\wwwroot\*" "..\..\[RootFolder]\Oqtane.Server\wwwroot\" /Y /S /I \ No newline at end of file +XCOPY "..\Server\wwwroot\*" "..\..\[RootFolder]\Oqtane.Server\wwwroot\_content\%ProjectName%\" /Y /S /I \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh index bcdee757..74703939 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh @@ -9,4 +9,4 @@ cp -f "../Server/bin/Debug/$TargetFramework/$ProjectName$.Server.Oqtane.dll" ".. cp -f "../Server/bin/Debug/$TargetFramework/$ProjectName$.Server.Oqtane.pdb" "../../[RootFolder]/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName$.Shared.Oqtane.dll" "../../[RootFolder]/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Shared/bin/Debug/$TargetFramework/$ProjectName$.Shared.Oqtane.pdb" "../../[RootFolder]/Oqtane.Server/bin/Debug/$TargetFramework/" -cp -rf "../Server/wwwroot/"* "../../[RootFolder]/Oqtane.Server/wwwroot/" \ No newline at end of file +cp -rf "../Server/wwwroot/"* "../../[RootFolder]/Oqtane.Server/wwwroot/_content/%ProjectName%/" diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd index 1785fa66..5b49099f 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.cmd @@ -3,5 +3,6 @@ set TargetFramework=%1 set ProjectName=%2 del "*.nupkg" +"..\..\oqtane.framework\oqtane.package\FixProps.exe" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName% XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh index 1334e6a7..98526bc0 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh @@ -1,5 +1,7 @@ TargetFramework=$1 ProjectName=$2 +find . -name "*.nupkg" -delete +"..\..\oqtane.framework\oqtane.package\FixProps.exe" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName% cp -f "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" \ No newline at end of file From e4b6d0ff2982244370df7743c7d6ac227f9d0ba4 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 16:30:49 -0400 Subject: [PATCH 20/25] improve default theme template to follow RCL/Nuget standards --- .../wwwroot/Themes/Templates/External/Client/ThemeInfo.cs | 2 +- .../External/Package/[Owner].Theme.[Theme].nuspec | 7 ++++++- .../wwwroot/Themes/Templates/External/Package/debug.cmd | 2 +- .../wwwroot/Themes/Templates/External/Package/debug.sh | 2 +- .../wwwroot/Themes/Templates/External/Package/release.cmd | 1 + .../wwwroot/Themes/Templates/External/Package/release.sh | 2 ++ 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs index 6ceaa574..3d007129 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs @@ -18,7 +18,7 @@ namespace [Owner].Theme.[Theme] { // obtained from https://cdnjs.com/libraries new Stylesheet(Constants.BootstrapStylesheetUrl, Constants.BootstrapStylesheetIntegrity, "anonymous"), - new Stylesheet("~/Theme.css"), + new Stylesheet("_content/[Owner].Theme.[Theme]/Theme.css"), new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") } }; diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec index 16777464..363ebaa9 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/[Owner].Theme.[Theme].nuspec @@ -23,7 +23,12 @@ - + + + + + + \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd index 196fb916..e7073b94 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.cmd @@ -4,4 +4,4 @@ set ProjectName=%2 XCOPY "..\Client\bin\Debug\%TargetFramework%\%ProjectName%.Client.Oqtane.dll" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\%TargetFramework%\" /Y XCOPY "..\Client\bin\Debug\%TargetFramework%\%ProjectName%.Client.Oqtane.pdb" "..\..\[RootFolder]\Oqtane.Server\bin\Debug\%TargetFramework%\" /Y -XCOPY "..\Client\wwwroot\*" "..\..\[RootFolder]\Oqtane.Server\wwwroot\" /Y /S /I \ No newline at end of file +XCOPY "..\Client\wwwroot\*" "..\..\oqtane.framework\Oqtane.Server\wwwroot\_content\%ProjectName%\" /Y /S /I \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh index 0caca359..47311fc8 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh @@ -5,4 +5,4 @@ ProjectName=$2 cp -f "../Client/bin/Debug/$TargetFramework/$ProjectName$.Client.Oqtane.dll" "../../[RootFolder]/Oqtane.Server/bin/Debug/$TargetFramework/" cp -f "../Client/bin/Debug/$TargetFramework/$ProjectName$.Client.Oqtane.pdb" "../../[RootFolder]/Oqtane.Server/bin/Debug/$TargetFramework/" -cp -rf "../Server/wwwroot/"* "../../[RootFolder]/Oqtane.Server/wwwroot/" \ No newline at end of file +cp -rf "../Client/wwwroot/"* "../../[RootFolder]/Oqtane.Server/wwwroot/_content/%ProjectName%/" \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd index 1785fa66..31809574 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd @@ -3,5 +3,6 @@ set TargetFramework=%1 set ProjectName=%2 del "*.nupkg" +"..\..\[RootFolder]\oqtane.package\FixProps.exe" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName% XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y \ No newline at end of file diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh index 1334e6a7..98526bc0 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh @@ -1,5 +1,7 @@ TargetFramework=$1 ProjectName=$2 +find . -name "*.nupkg" -delete +"..\..\oqtane.framework\oqtane.package\FixProps.exe" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName% cp -f "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" \ No newline at end of file From 713ec1b373b234a7801aeed2630377ff63195b8a Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 16:33:51 -0400 Subject: [PATCH 21/25] move default template static assets --- .../[Owner].Module.[Module] => }/Module.css | 0 .../[Owner].Module.[Module] => }/Module.js | 0 .../Server/wwwroot/_content/Placeholder.txt | 11 -- .../Themes/[Owner].Theme.[Theme]/Theme.css | 123 ------------------ 4 files changed, 134 deletions(-) rename Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/{Modules/[Owner].Module.[Module] => }/Module.css (100%) rename Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/{Modules/[Owner].Module.[Module] => }/Module.js (100%) delete mode 100644 Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/_content/Placeholder.txt delete mode 100644 Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Modules/[Owner].Module.[Module]/Module.css b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Module.css similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Modules/[Owner].Module.[Module]/Module.css rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Module.css diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Modules/[Owner].Module.[Module]/Module.js b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Module.js similarity index 100% rename from Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Modules/[Owner].Module.[Module]/Module.js rename to Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/Module.js diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/_content/Placeholder.txt b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/_content/Placeholder.txt deleted file mode 100644 index 5a324d79..00000000 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/wwwroot/_content/Placeholder.txt +++ /dev/null @@ -1,11 +0,0 @@ -The _content folder should only contain static resources from shared razor component libraries (RCLs). Static resources can be extracted from shared RCL Nuget packages by executing a Publish task on the module's Server project to a local folder and copying the files from the _content folder which is created. Each shared RCL would have its own appropriately named subfolder within the module's _content folder. - -ie. - -/_content - /Radzen.Blazor - /css - /fonts - /syncfusion.blazor - /scripts - /styles diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css deleted file mode 100644 index 2a6101ef..00000000 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/wwwroot/Themes/[Owner].Theme.[Theme]/Theme.css +++ /dev/null @@ -1,123 +0,0 @@ -/* Oqtane Styles */ - -body { - padding-top: 7rem; -} - -/* App Logo */ -.app-logo .img-fluid { - max-height: 90px; - padding: 0 5px 0 5px; -} - -.table > :not(caption) > * > * { - box-shadow: none; -} - -.table .form-control { - background-color: #ffffff !important; - border-width: 0.5px !important; - border-bottom-color: #ccc !important; -} - -.table .form-select { - background-color: #ffffff !important; - border-width: 0.5px !important; - border-bottom-color: #ccc !important; -} - -.table .btn-primary { - background-color: var(--bs-primary); -} - -.table .btn-secondary { - background-color: var(--bs-secondary); -} - -.alert-dismissible .btn-close { - z-index: 1; -} - -.controls { - z-index: 2000; - padding-top: 15px; - padding-bottom: 15px; - margin-right: 10px; -} - -.app-menu .nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; - white-space: nowrap; -} - -.app-menu .nav-item a { - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - padding-left: 1rem; -} - -.app-menu .nav-item a.active { - background-color: rgba(255,255,255,0.25); - color: white; -} - -.app-menu .nav-item a:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -.app-menu .nav-link .oi { - width: 1.5rem; - font-size: 1.1rem; - vertical-align: text-top; - top: -2px; -} - -.navbar-toggler { - background-color: rgba(255, 255, 255, 0.1); - margin: .5rem; -} - -div.app-moduleactions a.dropdown-toggle, div.app-moduleactions div.dropdown-menu { - color: #000000; -} - -.dropdown-menu span { - mix-blend-mode: difference; -} - -@media (max-width: 767.98px) { - - .app-menu { - width: 100%; - } - - .navbar { - position: fixed; - top: 60px; - width: 100%; - } - - .controls { - height: 60px; - top: 15px; - position: fixed; - top: 0px; - width: 100%; - background-color: rgb(0, 0, 0); - } - - .controls-group { - float: right; - margin-right: 25px; - } - - .content { - position: relative; - top: 60px; - } -} From a9bc356f37c3ef3dcc666b9c60a64811b15da42f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Fri, 29 Aug 2025 17:16:42 -0400 Subject: [PATCH 22/25] remove hardcoded references to LocalDB --- Oqtane.Client/Installer/Installer.razor | 2 +- Oqtane.Client/Modules/Admin/Site/Index.razor | 3 ++- Oqtane.Client/Modules/Admin/Sites/Add.razor | 2 +- Oqtane.Client/Modules/Admin/Sql/Index.razor | 5 +++-- Oqtane.Shared/Shared/Constants.cs | 1 + 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index 56756a5a..c5becbb0 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -182,7 +182,7 @@ } else { - _databaseName = "LocalDB"; + _databaseName = Constants.DefaultDBName; } LoadDatabaseConfigComponent(); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index d76f56a5..b921ab38 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -665,7 +665,8 @@ if (tenant != null) { _tenant = tenant.Name; - _database = _databases.Find(item => item.DBType == tenant.DBType && item.Name != "LocalDB")?.Name; + // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order + _database = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name; _connectionstring = tenant.DBConnectionString; } } diff --git a/Oqtane.Client/Modules/Admin/Sites/Add.razor b/Oqtane.Client/Modules/Admin/Sites/Add.razor index 459e5143..96949077 100644 --- a/Oqtane.Client/Modules/Admin/Sites/Add.razor +++ b/Oqtane.Client/Modules/Admin/Sites/Add.razor @@ -237,7 +237,7 @@ else } else { - _databaseName = "LocalDB"; + _databaseName = Constants.DefaultDBName; } LoadDatabaseConfigComponent(); } diff --git a/Oqtane.Client/Modules/Admin/Sql/Index.razor b/Oqtane.Client/Modules/Admin/Sql/Index.razor index 476ebd1e..bf2eefec 100644 --- a/Oqtane.Client/Modules/Admin/Sql/Index.razor +++ b/Oqtane.Client/Modules/Admin/Sql/Index.razor @@ -200,7 +200,8 @@ else if (tenant != null) { _tenant = tenant.Name; - _databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType && item.Name != "LocalDB").Name; + // hack - there are 3 providers with SqlServerDatabase DBTypes - so we are choosing the last one in alphabetical order + _databasetype = _databases.Where(item => item.DBType == tenant.DBType).OrderBy(item => item.Name).Last()?.Name; } } else @@ -211,7 +212,7 @@ else } else { - _databasetype = "LocalDB"; + _databasetype = Constants.DefaultDBName; } _showConnectionString = false; LoadDatabaseConfigComponent(); diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index bf30580b..0348e7c2 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -12,6 +12,7 @@ namespace Oqtane.Shared public const string PackageRegistryUrl = "https://www.oqtane.net"; public const string DataDirectory = "DataDirectory"; + public const string DefaultDBName = "LocalDB"; public const string DefaultDBType = "Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Server"; public const string DefaultTheme = "Oqtane.Themes.OqtaneTheme.Default, Oqtane.Client"; From efa466e1d654dd3169961716fe6a6bc5c3015266 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sat, 30 Aug 2025 07:26:37 -0400 Subject: [PATCH 23/25] add comments --- Oqtane.Application/Client/Program.cs | 1 + Oqtane.Application/Server/Program.cs | 1 + Oqtane.Application/Server/Startup.cs | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Oqtane.Application/Client/Program.cs b/Oqtane.Application/Client/Program.cs index 3d0820e2..0dfa0b4a 100644 --- a/Oqtane.Application/Client/Program.cs +++ b/Oqtane.Application/Client/Program.cs @@ -6,6 +6,7 @@ namespace Oqtane.Application.Client { static async Task Main(string[] args) { + // defer client startup to Oqtane - do not modify await Oqtane.Client.Program.Main(args); } } diff --git a/Oqtane.Application/Server/Program.cs b/Oqtane.Application/Server/Program.cs index 9cbd89e5..55a18e50 100644 --- a/Oqtane.Application/Server/Program.cs +++ b/Oqtane.Application/Server/Program.cs @@ -11,6 +11,7 @@ namespace Oqtane.Application.Server { public static void Main(string[] args) { + // defer server startup to Oqtane - do not modify var host = BuildWebHost(args); var databaseManager = host.Services.GetService(); var install = databaseManager.Install(); diff --git a/Oqtane.Application/Server/Startup.cs b/Oqtane.Application/Server/Startup.cs index da1bc828..71f5bd19 100644 --- a/Oqtane.Application/Server/Startup.cs +++ b/Oqtane.Application/Server/Startup.cs @@ -32,11 +32,13 @@ namespace Oqtane.Application.Server public void ConfigureServices(IServiceCollection services) { + // defer server startup to Oqtane - do not modify services.AddOqtane(_configuration, _environment); } public void Configure(IApplicationBuilder app, IConfigurationRoot configuration, IWebHostEnvironment environment, ICorsService corsService, ICorsPolicyProvider corsPolicyProvider, ISyncManager sync) { + // defer server startup to Oqtane - do not modify app.UseOqtane(configuration, environment, corsService, corsPolicyProvider, sync); } } From 1ebf3c4077e259e467d4071da9aae1595014aade Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sat, 30 Aug 2025 07:48:26 -0400 Subject: [PATCH 24/25] added StaticAssetPath properties to base classes --- Oqtane.Client/Modules/ModuleBase.cs | 11 ++++++++++- Oqtane.Client/Themes/ThemeBase.cs | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 94cef77c..782a4b0a 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -140,13 +140,22 @@ namespace Oqtane.Modules } } - // path method + // path methods public string ModulePath() { return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/"; } + public string StaticAssetPath + { + get + { + // requires module to have implemented IModule + return PageState?.Alias.BaseUrl + "_content/" + ModuleState.ModuleDefinition?.PackageName + "/"; + } + } + // fingerprint hash code for static assets public string Fingerprint diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index 4359f48e..d66ddb76 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -108,13 +108,22 @@ namespace Oqtane.Themes } } - // path method + // path methods public string ThemePath() { return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/"; } + public string StaticAssetPath + { + get + { + // requires theme to have implemented ITheme + return PageState?.Alias.BaseUrl + "_content/" + ThemeState?.PackageName + "/"; + } + } + // fingerprint hash code for static assets public string Fingerprint { From 6daf675e5299e27f8c436fa83b9b6c36511ded6f Mon Sep 17 00:00:00 2001 From: sbwalker Date: Sat, 30 Aug 2025 08:01:18 -0400 Subject: [PATCH 25/25] added support for cookie domain option in User Management Settings --- Oqtane.Client/Modules/Admin/Users/Index.razor | 9 +++++++++ Oqtane.Client/Resources/Modules/Admin/Users/Index.resx | 6 ++++++ .../OqtaneSiteAuthenticationBuilderExtensions.cs | 4 ++++ 3 files changed, 19 insertions(+) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index b3a7d623..0e76dc49 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -114,6 +114,12 @@ else
+
+ +
+ +
+
@@ -525,6 +531,7 @@ else private string _requireconfirmedemail; private string _twofactor; private string _cookiename; + private string _cookiedomain; private string _cookieexpiration; private string _alwaysremember; private string _logouteverywhere; @@ -600,6 +607,7 @@ else { _twofactor = SettingService.GetSetting(settings, "LoginOptions:TwoFactor", "false"); _cookiename = SettingService.GetSetting(settings, "LoginOptions:CookieName", ".AspNetCore.Identity.Application"); + _cookiedomain = SettingService.GetSetting(settings, "LoginOptions:CookieDomain", ""); _cookieexpiration = SettingService.GetSetting(settings, "LoginOptions:CookieExpiration", ""); _alwaysremember = SettingService.GetSetting(settings, "LoginOptions:AlwaysRemember", "false"); _logouteverywhere = SettingService.GetSetting(settings, "LoginOptions:LogoutEverywhere", "false"); @@ -736,6 +744,7 @@ else settings = SettingService.SetSetting(settings, "LoginOptions:RequireConfirmedEmail", _requireconfirmedemail, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); + settings = SettingService.SetSetting(settings, "LoginOptions:CookieDomain", _cookiedomain, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false); settings = SettingService.SetSetting(settings, "LoginOptions:LogoutEverywhere", _logouteverywhere, false); diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 8b2be8e7..e9bc7d13 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -549,4 +549,10 @@ Deleted Users + + Cookie Domain: + + + If you would like to share cookies across subdomains you will need to specify a root domain with a leading dot (ie. '.example.com') + \ No newline at end of file diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 1a1a3687..8c8f1990 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -31,6 +31,10 @@ namespace Oqtane.Extensions builder.AddSiteNamedOptions(Constants.AuthenticationScheme, (options, alias, sitesettings) => { options.Cookie.Name = sitesettings.GetValue("LoginOptions:CookieName", ".AspNetCore.Identity.Application"); + if (!string.IsNullOrEmpty(sitesettings.GetValue("LoginOptions:CookieDomain", ""))) + { + options.Cookie.Domain = sitesettings.GetValue("LoginOptions:CookieDomain", ""); + } string cookieExpStr = sitesettings.GetValue("LoginOptions:CookieExpiration", ""); if (!string.IsNullOrEmpty(cookieExpStr) && TimeSpan.TryParse(cookieExpStr, out TimeSpan cookieExpTS)) {