@@ -516,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;
@@ -543,6 +559,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;
@@ -590,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");
@@ -629,6 +647,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");
@@ -725,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);
@@ -750,6 +770,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);
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/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
index 16e0d40e..e9bc7d13 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 if Nonce validation is required for the ID token (the default is true)
+
Save Tokens?
@@ -543,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.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
{
diff --git a/Oqtane.Package/FixProps.exe b/Oqtane.Package/FixProps.exe
new file mode 100644
index 00000000..58d79183
Binary files /dev/null and b/Oqtane.Package/FixProps.exe differ
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/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
index f142c602..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))
{
@@ -61,6 +65,7 @@ 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"));
if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", "")))
@@ -476,8 +481,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 +519,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 +531,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();
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;
}
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);
}
}
}
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": ""
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": ""
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/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();
+ }
}
}
}
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..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
@@ -15,9 +15,10 @@
oqtane module
-
-
-
+
+
+
+
@@ -26,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
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/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs
index 4dea5a8f..3d007129 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("_content/[Owner].Theme.[Theme]/Theme.css"),
new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous")
-
}
};
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;
- }
-}
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..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
@@ -15,14 +15,20 @@
oqtane theme
-
-
-
+
+
+
+
-
+
+
+
+
+
+
\ 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
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";