diff --git a/Oqtane.Maui/Oqtane.Maui.csproj b/Oqtane.Maui/Oqtane.Maui.csproj
index 2314ac0a..f2273569 100644
--- a/Oqtane.Maui/Oqtane.Maui.csproj
+++ b/Oqtane.Maui/Oqtane.Maui.csproj
@@ -18,7 +18,7 @@
com.oqtane.maui
-
10.0.1
+
10.0.2
1
diff --git a/Oqtane.Maui/wwwroot/css/app.css b/Oqtane.Maui/wwwroot/css/app.css
index 13c7d539..013ef348 100644
--- a/Oqtane.Maui/wwwroot/css/app.css
+++ b/Oqtane.Maui/wwwroot/css/app.css
@@ -273,6 +273,11 @@ app {
min-height: 250px;
}
+.app-editor-resizable {
+ resize: vertical;
+ overflow: auto;
+}
+
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}
diff --git a/Oqtane.Maui/wwwroot/js/interop.js b/Oqtane.Maui/wwwroot/js/interop.js
index fecc4c99..944f9812 100644
--- a/Oqtane.Maui/wwwroot/js/interop.js
+++ b/Oqtane.Maui/wwwroot/js/interop.js
@@ -124,7 +124,7 @@ Oqtane.Interop = {
}
},
includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) {
- var script;
+ var script = null;
if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]");
}
@@ -140,7 +140,7 @@ Oqtane.Interop = {
}
}
}
- if (script !== null) {
+ if (script instanceof HTMLScriptElement) {
script.remove();
script = null;
}
@@ -516,5 +516,17 @@ Oqtane.Interop = {
}
}
}
+ },
+ createCredential: async function (optionsResponse) {
+ const optionsJson = JSON.parse(optionsResponse);
+ const options = PublicKeyCredential.parseCreationOptionsFromJSON(optionsJson);
+ const credential = await navigator.credentials.create({ publicKey: options });
+ return JSON.stringify(credential);
+ },
+ requestCredential: async function (optionsResponse) {
+ const optionsJson = JSON.parse(optionsResponse);
+ const options = PublicKeyCredential.parseRequestOptionsFromJSON(optionsJson);
+ const credential = await navigator.credentials.get({ publicKey: options, undefined });
+ return JSON.stringify(credential);
}
};
diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec
index de21e194..ef0fa733 100644
--- a/Oqtane.Package/Oqtane.Client.nuspec
+++ b/Oqtane.Package/Oqtane.Client.nuspec
@@ -2,7 +2,7 @@
Oqtane.Client
- 10.0.1
+ 10.0.2
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -12,7 +12,7 @@
false
MIT
https://github.com/oqtane/oqtane.framework
- https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1
+ https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2
readme.md
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec
index 952bb267..19cd8031 100644
--- a/Oqtane.Package/Oqtane.Framework.nuspec
+++ b/Oqtane.Package/Oqtane.Framework.nuspec
@@ -2,7 +2,7 @@
Oqtane.Framework
- 10.0.1
+ 10.0.2
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -11,8 +11,8 @@
.NET Foundation
false
MIT
- https://github.com/oqtane/oqtane.framework/releases/download/v10.0.1/Oqtane.Framework.10.0.1.Upgrade.zip
- https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1
+ https://github.com/oqtane/oqtane.framework/releases/download/v10.0.2/Oqtane.Framework.10.0.2.Upgrade.zip
+ https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2
readme.md
icon.png
oqtane framework
diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec
index 47a0b13a..f24dc68f 100644
--- a/Oqtane.Package/Oqtane.Server.nuspec
+++ b/Oqtane.Package/Oqtane.Server.nuspec
@@ -2,7 +2,7 @@
Oqtane.Server
- 10.0.1
+ 10.0.2
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -12,7 +12,7 @@
false
MIT
https://github.com/oqtane/oqtane.framework
- https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1
+ https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2
readme.md
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec
index 78dfbf9d..07e816d0 100644
--- a/Oqtane.Package/Oqtane.Shared.nuspec
+++ b/Oqtane.Package/Oqtane.Shared.nuspec
@@ -2,7 +2,7 @@
Oqtane.Shared
- 10.0.1
+ 10.0.2
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -12,7 +12,7 @@
false
MIT
https://github.com/oqtane/oqtane.framework
- https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1
+ https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2
readme.md
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec
index 6873d041..3994a0d3 100644
--- a/Oqtane.Package/Oqtane.Updater.nuspec
+++ b/Oqtane.Package/Oqtane.Updater.nuspec
@@ -2,7 +2,7 @@
Oqtane.Updater
- 10.0.1
+ 10.0.2
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -12,7 +12,7 @@
false
MIT
https://github.com/oqtane/oqtane.framework
- https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1
+ https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2
readme.md
icon.png
oqtane
diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1
index 6efebb86..bbe63104 100644
--- a/Oqtane.Package/install.ps1
+++ b/Oqtane.Package/install.ps1
@@ -1 +1 @@
-Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.1.Install.zip" -Force
+Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.2.Install.zip" -Force
diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1
index 4f20bec7..7d31b12f 100644
--- a/Oqtane.Package/upgrade.ps1
+++ b/Oqtane.Package/upgrade.ps1
@@ -1 +1 @@
-Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.1.Upgrade.zip" -Force
+Compress-Archive -Path "..\Oqtane.Server\bin\Release\net10.0\publish\*" -DestinationPath "Oqtane.Framework.10.0.2.Upgrade.zip" -Force
diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor
index 6276eea0..fe668866 100644
--- a/Oqtane.Server/Components/App.razor
+++ b/Oqtane.Server/Components/App.razor
@@ -294,8 +294,11 @@
private void HandlePageNotFound(Site site, Page page, Route route)
{
+ // referrer will only be set if the link originated externally
+ string referrer = (Context.Request.Headers[HeaderNames.Referer] != StringValues.Empty) ? Context.Request.Headers[HeaderNames.Referer] : "";
+
// page not found - look for url mapping
- var urlMapping = UrlMappingRepository.GetUrlMapping(site.SiteId, route.PagePath);
+ var urlMapping = UrlMappingRepository.GetUrlMapping(site.SiteId, route.PagePath, referrer);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
// redirect to mapped url
diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs
index 0a2eb0f5..c7a6d2ac 100644
--- a/Oqtane.Server/Controllers/SettingController.cs
+++ b/Oqtane.Server/Controllers/SettingController.cs
@@ -90,7 +90,7 @@ namespace Oqtane.Controllers
else
{
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions
- if (entityName != EntityNames.Visitor)
+ if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings For EntityName {EntityName} And EntityId {EntityId}", entityName, entityId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -114,7 +114,7 @@ namespace Oqtane.Controllers
}
else
{
- if (setting != null && entityName != EntityNames.Visitor)
+ if (setting != null && FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access SettingId {SettingId} For EntityName {EntityName} ", id, entityName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -139,7 +139,7 @@ namespace Oqtane.Controllers
}
else
{
- if (setting.EntityName != EntityNames.Visitor)
+ if (FormatName(setting.EntityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Add Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -161,7 +161,7 @@ namespace Oqtane.Controllers
}
else
{
- if (setting.EntityName != EntityNames.Visitor)
+ if (FormatName(setting.EntityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Update, "User Not Authorized To Update Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -261,7 +261,7 @@ namespace Oqtane.Controllers
}
else
{
- if (entityName != EntityNames.Visitor)
+ if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For EntityName {EntityName} EntityId {EntityId} SettingName {SettingName}", entityName, entityId, settingName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -282,7 +282,7 @@ namespace Oqtane.Controllers
}
else
{
- if (entityName != EntityNames.Visitor)
+ if (FormatName(entityName) != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For SettingId {SettingId} For EntityName {EntityName} ", id, entityName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -408,19 +408,21 @@ namespace Oqtane.Controllers
private bool IsAuthorized(string entityName, int entityId, string permissionName)
{
bool authorized = false;
+
if (entityName == EntityNames.PageModule)
{
entityName = EntityNames.Module;
entityId = _pageModules.GetPageModule(entityId).ModuleId;
}
- switch (entityName)
+
+ switch (FormatName(entityName))
{
case EntityNames.Tenant:
case EntityNames.ModuleDefinition:
case EntityNames.Host:
case EntityNames.Job:
case EntityNames.Theme:
- if (permissionName == PermissionNames.Edit)
+ if (FormatName(permissionName) == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Host);
}
@@ -431,7 +433,7 @@ namespace Oqtane.Controllers
break;
case EntityNames.Site:
case EntityNames.Role:
- if (permissionName == PermissionNames.Edit)
+ if (FormatName(permissionName) == PermissionNames.Edit)
{
authorized = User.IsInRole(RoleNames.Admin);
}
@@ -458,7 +460,7 @@ namespace Oqtane.Controllers
break;
default: // custom entity
authorized = true;
- if (permissionName == PermissionNames.Edit)
+ if (FormatName(permissionName) == PermissionNames.Edit)
{
if (entityId == -1)
{
@@ -477,7 +479,7 @@ namespace Oqtane.Controllers
private bool FilterPrivate(string entityName, int entityId)
{
bool filter = false;
- switch (entityName)
+ switch (FormatName(entityName))
{
case EntityNames.Tenant:
case EntityNames.ModuleDefinition:
@@ -526,9 +528,9 @@ namespace Oqtane.Controllers
private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action)
{
- _syncManager.AddSyncEvent(_alias, EntityName + "Setting", SettingId, Action);
+ _syncManager.AddSyncEvent(_alias, FormatName(EntityName) + "Setting", SettingId, Action);
- switch (EntityName)
+ switch (FormatName(EntityName))
{
case EntityNames.Module:
case EntityNames.Page:
@@ -540,5 +542,15 @@ namespace Oqtane.Controllers
break;
}
}
+
+ private string FormatName(string name)
+ {
+ if (!string.IsNullOrEmpty(name))
+ {
+ // entity names and permission names are case sensitive
+ name = name.Substring(0, 1).ToUpper() + name.Substring(1).ToLower();
+ }
+ return name;
+ }
}
}
diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs
index ece7d295..aa6aa909 100644
--- a/Oqtane.Server/Controllers/UserController.cs
+++ b/Oqtane.Server/Controllers/UserController.cs
@@ -563,11 +563,11 @@ namespace Oqtane.Controllers
}
}
- // GET api//loginlink/x
- [HttpGet("loginlink/{email}")]
- public async Task SendLoginLink(string email)
+ // GET api//loginlink/x/y
+ [HttpGet("loginlink/{email}/{returnurl}")]
+ public async Task SendLoginLink(string email, string returnurl)
{
- return await _userManager.SendLoginLink(email);
+ return await _userManager.SendLoginLink(email, returnurl);
}
}
}
diff --git a/Oqtane.Server/Databases/PostgreSQL/PostgreSQLDatabase.cs b/Oqtane.Server/Databases/PostgreSQL/PostgreSQLDatabase.cs
index c22908b2..96c7b65f 100644
--- a/Oqtane.Server/Databases/PostgreSQL/PostgreSQLDatabase.cs
+++ b/Oqtane.Server/Databases/PostgreSQL/PostgreSQLDatabase.cs
@@ -1,6 +1,7 @@
using System;
using System.Data;
using System.Globalization;
+using System.Linq;
using EFCore.NamingConventions.Internal;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;
@@ -108,36 +109,40 @@ namespace Oqtane.Database.PostgreSQL
public override void UpdateIdentityStoreTableNames(ModelBuilder builder)
{
- foreach(var entity in builder.Model.GetEntityTypes())
+ foreach (var entity in builder.Model.GetEntityTypes())
{
- var tableName = entity.GetTableName();
- if (tableName.StartsWith("AspNetUser"))
+ // the IdentityPasskeyData entity was introduced in .NET 10 and is not mapped to a database table so should be ignored
+ if (entity.ClrType.Name != "IdentityPasskeyData")
{
- // replace table name
- entity.SetTableName(RewriteName(entity.GetTableName()));
-
- // replace column names
- foreach(var property in entity.GetProperties())
+ var tableName = entity.GetTableName();
+ if (tableName.StartsWith("AspNetUser"))
{
- property.SetColumnName(RewriteName(property.Name));
- }
+ // replace table name
+ entity.SetTableName(RewriteName(entity.GetTableName()));
- // replace key names
- foreach(var key in entity.GetKeys())
- {
- key.SetName(RewriteName(key.GetName()));
- }
+ // replace column names
+ foreach (var property in entity.GetProperties())
+ {
+ property.SetColumnName(RewriteName(property.Name));
+ }
- // replace foreign key names
- foreach (var key in entity.GetForeignKeys())
- {
- key.PrincipalKey.SetName(RewriteName(key.PrincipalKey.GetName()));
- }
+ // replace key names
+ foreach (var key in entity.GetKeys())
+ {
+ key.SetName(RewriteName(key.GetName()));
+ }
- // replace index names
- foreach (var index in entity.GetIndexes())
- {
- index.SetDatabaseName(RewriteName(index.GetDatabaseName()));
+ // replace foreign key names
+ foreach (var key in entity.GetForeignKeys())
+ {
+ key.PrincipalKey.SetName(RewriteName(key.PrincipalKey.GetName()));
+ }
+
+ // replace index names
+ foreach (var index in entity.GetIndexes())
+ {
+ index.SetDatabaseName(RewriteName(index.GetDatabaseName()));
+ }
}
}
}
diff --git a/Oqtane.Server/Databases/Sqlite/SqliteDatabase.cs b/Oqtane.Server/Databases/Sqlite/SqliteDatabase.cs
index e8cd6fd3..7d1226da 100644
--- a/Oqtane.Server/Databases/Sqlite/SqliteDatabase.cs
+++ b/Oqtane.Server/Databases/Sqlite/SqliteDatabase.cs
@@ -31,12 +31,15 @@ namespace Oqtane.Database.Sqlite
public override void DropColumn(MigrationBuilder builder, string name, string table)
{
- // not implemented as SQLite does not support dropping columns
+ // SQLite supports dropping columns starting with version 3.35.0 but EF Core does not implement it yet
+ // note that a column cannot be dropped if it has a UNIQUE constraint, is part of a PRIMARY KEY, is indexed, or is referenced by other parts of the schema
+ builder.Sql($"ALTER TABLE {table} DROP COLUMN {name};");
}
public override void AlterStringColumn(MigrationBuilder builder, string name, string table, int length, bool nullable, bool unicode, string index)
{
// not implemented as SQLite does not support altering columns
+ // note that column length does not need to be modified as SQLite uses a TEXT type which utilizes variable length strings
}
public override string ConcatenateSql(params string[] values)
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index 0452686c..8d0c3f75 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -44,7 +44,10 @@ namespace Microsoft.Extensions.DependencyInjection
// process forwarded headers on load balancers and proxy servers
services.Configure(options =>
{
- options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
+ options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
+
+ options.KnownIPNetworks.Clear();
+ options.KnownProxies.Clear();
});
// register localization services
diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs
index afad9465..87d6aab0 100644
--- a/Oqtane.Server/Infrastructure/DatabaseManager.cs
+++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs
@@ -449,8 +449,6 @@ namespace Oqtane.Infrastructure
private Installation MigrateModules(InstallConfig install)
{
- var result = new Installation { Success = false, Message = string.Empty };
-
using (var scope = _serviceScopeFactory.CreateScope())
{
var moduleDefinitions = scope.ServiceProvider.GetRequiredService();
@@ -464,6 +462,8 @@ namespace Oqtane.Infrastructure
var versions = moduleDefinition.ReleaseVersions.Split(',', StringSplitOptions.RemoveEmptyEntries);
using (var db = GetInstallationContext())
{
+ var message = "";
+
if (!string.IsNullOrEmpty(moduleDefinition.ServerManagerType))
{
var moduleType = Type.GetType(moduleDefinition.ServerManagerType);
@@ -488,20 +488,23 @@ namespace Oqtane.Infrastructure
var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable;
if (moduleObject == null || !moduleObject.Install(tenant, versions[i]))
{
- result.Message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType;
+ message = "An Error Occurred Executing IInstallable Interface For " + moduleDefinition.ServerManagerType + " On Tenant " + tenant.Name;
+ _filelogger.LogError(Utilities.LogMessage(this, message));
}
}
else
{
if (!sql.ExecuteScript(tenant, moduleType.Assembly, Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql"))
{
- result.Message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql";
+ message = "An Error Occurred Executing Database Script " + Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql On Tenant " + tenant.Name;
+ _filelogger.LogError(Utilities.LogMessage(this, message));
}
}
}
catch (Exception ex)
{
- result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
+ message = "An Error Occurred Installing " + moduleDefinition.Name + " Version " + versions[i] + " On Tenant " + tenant.Name + " - " + ex.ToString();
+ _filelogger.LogError(Utilities.LogMessage(this, message));
}
}
}
@@ -509,11 +512,13 @@ namespace Oqtane.Infrastructure
}
else
{
- result.Message = "An Error Occurred Installing " + moduleDefinition.Name + " - ServerManagerType " + moduleDefinition.ServerManagerType + " Does Not Exist";
+ message = "An Error Occurred Installing " + moduleDefinition.Name + " - ServerManagerType " + moduleDefinition.ServerManagerType + " Does Not Exist";
+ _filelogger.LogError(Utilities.LogMessage(this, message));
}
}
- if (string.IsNullOrEmpty(result.Message) && moduleDefinition.Version != versions[versions.Length - 1])
+ // update module if all migrations were successful and version is not current
+ if (string.IsNullOrEmpty(message) && moduleDefinition.Version != versions[versions.Length - 1])
{
// get module definition from database to retain user customizable property values
var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId);
@@ -531,16 +536,8 @@ namespace Oqtane.Infrastructure
}
}
- if (string.IsNullOrEmpty(result.Message))
- {
- result.Success = true;
- }
- else
- {
- _filelogger.LogError(Utilities.LogMessage(this, result.Message));
- }
-
- return result;
+ // module migration issues are logged and should not prevent the framework from starting up
+ return new Installation { Success = true, Message = string.Empty };
}
private Installation CreateSite(InstallConfig install)
@@ -596,6 +593,7 @@ namespace Oqtane.Infrastructure
Runtime = runtime,
Prerender = (rendermode == RenderModes.Interactive),
Hybrid = false,
+ EnhancedNavigation = true,
TenantId = tenant.TenantId
};
site = sites.AddSite(site);
diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs
index 3e032400..ea76e39a 100644
--- a/Oqtane.Server/Managers/UserManager.cs
+++ b/Oqtane.Server/Managers/UserManager.cs
@@ -41,7 +41,7 @@ namespace Oqtane.Managers
Task> GetLogins(int userId, int siteId);
Task AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key);
- Task SendLoginLink(string email);
+ Task SendLoginLink(string email, string returnurl);
}
public class UserManager : IUserManager
@@ -960,7 +960,7 @@ namespace Oqtane.Managers
}
}
- public async Task SendLoginLink(string email)
+ public async Task SendLoginLink(string email, string returnurl)
{
try
{
@@ -973,7 +973,7 @@ namespace Oqtane.Managers
var alias = _tenantManager.GetAlias();
var user = GetUser(identityuser.UserName, alias.SiteId);
- string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
+ string url = alias.Protocol + alias.Name + "/pages/loginlink?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token) + "&returnurl=" + WebUtility.UrlEncode(returnurl);
string siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["LoginLinkEmailSubject"];
subject = subject.Replace("[SiteName]", siteName);
diff --git a/Oqtane.Server/Migrations/Tenant/03020201_RemoveFolderFileDeletableColumns.cs b/Oqtane.Server/Migrations/Tenant/03020201_RemoveFolderFileDeletableColumns.cs
index f17cda4b..ec2fff10 100644
--- a/Oqtane.Server/Migrations/Tenant/03020201_RemoveFolderFileDeletableColumns.cs
+++ b/Oqtane.Server/Migrations/Tenant/03020201_RemoveFolderFileDeletableColumns.cs
@@ -16,15 +16,18 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
- var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
- folderEntityBuilder.DropColumn("DeletedBy");
- folderEntityBuilder.DropColumn("DeletedOn");
- folderEntityBuilder.DropColumn("IsDeleted");
+ if (ActiveDatabase.Name != "Sqlite")
+ {
+ var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
+ folderEntityBuilder.DropColumn("DeletedBy");
+ folderEntityBuilder.DropColumn("DeletedOn");
+ folderEntityBuilder.DropColumn("IsDeleted");
- var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
- fileEntityBuilder.DropColumn("DeletedBy");
- fileEntityBuilder.DropColumn("DeletedOn");
- fileEntityBuilder.DropColumn("IsDeleted");
+ var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
+ fileEntityBuilder.DropColumn("DeletedBy");
+ fileEntityBuilder.DropColumn("DeletedOn");
+ fileEntityBuilder.DropColumn("IsDeleted");
+ }
}
protected override void Down(MigrationBuilder migrationBuilder)
diff --git a/Oqtane.Server/Migrations/Tenant/03030201_AddFolderFileIsDeletedColumns.cs b/Oqtane.Server/Migrations/Tenant/03030201_AddFolderFileIsDeletedColumns.cs
index ad954a7d..ae62f197 100644
--- a/Oqtane.Server/Migrations/Tenant/03030201_AddFolderFileIsDeletedColumns.cs
+++ b/Oqtane.Server/Migrations/Tenant/03030201_AddFolderFileIsDeletedColumns.cs
@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
- // IsDeleted columns were removed in 3.2.2 however SQLite does not support column removal so they had to be restored
+ // IsDeleted columns were removed in 3.2.2 however SQLite did not support column removal so they had to be restored
if (ActiveDatabase.Name != "Sqlite")
{
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
diff --git a/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs b/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs
index 063b7027..b2f814d4 100644
--- a/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs
+++ b/Oqtane.Server/Migrations/Tenant/05020401_RemoveLanguageName.cs
@@ -16,8 +16,11 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
- var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
- languageEntityBuilder.DropColumn("Name");
+ if (ActiveDatabase.Name != "Sqlite")
+ {
+ var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
+ languageEntityBuilder.DropColumn("Name");
+ }
}
protected override void Down(MigrationBuilder migrationBuilder)
diff --git a/Oqtane.Server/Migrations/Tenant/06000101_AddLanguageName.cs b/Oqtane.Server/Migrations/Tenant/06000101_AddLanguageName.cs
index 76fedad6..69c48ddf 100644
--- a/Oqtane.Server/Migrations/Tenant/06000101_AddLanguageName.cs
+++ b/Oqtane.Server/Migrations/Tenant/06000101_AddLanguageName.cs
@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder)
{
- // Name column was removed in 5.2.4 however SQLite does not support column removal so it had to be restored
+ // Name column was removed in 5.2.4 however SQLite did not support column removal so it had to be restored
if (ActiveDatabase.Name != "Sqlite")
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
diff --git a/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs b/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs
index 3ac75cbe..117f6af4 100644
--- a/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs
+++ b/Oqtane.Server/Migrations/Tenant/10000102_RemoveSiteTenantId.cs
@@ -18,7 +18,10 @@ namespace Oqtane.Migrations.Tenant
{
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropIndex("IX_Site"); // TenantId, Name
- siteEntityBuilder.DropColumn("TenantId");
+ if (ActiveDatabase.Name != "Sqlite")
+ {
+ siteEntityBuilder.DropColumn("TenantId");
+ }
}
protected override void Down(MigrationBuilder migrationBuilder)
diff --git a/Oqtane.Server/Migrations/Tenant/10000201_RemoveDeprecatedColumns.cs b/Oqtane.Server/Migrations/Tenant/10000201_RemoveDeprecatedColumns.cs
new file mode 100644
index 00000000..c719f88c
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/10000201_RemoveDeprecatedColumns.cs
@@ -0,0 +1,60 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations.EntityBuilders;
+using Oqtane.Repository;
+
+namespace Oqtane.Migrations.Tenant
+{
+ [DbContext(typeof(TenantDBContext))]
+ [Migration("Tenant.10.00.02.01")]
+ public class RemoveDeprecatedColumns : MultiDatabaseMigration
+ {
+ public RemoveDeprecatedColumns(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ // Oqtane 10.0.2 includes support for column removal in SQLite, so we can now clean up deprecated columns
+
+ // Folder columns were deprecated in Oqtane 3.2.2
+ var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
+ folderEntityBuilder.DropColumn("IsDeleted");
+ if (ActiveDatabase.Name == "Sqlite")
+ {
+ /// the following columns were not added back in 3.2.3 but they still exist in SQLite databases
+ folderEntityBuilder.DropColumn("DeletedBy");
+ folderEntityBuilder.DropColumn("DeletedOn");
+ }
+
+ // File columns were deprecated in Oqtane 3.2.2
+ var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
+ // IsDeleted was added back in 3.2.3 for non-SQLLite databases
+ fileEntityBuilder.DropColumn("IsDeleted");
+ if (ActiveDatabase.Name == "Sqlite")
+ {
+ /// the following columns were not added back in 3.2.3 but they still exist in SQLite databases
+ fileEntityBuilder.DropColumn("DeletedBy");
+ fileEntityBuilder.DropColumn("DeletedOn");
+ }
+
+ // Language columns were deprecated in Oqtane 5.2.4
+ var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
+ languageEntityBuilder.DropColumn("Name");
+
+ // Site columns were deprecated in Oqtane 10.0.1
+ var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
+ if (ActiveDatabase.Name == "Sqlite")
+ {
+ /// the following column was removed for non-SQLite databases in 10.0.1
+ siteEntityBuilder.DropColumn("TenantId");
+ }
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // not implemented
+ }
+ }
+}
diff --git a/Oqtane.Server/Migrations/Tenant/10000202_ExpandPageName.cs b/Oqtane.Server/Migrations/Tenant/10000202_ExpandPageName.cs
new file mode 100644
index 00000000..c7111406
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/10000202_ExpandPageName.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations.EntityBuilders;
+using Oqtane.Repository;
+
+namespace Oqtane.Migrations.Tenant
+{
+ [DbContext(typeof(TenantDBContext))]
+ [Migration("Tenant.10.00.02.02")]
+ public class ExpandPageName : MultiDatabaseMigration
+ {
+ public ExpandPageName(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var pageEntityBuilder = new PageEntityBuilder(migrationBuilder, ActiveDatabase);
+ pageEntityBuilder.AlterStringColumn("Name", 100);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // not implemented
+ }
+ }
+}
diff --git a/Oqtane.Server/Migrations/Tenant/10000203_AddUrlMappingReferrer.cs b/Oqtane.Server/Migrations/Tenant/10000203_AddUrlMappingReferrer.cs
new file mode 100644
index 00000000..217b0d4c
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/10000203_AddUrlMappingReferrer.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations.EntityBuilders;
+using Oqtane.Repository;
+
+namespace Oqtane.Migrations.Tenant
+{
+ [DbContext(typeof(TenantDBContext))]
+ [Migration("Tenant.10.00.02.03")]
+ public class AddUrlMappingReferrer : MultiDatabaseMigration
+ {
+ public AddUrlMappingReferrer(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var urlMappingEntityBuilder = new UrlMappingEntityBuilder(migrationBuilder, ActiveDatabase);
+ urlMappingEntityBuilder.AddStringColumn("Referrer", 2048);
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ // not implemented
+ }
+ }
+}
diff --git a/Oqtane.Server/Modules/Admin/Files/Manager/FileManager.cs b/Oqtane.Server/Modules/Admin/Files/Manager/FileManager.cs
index 43dbda33..dd684d59 100644
--- a/Oqtane.Server/Modules/Admin/Files/Manager/FileManager.cs
+++ b/Oqtane.Server/Modules/Admin/Files/Manager/FileManager.cs
@@ -34,7 +34,6 @@ namespace Oqtane.Modules.Admin.Files.Manager
if (folder.ModifiedOn >= lastIndexedOn)
{
changed = true;
- removed = folder.IsDeleted.Value;
}
var files = _fileRepository.GetFiles(folder.FolderId);
@@ -78,7 +77,7 @@ namespace Oqtane.Modules.Admin.Files.Manager
Permissions = $"{EntityNames.Folder}:{folder.FolderId}",
ContentModifiedBy = file.ModifiedBy,
ContentModifiedOn = file.ModifiedOn,
- IsDeleted = (removed || file.IsDeleted.Value)
+ IsDeleted = (removed)
};
searchContents.Add(searchContent);
}
diff --git a/Oqtane.Server/Pages/LoginLink.cshtml.cs b/Oqtane.Server/Pages/LoginLink.cshtml.cs
index d090c7ed..16b34d0c 100644
--- a/Oqtane.Server/Pages/LoginLink.cshtml.cs
+++ b/Oqtane.Server/Pages/LoginLink.cshtml.cs
@@ -27,38 +27,45 @@ namespace Oqtane.Pages
_logger = logger;
}
- public async Task OnGetAsync(string name, string token)
+ public async Task OnGetAsync(string name, string token, string returnurl)
{
- var returnurl = "/login";
+ returnurl = (returnurl == null) ? "" : WebUtility.UrlDecode(returnurl);
if (bool.Parse(HttpContext.GetSiteSettings().GetValue("LoginOptions:LoginLink", "false")) &&
- !User.Identity.IsAuthenticated && !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token))
+ !string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(token))
{
var validuser = false;
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
- if (identityuser != null)
+ if (!User.Identity.IsAuthenticated)
{
- var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
- if (result.Succeeded)
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
+ if (identityuser != null)
{
- await _identitySignInManager.SignInAsync(identityuser, false);
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
- validuser = true;
- returnurl = "/";
+ var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
+ if (result.Succeeded)
+ {
+ await _identitySignInManager.SignInAsync(identityuser, false);
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
+ validuser = true;
+ }
}
}
if (!validuser)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name);
- returnurl += $"?status={ExternalLoginStatus.LoginLinkFailed}";
+ returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.LoginLinkFailed}";
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name);
- returnurl = "/";
+ returnurl = HttpContext.GetAlias().Path;
+ }
+
+ if (!returnurl.StartsWith("/"))
+ {
+ returnurl = "/" + returnurl;
}
return LocalRedirect(Url.Content("~" + returnurl));
diff --git a/Oqtane.Server/Pages/Logout.cshtml.cs b/Oqtane.Server/Pages/Logout.cshtml.cs
index 3d72d2ec..86263836 100644
--- a/Oqtane.Server/Pages/Logout.cshtml.cs
+++ b/Oqtane.Server/Pages/Logout.cshtml.cs
@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.Cookies;
-using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -11,7 +9,6 @@ using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Shared;
-using Radzen.Blazor.Markdown;
namespace Oqtane.Pages
{
diff --git a/Oqtane.Server/Pages/Passkey.cshtml.cs b/Oqtane.Server/Pages/Passkey.cshtml.cs
index 8923d78d..89d3ee2e 100644
--- a/Oqtane.Server/Pages/Passkey.cshtml.cs
+++ b/Oqtane.Server/Pages/Passkey.cshtml.cs
@@ -10,6 +10,7 @@ using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared;
+using Oqtane.UI;
namespace Oqtane.Pages
{
@@ -103,7 +104,7 @@ namespace Oqtane.Pages
{
identityuser = null;
var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser);
- returnurl += $"?options={WebUtility.UrlEncode(requestOptionsJson)}";
+ returnurl = HttpContext.GetAlias().Path + $"/login?options={WebUtility.UrlEncode(requestOptionsJson)}&returnurl={WebUtility.UrlEncode(returnurl)}";
}
else
{
@@ -129,6 +130,7 @@ namespace Oqtane.Pages
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential");
+ returnurl = HttpContext.GetAlias().Path + $"/login?status={ExternalLoginStatus.PasskeyFailed}&returnurl={WebUtility.UrlEncode(returnurl)}";
}
}
else
diff --git a/Oqtane.Server/Repository/FileRepository.cs b/Oqtane.Server/Repository/FileRepository.cs
index def51519..3cd3e2bf 100644
--- a/Oqtane.Server/Repository/FileRepository.cs
+++ b/Oqtane.Server/Repository/FileRepository.cs
@@ -72,7 +72,6 @@ namespace Oqtane.Repository
public File AddFile(File file)
{
using var db = _dbContextFactory.CreateDbContext();
- file.IsDeleted = false;
db.File.Add(file);
db.SaveChanges();
file.Folder = _folderRepository.GetFolder(file.FolderId);
diff --git a/Oqtane.Server/Repository/FolderRepository.cs b/Oqtane.Server/Repository/FolderRepository.cs
index d00a6a7c..fb68033f 100644
--- a/Oqtane.Server/Repository/FolderRepository.cs
+++ b/Oqtane.Server/Repository/FolderRepository.cs
@@ -51,7 +51,6 @@ namespace Oqtane.Repository
public Folder AddFolder(Folder folder)
{
using var db = _dbContextFactory.CreateDbContext();
- folder.IsDeleted = false;
db.Folder.Add(folder);
db.SaveChanges();
_permissions.UpdatePermissions(folder.SiteId, EntityNames.Folder, folder.FolderId, folder.PermissionList);
diff --git a/Oqtane.Server/Repository/UrlMappingRepository.cs b/Oqtane.Server/Repository/UrlMappingRepository.cs
index 9c2efbb6..b86b68b8 100644
--- a/Oqtane.Server/Repository/UrlMappingRepository.cs
+++ b/Oqtane.Server/Repository/UrlMappingRepository.cs
@@ -14,6 +14,7 @@ namespace Oqtane.Repository
UrlMapping GetUrlMapping(int urlMappingId);
UrlMapping GetUrlMapping(int urlMappingId, bool tracking);
UrlMapping GetUrlMapping(int siteId, string url);
+ UrlMapping GetUrlMapping(int siteId, string url, string referrer);
void DeleteUrlMapping(int urlMappingId);
int DeleteUrlMappings(int siteId, int age);
}
@@ -78,6 +79,11 @@ namespace Oqtane.Repository
}
public UrlMapping GetUrlMapping(int siteId, string url)
+ {
+ return GetUrlMapping(siteId, url, "");
+ }
+
+ public UrlMapping GetUrlMapping(int siteId, string url, string referrer)
{
using var db = _dbContextFactory.CreateDbContext();
url = (url.StartsWith("/")) ? url.Substring(1) : url;
@@ -93,6 +99,7 @@ namespace Oqtane.Repository
urlMapping.Url = url;
urlMapping.MappedUrl = "";
urlMapping.Requests = 1;
+ urlMapping.Referrer = referrer;
urlMapping.CreatedOn = DateTime.UtcNow;
urlMapping.RequestedOn = DateTime.UtcNow;
try
@@ -109,6 +116,10 @@ namespace Oqtane.Repository
{
urlMapping.Requests += 1;
urlMapping.RequestedOn = DateTime.UtcNow;
+ if (!string.IsNullOrEmpty(referrer))
+ {
+ urlMapping.Referrer = referrer;
+ }
urlMapping = UpdateUrlMapping(urlMapping);
}
return urlMapping;
diff --git a/Oqtane.Shared/Models/File.cs b/Oqtane.Shared/Models/File.cs
index e395bdb5..18c320a7 100644
--- a/Oqtane.Shared/Models/File.cs
+++ b/Oqtane.Shared/Models/File.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel.DataAnnotations.Schema;
+using System.Text.Json.Serialization;
using Oqtane.Shared;
namespace Oqtane.Models
@@ -55,13 +56,6 @@ namespace Oqtane.Models
///
public string Description { get; set; }
- ///
- /// Deprecated
- /// Note that this property still exists in the database because columns cannot be dropped in SQLite
- /// Therefore the property must be retained/mapped even though the framework no longer uses it
- ///
- public bool? IsDeleted { get; set; }
-
///
/// Object reference to the object.
/// Use this if you need to determine what the file belongs to.
@@ -74,5 +68,16 @@ namespace Oqtane.Models
///
[NotMapped]
public string Url { get; set; }
+
+ #region Deprecated Properties
+
+ [Obsolete("The IsDeleted property is deprecated. Soft delete of files is not supported.", false)]
+ [NotMapped]
+ [JsonIgnore] // exclude from API payload
+ public bool? IsDeleted { get; set; }
+
+ #endregion
+
+
}
}
diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs
index 47177cdb..d8e3e1ce 100644
--- a/Oqtane.Shared/Models/Folder.cs
+++ b/Oqtane.Shared/Models/Folder.cs
@@ -67,13 +67,6 @@ namespace Oqtane.Models
///
public string CacheControl { get; set; }
- ///
- /// Deprecated
- /// Note that this property still exists in the database because columns cannot be dropped in SQLite
- /// Therefore the property must be retained/mapped even though the framework no longer uses it
- ///
- public bool? IsDeleted { get; set; }
-
///
/// TODO: todoc what would this contain?
///
@@ -110,6 +103,11 @@ namespace Oqtane.Models
}
}
+ [Obsolete("The IsDeleted property is deprecated. Soft delete of folders is not supported.", false)]
+ [NotMapped]
+ [JsonIgnore] // exclude from API payload
+ public bool? IsDeleted { get; set; }
+
#endregion
}
}
diff --git a/Oqtane.Shared/Models/Language.cs b/Oqtane.Shared/Models/Language.cs
index 7f226168..750d8583 100644
--- a/Oqtane.Shared/Models/Language.cs
+++ b/Oqtane.Shared/Models/Language.cs
@@ -31,9 +31,8 @@ namespace Oqtane.Models
///
/// Language Name - corresponds to , _not_
- /// Note that this property still exists in the database because columns cannot be dropped in SQLite
- /// Therefore the property must be retained/mapped even though the framework populates it from the Culture API
///
+ [NotMapped]
public string Name { get; set; }
[NotMapped]
diff --git a/Oqtane.Shared/Models/UrlMapping.cs b/Oqtane.Shared/Models/UrlMapping.cs
index b5a15384..fb00e709 100644
--- a/Oqtane.Shared/Models/UrlMapping.cs
+++ b/Oqtane.Shared/Models/UrlMapping.cs
@@ -33,6 +33,11 @@ namespace Oqtane.Models
///
public int Requests { get; set; }
+ ///
+ /// Last referrer to the Url (only set if linked to externally)
+ ///
+ public string Referrer { get; set; }
+
///
/// Date when the url was first requested for the site
///
diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs
index 3189ccce..d642b176 100644
--- a/Oqtane.Shared/Shared/Constants.cs
+++ b/Oqtane.Shared/Shared/Constants.cs
@@ -4,8 +4,8 @@ namespace Oqtane.Shared
{
public class Constants
{
- public static readonly string Version = "10.0.1";
- public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1";
+ public static readonly string Version = "10.0.2";
+ public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3,6.1.4,6.1.5,6.2.0,6.2.1,10.0.0,10.0.1,10.0.2";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";
diff --git a/Oqtane.Shared/Shared/ExternalLoginStatus.cs b/Oqtane.Shared/Shared/ExternalLoginStatus.cs
index 8423d799..8acd1694 100644
--- a/Oqtane.Shared/Shared/ExternalLoginStatus.cs
+++ b/Oqtane.Shared/Shared/ExternalLoginStatus.cs
@@ -11,5 +11,6 @@ namespace Oqtane.Shared {
public const string RemoteFailure = "RemoteFailure";
public const string ReviewClaims = "ReviewClaims";
public const string LoginLinkFailed = "LoginLinkFailed";
+ public const string PasskeyFailed = "PasskeyFailed";
}
}
diff --git a/README.md b/README.md
index 03b1c488..2f481f7d 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ Oqtane is being developed based on some fundamental principles which are outline
# Latest Release
-[10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) was released on December 15, 2025 and is a major release including 38 pull requests by 5 different contributors, pushing the total number of project commits all-time over 7400. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
+[10.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2) was released on December 23, 2025 and is a maintenance release including 19 pull requests by 2 different contributors, pushing the total number of project commits all-time to nearly 7500. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
# Try It Now!
@@ -111,6 +111,9 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap
This project is open source, and therefore is a work in progress...
+[10.0.2](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2) (Dec 23, 2025)
+- [x] Stabilization improvements
+
[10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) (Dec 15, 2025)
- [x] Stabilization improvements
diff --git a/azuredeploy.json b/azuredeploy.json
index 54ab2451..beb6ea0a 100644
--- a/azuredeploy.json
+++ b/azuredeploy.json
@@ -220,7 +220,7 @@
"apiVersion": "2024-04-01",
"name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]",
"properties": {
- "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.1/Oqtane.Framework.10.0.1.Install.zip"
+ "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v10.0.2/Oqtane.Framework.10.0.2.Install.zip"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"