Merge pull request #5920 from oqtane/dev

10.0.2 Release
This commit is contained in:
Shaun Walker
2025-12-23 13:21:11 -05:00
committed by GitHub
58 changed files with 496 additions and 273 deletions

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Configurations>Debug;Release</Configurations>
<Version>10.0.1</Version>
<Version>10.0.2</Version>
<Product>Oqtane</Product>
<Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</PackageReleaseNotes>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
</PropertyGroup>

View File

@ -23,7 +23,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Client" Version="10.0.1" />
<PackageReference Include="Oqtane.Client" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd">
<metadata>
<id>Oqtane.Application.Template</id>
<version>10.0.1</version>
<version>10.0.2</version>
<title>Oqtane Application Template For Blazor</title>
<authors>Shaun Walker</authors>
<requireLicenseAcceptance>false</requireLicenseAcceptance>

View File

@ -33,7 +33,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Server" Version="10.0.1" />
<PackageReference Include="Oqtane.Server" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@ -11,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Oqtane.Shared" Version="10.0.1" />
<PackageReference Include="Oqtane.Shared" Version="10.0.2" />
</ItemGroup>
</Project>

View File

@ -127,6 +127,7 @@ else
private bool _allowsitelogin = true;
private bool _allowloginlink = false;
private bool _allowpasskeys = false;
private string _returnurl = string.Empty;
private ElementReference login;
private bool validated = false;
@ -169,6 +170,9 @@ else
_registerurl = NavigateUrl("register");
}
// PageState.ReturnUrl is not specified if user navigated directly to login page
_returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
_togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name"))
@ -216,7 +220,7 @@ else
{
if (PageState.QueryString.ContainsKey("status"))
{
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Info);
AddModuleMessage(Localizer["ExternalLoginStatus." + PageState.QueryString["status"]], MessageType.Warning);
}
}
}
@ -252,7 +256,7 @@ else
private void ExternalLogin()
{
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(PageState.ReturnUrl)), true);
NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + WebUtility.UrlEncode(_returnurl)), true);
}
private void TogglePassword()
@ -294,20 +298,17 @@ else
{
await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress);
// return url is not specified if user navigated directly to login page
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
if (hybrid)
{
// hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(returnurl, true));
NavigationManager.NavigateTo(NavigateUrl(_returnurl, true));
}
else
{
// post back to the Login page so that the cookies are set correctly
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(returnurl) };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = _username, password = _password, remember = _remember, returnurl = WebUtility.UrlEncode(_returnurl) };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields);
}
@ -349,14 +350,14 @@ else
private void CancelLogin()
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
NavigationManager.NavigateTo(_returnurl);
}
private async Task PasskeyLogin()
{
// post back to the Passkey page so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = NavigateUrl() };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "request", returnurl = _returnurl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
@ -423,7 +424,7 @@ else
{
if (!string.IsNullOrEmpty(_email))
{
if (await UserService.SendLoginLinkAsync(_email))
if (await UserService.SendLoginLinkAsync(_email, _returnurl))
{
AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info);
await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email);
@ -457,8 +458,7 @@ else
if (!string.IsNullOrEmpty(credential))
{
// post back to the Passkey page so that the cookies are set correctly
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path + "/";
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = returnurl };
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, operation = "login", credential = credential, returnurl = _returnurl };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields);
}
@ -497,7 +497,7 @@ else
// redirect logged in user to specified page
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
{
NavigationManager.NavigateTo(PageState.ReturnUrl);
NavigationManager.NavigateTo(_returnurl);
}
}
}

View File

@ -16,7 +16,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
<input id="name" class="form-control" @bind="@_name" maxlength="100" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@ -81,21 +81,9 @@
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -110,27 +98,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
<div class="col-sm-9">
@ -141,15 +108,8 @@
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance" Heading=@Localizer["Appearance.Name"]>
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
@ -181,6 +141,49 @@
</div>
</div>
</Section>
<Section Name="Appearance" Heading="Appearance" ResourceKey="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" ResourceKey="PageContent" Heading=@Localizer["PageContent.Heading"]>
<div class="container">
<div class="row mb-1 align-items-center">

View File

@ -22,7 +22,7 @@
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9">
<input id="name" class="form-control" @bind="@_name" maxlength="50" required />
<input id="name" class="form-control" @bind="@_name" maxlength="100" required />
</div>
</div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@ -98,21 +98,9 @@
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
@ -127,27 +115,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="personalizable" HelpText="Select whether you would like users to be able to personalize this page with their own content" ResourceKey="Personalizable">Personalizable? </Label>
<div class="col-sm-9">
@ -158,14 +125,8 @@
</div>
</div>
</div>
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance">
<Section Name="Theme" ResourceKey="Theme" Heading="Theme">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="title" HelpText="Optionally enter the page title. If you do not provide a page title, the page name will be used." ResourceKey="Title">Title: </Label>
<div class="col-sm-9">
<input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9">
@ -200,6 +161,49 @@
</div>
</div>
</Section>
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="icon" HelpText="Optionally provide an icon class name for this page which will be displayed in the site navigation" ResourceKey="Icon">Icon: </Label>
<div class="col-sm-8">
<InputList Value="@_icon" ValueChanged="IconChanged" DataList="@_icons" ResourceKey="Icon" ResourceType="@_iconresources" />
</div>
<div class="col-sm-1">
<i class="@_icon"></i>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="navigation" HelpText="Select whether the page is part of the site navigation or hidden" ResourceKey="Navigation">Navigation? </Label>
<div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clickable" HelpText="Select whether the link in the site navigation is enabled or disabled" ResourceKey="Clickable">Clickable? </Label>
<div class="col-sm-9">
<select id="clickable" class="form-select" @bind="@_isclickable" required>
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="effectiveDate" HelpText="The date that this page is active" ResourceKey="EffectiveDate">Effective Date: </Label>
<div class="col-sm-9">
<input type="date" id="effectiveDate" class="form-control" @bind="@_effectivedate" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="expiryDate" HelpText="The date that this page expires" ResourceKey="ExpiryDate">Expiry Date: </Label>
<div class="col-sm-9">
<input type="date" id="expiryDate" class="form-control" @bind="@_expirydate" />
</div>
</div>
</div>
</Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
<div class="container">
<div class="row mb-1 align-items-center">

View File

@ -36,6 +36,7 @@ else
<th>@Localizer["Url"]</th>
<th>@Localizer["Requests"]</th>
<th>@Localizer["Requested"]</th>
<th>@Localizer["Referrer"]</th>
</Header>
<Row>
<td><ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UrlMappingId.ToString())" ResourceKey="Edit" /></td>
@ -49,7 +50,8 @@ else
</td>
<td>@context.Requests</td>
<td>@UtcToLocal(context.RequestedOn)</td>
</Row>
<td>@context.Referrer</td>
</Row>
</Pager>
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">

View File

@ -114,9 +114,9 @@
}
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys" Expanded="@((_passkeys.Count > 0).ToString())">
<button type="button" class="btn btn-primary" @onclick="AddPasskey">@SharedLocalizer["Add"]</button>
@if (_passkeys != null && _passkeys.Count > 0)
@if (_passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
@ -142,15 +142,15 @@
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
<div class="mt-2">@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
<Section Name="Logins" Heading="Logins" ResourceKey="Logins" Expanded="@((_logins.Count > 0).ToString())">
@if (_logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
@ -165,7 +165,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />

View File

@ -106,8 +106,8 @@
<br /><br />
@if (_allowpasskeys)
{
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys">
@if (_passkeys != null && _passkeys.Count > 0)
<Section Name="Passkeys" Heading="Passkeys" ResourceKey="Passkeys" Expanded="@((_passkeys.Count > 0).ToString())">
@if (_passkeys.Count > 0)
{
<Pager Items="@_passkeys">
<Header>
@ -122,15 +122,15 @@
}
else
{
<div>@Localizer["Message.Passkeys.None"]</div>
<div class="mt-2">@Localizer["Message.Passkeys.None"]</div>
}
</Section>
<br />
}
@if (_allowexternallogin)
{
<Section Name="Logins" Heading="Logins" ResourceKey="Logins">
@if (_logins != null && _logins.Count > 0)
<Section Name="Logins" Heading="Logins" ResourceKey="Logins" Expanded="@((_logins.Count > 0).ToString())">
@if (_logins.Count > 0)
{
<Pager Items="@_logins">
<Header>
@ -145,7 +145,7 @@
}
else
{
<div>@Localizer["Message.Logins.None"]</div>
<div class="mt-2">@Localizer["Message.Logins.None"]</div>
}
</Section>
<br />

View File

@ -246,6 +246,9 @@
<data name="ExternalLoginStatus.LoginLinkFailed" xml:space="preserve">
<value>Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.</value>
</data>
<data name="ExternalLoginStatus.PasskeyFailed" xml:space="preserve">
<value>Passkey Login Was Unsuccessful. Please Ensure You Selected The Correct Passkey For This Site.</value>
</data>
<data name="Register" xml:space="preserve">
<value>Register as new user?</value>
</data>

View File

@ -225,9 +225,6 @@
<data name="Personalizable.Text" xml:space="preserve">
<value>Personalizable? </value>
</data>
<data name="Appearance.Name" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="HeadContent.HelpText" xml:space="preserve">
<value>Optionally enter content to be included in the page head (ie. meta, link, or script tags)</value>
</data>
@ -253,7 +250,7 @@
<value>Permissions</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme Settings</value>
<value>Theme</value>
</data>
<data name="EffectiveDate.HelpText" xml:space="preserve">
<value>The date that this page is active</value>
@ -267,4 +264,7 @@
<data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value>
</data>
</root>
<data name="Appearance.Heading" xml:space="preserve">
<value>Appearance</value>
</data>
</root>

View File

@ -309,4 +309,7 @@
<data name="UpdateModulePermissions.HelpText" xml:space="preserve">
<value>Specify if changes made to page permissions should be propagated to the modules on this page</value>
</data>
<data name="Theme.Heading" xml:space="preserve">
<value>Theme</value>
</data>
</root>

View File

@ -224,7 +224,7 @@ namespace Oqtane.Services
/// </summary>
/// <param name="email"></param>
/// <returns></returns>
Task<bool> SendLoginLinkAsync(string email);
Task<bool> SendLoginLinkAsync(string email, string returnurl);
}
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
@ -386,9 +386,9 @@ namespace Oqtane.Services
await DeleteAsync($"{Apiurl}/login?id={userId}&provider={provider}&key={key}");
}
public async Task<bool> SendLoginLinkAsync(string email)
public async Task<bool> SendLoginLinkAsync(string email, string returnurl)
{
return await GetJsonAsync<bool>($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}");
return await GetJsonAsync<bool>($"{Apiurl}/loginlink/{WebUtility.UrlEncode(email)}/{WebUtility.UrlEncode(returnurl)}");
}
}
}

View File

@ -1,20 +1,36 @@
@namespace Oqtane.Themes.Controls
@namespace Oqtane.Themes.Controls
@switch (Orientation)
@if (_menuType != null)
{
case "Horizontal":
<MenuHorizontal/>
break;
default: // Vertical
{
<MenuVertical/>
break;
}
<DynamicComponent Type="@_menuType" Parameters="@Attributes"></DynamicComponent>
}
@code{
[Parameter]
public string Orientation { get; set; }
[Parameter]
public string MenuType { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object> Attributes { get; set; } = new Dictionary<string, object>();
private Type _menuType;
protected override void OnInitialized()
{
if (string.IsNullOrEmpty(MenuType) && !string.IsNullOrEmpty(Orientation))
{
if (Orientation == "Horizontal")
{
MenuType = "Oqtane.Themes.Controls.MenuHorizontal, Oqtane.Client";
}
else
{
MenuType = "Oqtane.Themes.Controls.MenuVertical, Oqtane.Client";
}
}
_menuType = Type.GetType(MenuType);
}
}

View File

@ -4,7 +4,8 @@
<main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top">
<Logo UseSiteNameAsFallback="true" /><Menu Orientation="Horizontal" />
<Logo UseSiteNameAsFallback="true" />
<Menu MenuType="Oqtane.Themes.Controls.MenuHorizontal, Oqtane.Client" />
<div class="controls ms-auto">
<div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" />

View File

@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>10.0.1</ApplicationDisplayVersion>
<ApplicationDisplayVersion>10.0.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->

View File

@ -273,6 +273,11 @@ app {
min-height: 250px;
}
.app-editor-resizable {
resize: vertical;
overflow: auto;
}
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}

View File

@ -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);
}
};

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Client</id>
<version>10.0.1</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Framework</id>
<version>10.0.1</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -11,8 +11,8 @@
<copyright>.NET Foundation</copyright>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.1/Oqtane.Framework.10.0.1.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</releaseNotes>
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v10.0.2/Oqtane.Framework.10.0.2.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane framework</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Server</id>
<version>10.0.1</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Shared</id>
<version>10.0.1</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -2,7 +2,7 @@
<package>
<metadata>
<id>Oqtane.Updater</id>
<version>10.0.1</version>
<version>10.0.2</version>
<authors>Shaun Walker</authors>
<owners>.NET Foundation</owners>
<title>Oqtane Framework</title>
@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1</releaseNotes>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.2</releaseNotes>
<readme>readme.md</readme>
<icon>icon.png</icon>
<tags>oqtane</tags>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;
}
}
}

View File

@ -563,11 +563,11 @@ namespace Oqtane.Controllers
}
}
// GET api/<controller>/loginlink/x
[HttpGet("loginlink/{email}")]
public async Task<bool> SendLoginLink(string email)
// GET api/<controller>/loginlink/x/y
[HttpGet("loginlink/{email}/{returnurl}")]
public async Task<bool> SendLoginLink(string email, string returnurl)
{
return await _userManager.SendLoginLink(email);
return await _userManager.SendLoginLink(email, returnurl);
}
}
}

View File

@ -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()));
}
}
}
}

View File

@ -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)

View File

@ -44,7 +44,10 @@ namespace Microsoft.Extensions.DependencyInjection
// process forwarded headers on load balancers and proxy servers
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
// register localization services

View File

@ -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<IModuleDefinitionRepository>();
@ -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);

View File

@ -41,7 +41,7 @@ namespace Oqtane.Managers
Task<List<UserLogin>> GetLogins(int userId, int siteId);
Task<User> AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key);
Task<bool> SendLoginLink(string email);
Task<bool> SendLoginLink(string email, string returnurl);
}
public class UserManager : IUserManager
@ -960,7 +960,7 @@ namespace Oqtane.Managers
}
}
public async Task<bool> SendLoginLink(string email)
public async Task<bool> 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);

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -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);

View File

@ -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)

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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);
}

View File

@ -27,38 +27,45 @@ namespace Oqtane.Pages
_logger = logger;
}
public async Task<IActionResult> OnGetAsync(string name, string token)
public async Task<IActionResult> 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));

View File

@ -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
{

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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
/// </summary>
public string Description { get; set; }
/// <summary>
/// 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
/// </summary>
public bool? IsDeleted { get; set; }
/// <summary>
/// Object reference to the <see cref="Folder"/> object.
/// Use this if you need to determine what <see cref="Site"/> the file belongs to.
@ -74,5 +68,16 @@ namespace Oqtane.Models
/// </summary>
[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
}
}

View File

@ -67,13 +67,6 @@ namespace Oqtane.Models
/// </summary>
public string CacheControl { get; set; }
/// <summary>
/// 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
/// </summary>
public bool? IsDeleted { get; set; }
/// <summary>
/// TODO: todoc what would this contain?
/// </summary>
@ -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
}
}

View File

@ -31,9 +31,8 @@ namespace Oqtane.Models
/// <summary>
/// Language Name - corresponds to <see cref="Culture.DisplayName"/>, _not_ <see cref="Culture.Name"/>
/// 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
/// </summary>
[NotMapped]
public string Name { get; set; }
[NotMapped]

View File

@ -33,6 +33,11 @@ namespace Oqtane.Models
/// </summary>
public int Requests { get; set; }
/// <summary>
/// Last referrer to the Url (only set if linked to externally)
/// </summary>
public string Referrer { get; set; }
/// <summary>
/// Date when the url was first requested for the site
/// </summary>

View File

@ -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";

View File

@ -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";
}
}

View File

@ -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

View File

@ -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'))]"