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

View File

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

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@ else
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
private bool _allowloginlink = false; private bool _allowloginlink = false;
private bool _allowpasskeys = false; private bool _allowpasskeys = false;
private string _returnurl = string.Empty;
private ElementReference login; private ElementReference login;
private bool validated = false; private bool validated = false;
@@ -169,6 +170,9 @@ else
_registerurl = NavigateUrl("register"); _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"]; _togglepassword = SharedLocalizer["ShowPassword"];
if (PageState.QueryString.ContainsKey("name")) if (PageState.QueryString.ContainsKey("name"))
@@ -216,7 +220,7 @@ else
{ {
if (PageState.QueryString.ContainsKey("status")) 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() 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() private void TogglePassword()
@@ -294,20 +298,17 @@ else
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For {Username} From IP Address {IPAddress}", _username, SiteState.RemoteIPAddress); 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) if (hybrid)
{ {
// hybrid apps utilize an interactive login // hybrid apps utilize an interactive login
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider)); var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged(); authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(NavigateUrl(returnurl, true)); NavigationManager.NavigateTo(NavigateUrl(_returnurl, true));
} }
else else
{ {
// post back to the Login page so that the cookies are set correctly // 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/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/login/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
@@ -349,14 +350,14 @@ else
private void CancelLogin() private void CancelLogin()
{ {
NavigationManager.NavigateTo(PageState.ReturnUrl); NavigationManager.NavigateTo(_returnurl);
} }
private async Task PasskeyLogin() private async Task PasskeyLogin()
{ {
// post back to the Passkey page so that the cookies are set correctly // post back to the Passkey page so that the cookies are set correctly
var interop = new Interop(JSRuntime); 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/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
@@ -423,7 +424,7 @@ else
{ {
if (!string.IsNullOrEmpty(_email)) if (!string.IsNullOrEmpty(_email))
{ {
if (await UserService.SendLoginLinkAsync(_email)) if (await UserService.SendLoginLinkAsync(_email, _returnurl))
{ {
AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info); AddModuleMessage(Localizer["Message.SendLoginLink"], MessageType.Info);
await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email); await logger.LogInformation(LogFunction.Security, "Login Link Sent To Email {Email}", _email);
@@ -457,8 +458,7 @@ else
if (!string.IsNullOrEmpty(credential)) if (!string.IsNullOrEmpty(credential))
{ {
// post back to the Passkey page so that the cookies are set correctly // 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/"); string url = Utilities.TenantUrl(PageState.Alias, "/pages/passkey/");
await interop.SubmitForm(url, fields); await interop.SubmitForm(url, fields);
} }
@@ -497,7 +497,7 @@ else
// redirect logged in user to specified page // redirect logged in user to specified page
if (PageState.User != null && !UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) 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"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9"> <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>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -81,21 +81,9 @@
</div> </div>
} }
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required> <input id="title" class="form-control" @bind="@_title" maxlength="200" />
<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> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -110,27 +98,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" /> <input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div> </div>
</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"> <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> <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"> <div class="col-sm-9">
@@ -141,15 +108,8 @@
</div> </div>
</div> </div>
</div> </div>
<Section Name="Theme" Heading="Theme" ResourceKey="Theme">
<Section Name="Appearance" ResourceKey="Appearance" Heading=@Localizer["Appearance.Name"]>
<div class="container"> <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"> <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> <Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -181,6 +141,49 @@
</div> </div>
</div> </div>
</Section> </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"]> <Section Name="PageContent" ResourceKey="PageContent" Heading=@Localizer["PageContent.Heading"]>
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">

View File

@@ -22,7 +22,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the page name" ResourceKey="Name">Name: </Label>
<div class="col-sm-9"> <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>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin))
@@ -98,21 +98,9 @@
</div> </div>
} }
<div class="row mb-1 align-items-center"> <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"> <div class="col-sm-9">
<select id="navigation" class="form-select" @bind="@_isnavigation" required> <input id="title" class="form-control" @bind="@_title" maxlength="200" />
<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> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@@ -127,27 +115,6 @@
<input id="url" class="form-control" @bind="@_url" maxlength="500" /> <input id="url" class="form-control" @bind="@_url" maxlength="500" />
</div> </div>
</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"> <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> <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"> <div class="col-sm-9">
@@ -158,14 +125,8 @@
</div> </div>
</div> </div>
</div> </div>
<Section Name="Appearance" ResourceKey="Appearance" Heading="Appearance"> <Section Name="Theme" ResourceKey="Theme" Heading="Theme">
<div class="container"> <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"> <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> <Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
@@ -200,6 +161,49 @@
</div> </div>
</div> </div>
</Section> </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"> <Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">

View File

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

View File

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

View File

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

View File

@@ -246,6 +246,9 @@
<data name="ExternalLoginStatus.LoginLinkFailed" xml:space="preserve"> <data name="ExternalLoginStatus.LoginLinkFailed" xml:space="preserve">
<value>Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.</value> <value>Login Links Are Time Sensitive. Please Request Another Login Link To Complete The Login Process.</value>
</data> </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"> <data name="Register" xml:space="preserve">
<value>Register as new user?</value> <value>Register as new user?</value>
</data> </data>

View File

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

View File

@@ -224,7 +224,7 @@ namespace Oqtane.Services
/// </summary> /// </summary>
/// <param name="email"></param> /// <param name="email"></param>
/// <returns></returns> /// <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")] [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}"); 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": <DynamicComponent Type="@_menuType" Parameters="@Attributes"></DynamicComponent>
<MenuHorizontal/>
break;
default: // Vertical
{
<MenuVertical/>
break;
}
} }
@code{ @code{
[Parameter] [Parameter]
public string Orientation { get; set; } 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"> <main role="main">
<nav class="navbar navbar-dark bg-primary fixed-top"> <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 ms-auto">
<div class="controls-group"> <div class="controls-group">
<Search CssClass="me-3 text-center bg-primary" /> <Search CssClass="me-3 text-center bg-primary" />

View File

@@ -18,7 +18,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId> <ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>10.0.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>10.0.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- 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; min-height: 250px;
} }
.app-editor-resizable {
resize: vertical;
overflow: auto;
}
.app-logo .navbar-brand { .app-logo .navbar-brand {
padding: 5px 20px 5px 20px; padding: 5px 20px 5px 20px;
} }

View File

@@ -124,7 +124,7 @@ Oqtane.Interop = {
} }
}, },
includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) { includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) {
var script; var script = null;
if (src !== "") { if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(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.remove();
script = null; 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> <package>
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>10.0.1</version> <version>10.0.2</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <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> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
<package> <package>
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>10.0.1</version> <version>10.0.2</version>
<authors>Shaun Walker</authors> <authors>Shaun Walker</authors>
<owners>.NET Foundation</owners> <owners>.NET Foundation</owners>
<title>Oqtane Framework</title> <title>Oqtane Framework</title>
@@ -12,7 +12,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<license type="expression">MIT</license> <license type="expression">MIT</license>
<projectUrl>https://github.com/oqtane/oqtane.framework</projectUrl> <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> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <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) 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 // 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)) if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{ {
// redirect to mapped url // redirect to mapped url

View File

@@ -90,7 +90,7 @@ namespace Oqtane.Controllers
else else
{ {
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions // 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); _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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -114,7 +114,7 @@ namespace Oqtane.Controllers
} }
else 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); _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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -139,7 +139,7 @@ namespace Oqtane.Controllers
} }
else 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); _logger.Log(LogLevel.Error, this, LogFunction.Create, "User Not Authorized To Add Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -161,7 +161,7 @@ namespace Oqtane.Controllers
} }
else 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); _logger.Log(LogLevel.Error, this, LogFunction.Update, "User Not Authorized To Update Setting {Setting}", setting);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -261,7 +261,7 @@ namespace Oqtane.Controllers
} }
else 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); _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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -282,7 +282,7 @@ namespace Oqtane.Controllers
} }
else 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); _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; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
@@ -408,19 +408,21 @@ namespace Oqtane.Controllers
private bool IsAuthorized(string entityName, int entityId, string permissionName) private bool IsAuthorized(string entityName, int entityId, string permissionName)
{ {
bool authorized = false; bool authorized = false;
if (entityName == EntityNames.PageModule) if (entityName == EntityNames.PageModule)
{ {
entityName = EntityNames.Module; entityName = EntityNames.Module;
entityId = _pageModules.GetPageModule(entityId).ModuleId; entityId = _pageModules.GetPageModule(entityId).ModuleId;
} }
switch (entityName)
switch (FormatName(entityName))
{ {
case EntityNames.Tenant: case EntityNames.Tenant:
case EntityNames.ModuleDefinition: case EntityNames.ModuleDefinition:
case EntityNames.Host: case EntityNames.Host:
case EntityNames.Job: case EntityNames.Job:
case EntityNames.Theme: case EntityNames.Theme:
if (permissionName == PermissionNames.Edit) if (FormatName(permissionName) == PermissionNames.Edit)
{ {
authorized = User.IsInRole(RoleNames.Host); authorized = User.IsInRole(RoleNames.Host);
} }
@@ -431,7 +433,7 @@ namespace Oqtane.Controllers
break; break;
case EntityNames.Site: case EntityNames.Site:
case EntityNames.Role: case EntityNames.Role:
if (permissionName == PermissionNames.Edit) if (FormatName(permissionName) == PermissionNames.Edit)
{ {
authorized = User.IsInRole(RoleNames.Admin); authorized = User.IsInRole(RoleNames.Admin);
} }
@@ -458,7 +460,7 @@ namespace Oqtane.Controllers
break; break;
default: // custom entity default: // custom entity
authorized = true; authorized = true;
if (permissionName == PermissionNames.Edit) if (FormatName(permissionName) == PermissionNames.Edit)
{ {
if (entityId == -1) if (entityId == -1)
{ {
@@ -477,7 +479,7 @@ namespace Oqtane.Controllers
private bool FilterPrivate(string entityName, int entityId) private bool FilterPrivate(string entityName, int entityId)
{ {
bool filter = false; bool filter = false;
switch (entityName) switch (FormatName(entityName))
{ {
case EntityNames.Tenant: case EntityNames.Tenant:
case EntityNames.ModuleDefinition: case EntityNames.ModuleDefinition:
@@ -526,9 +528,9 @@ namespace Oqtane.Controllers
private void AddSyncEvent(string EntityName, int EntityId, int SettingId, string Action) 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.Module:
case EntityNames.Page: case EntityNames.Page:
@@ -540,5 +542,15 @@ namespace Oqtane.Controllers
break; 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 // GET api/<controller>/loginlink/x/y
[HttpGet("loginlink/{email}")] [HttpGet("loginlink/{email}/{returnurl}")]
public async Task<bool> SendLoginLink(string email) 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;
using System.Data; using System.Data;
using System.Globalization; using System.Globalization;
using System.Linq;
using EFCore.NamingConventions.Internal; using EFCore.NamingConventions.Internal;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations;
@@ -108,36 +109,40 @@ namespace Oqtane.Database.PostgreSQL
public override void UpdateIdentityStoreTableNames(ModelBuilder builder) public override void UpdateIdentityStoreTableNames(ModelBuilder builder)
{ {
foreach(var entity in builder.Model.GetEntityTypes()) foreach (var entity in builder.Model.GetEntityTypes())
{ {
var tableName = entity.GetTableName(); // the IdentityPasskeyData entity was introduced in .NET 10 and is not mapped to a database table so should be ignored
if (tableName.StartsWith("AspNetUser")) if (entity.ClrType.Name != "IdentityPasskeyData")
{ {
// replace table name var tableName = entity.GetTableName();
entity.SetTableName(RewriteName(entity.GetTableName())); if (tableName.StartsWith("AspNetUser"))
// replace column names
foreach(var property in entity.GetProperties())
{ {
property.SetColumnName(RewriteName(property.Name)); // replace table name
} entity.SetTableName(RewriteName(entity.GetTableName()));
// replace key names // replace column names
foreach(var key in entity.GetKeys()) foreach (var property in entity.GetProperties())
{ {
key.SetName(RewriteName(key.GetName())); property.SetColumnName(RewriteName(property.Name));
} }
// replace foreign key names // replace key names
foreach (var key in entity.GetForeignKeys()) foreach (var key in entity.GetKeys())
{ {
key.PrincipalKey.SetName(RewriteName(key.PrincipalKey.GetName())); key.SetName(RewriteName(key.GetName()));
} }
// replace index names // replace foreign key names
foreach (var index in entity.GetIndexes()) foreach (var key in entity.GetForeignKeys())
{ {
index.SetDatabaseName(RewriteName(index.GetDatabaseName())); 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) 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) 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 // 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) 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 // process forwarded headers on load balancers and proxy servers
services.Configure<ForwardedHeadersOptions>(options => 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 // register localization services

View File

@@ -449,8 +449,6 @@ namespace Oqtane.Infrastructure
private Installation MigrateModules(InstallConfig install) private Installation MigrateModules(InstallConfig install)
{ {
var result = new Installation { Success = false, Message = string.Empty };
using (var scope = _serviceScopeFactory.CreateScope()) using (var scope = _serviceScopeFactory.CreateScope())
{ {
var moduleDefinitions = scope.ServiceProvider.GetRequiredService<IModuleDefinitionRepository>(); var moduleDefinitions = scope.ServiceProvider.GetRequiredService<IModuleDefinitionRepository>();
@@ -464,6 +462,8 @@ namespace Oqtane.Infrastructure
var versions = moduleDefinition.ReleaseVersions.Split(',', StringSplitOptions.RemoveEmptyEntries); var versions = moduleDefinition.ReleaseVersions.Split(',', StringSplitOptions.RemoveEmptyEntries);
using (var db = GetInstallationContext()) using (var db = GetInstallationContext())
{ {
var message = "";
if (!string.IsNullOrEmpty(moduleDefinition.ServerManagerType)) if (!string.IsNullOrEmpty(moduleDefinition.ServerManagerType))
{ {
var moduleType = Type.GetType(moduleDefinition.ServerManagerType); var moduleType = Type.GetType(moduleDefinition.ServerManagerType);
@@ -488,20 +488,23 @@ namespace Oqtane.Infrastructure
var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable; var moduleObject = ActivatorUtilities.CreateInstance(scope.ServiceProvider, moduleType) as IInstallable;
if (moduleObject == null || !moduleObject.Install(tenant, versions[i])) 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 else
{ {
if (!sql.ExecuteScript(tenant, moduleType.Assembly, Utilities.GetTypeName(moduleDefinition.ModuleDefinitionName) + "." + versions[i] + ".sql")) 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) 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 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 // get module definition from database to retain user customizable property values
var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId); var moduledef = db.ModuleDefinition.AsNoTracking().FirstOrDefault(item => item.ModuleDefinitionId == moduleDefinition.ModuleDefinitionId);
@@ -531,16 +536,8 @@ namespace Oqtane.Infrastructure
} }
} }
if (string.IsNullOrEmpty(result.Message)) // module migration issues are logged and should not prevent the framework from starting up
{ return new Installation { Success = true, Message = string.Empty };
result.Success = true;
}
else
{
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
}
return result;
} }
private Installation CreateSite(InstallConfig install) private Installation CreateSite(InstallConfig install)
@@ -596,6 +593,7 @@ namespace Oqtane.Infrastructure
Runtime = runtime, Runtime = runtime,
Prerender = (rendermode == RenderModes.Interactive), Prerender = (rendermode == RenderModes.Interactive),
Hybrid = false, Hybrid = false,
EnhancedNavigation = true,
TenantId = tenant.TenantId TenantId = tenant.TenantId
}; };
site = sites.AddSite(site); site = sites.AddSite(site);

View File

@@ -41,7 +41,7 @@ namespace Oqtane.Managers
Task<List<UserLogin>> GetLogins(int userId, int siteId); Task<List<UserLogin>> GetLogins(int userId, int siteId);
Task<User> AddLogin(User user, string token, string type, string key, string name); Task<User> AddLogin(User user, string token, string type, string key, string name);
Task DeleteLogin(int userId, string provider, string key); Task DeleteLogin(int userId, string provider, string key);
Task<bool> SendLoginLink(string email); Task<bool> SendLoginLink(string email, string returnurl);
} }
public class UserManager : IUserManager 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 try
{ {
@@ -973,7 +973,7 @@ namespace Oqtane.Managers
var alias = _tenantManager.GetAlias(); var alias = _tenantManager.GetAlias();
var user = GetUser(identityuser.UserName, alias.SiteId); 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 siteName = _sites.GetSite(alias.SiteId).Name;
string subject = _localizer["LoginLinkEmailSubject"]; string subject = _localizer["LoginLinkEmailSubject"];
subject = subject.Replace("[SiteName]", siteName); subject = subject.Replace("[SiteName]", siteName);

View File

@@ -16,15 +16,18 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); if (ActiveDatabase.Name != "Sqlite")
folderEntityBuilder.DropColumn("DeletedBy"); {
folderEntityBuilder.DropColumn("DeletedOn"); var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.DropColumn("IsDeleted"); folderEntityBuilder.DropColumn("DeletedBy");
folderEntityBuilder.DropColumn("DeletedOn");
folderEntityBuilder.DropColumn("IsDeleted");
var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase); var fileEntityBuilder = new FileEntityBuilder(migrationBuilder, ActiveDatabase);
fileEntityBuilder.DropColumn("DeletedBy"); fileEntityBuilder.DropColumn("DeletedBy");
fileEntityBuilder.DropColumn("DeletedOn"); fileEntityBuilder.DropColumn("DeletedOn");
fileEntityBuilder.DropColumn("IsDeleted"); fileEntityBuilder.DropColumn("IsDeleted");
}
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)

View File

@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) 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") if (ActiveDatabase.Name != "Sqlite")
{ {
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase); var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);

View File

@@ -16,8 +16,11 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) protected override void Up(MigrationBuilder migrationBuilder)
{ {
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase); if (ActiveDatabase.Name != "Sqlite")
languageEntityBuilder.DropColumn("Name"); {
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.DropColumn("Name");
}
} }
protected override void Down(MigrationBuilder migrationBuilder) protected override void Down(MigrationBuilder migrationBuilder)

View File

@@ -16,7 +16,7 @@ namespace Oqtane.Migrations.Tenant
protected override void Up(MigrationBuilder migrationBuilder) 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") if (ActiveDatabase.Name != "Sqlite")
{ {
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase); var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);

View File

@@ -18,7 +18,10 @@ namespace Oqtane.Migrations.Tenant
{ {
var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase);
siteEntityBuilder.DropIndex("IX_Site"); // TenantId, Name siteEntityBuilder.DropIndex("IX_Site"); // TenantId, Name
siteEntityBuilder.DropColumn("TenantId"); if (ActiveDatabase.Name != "Sqlite")
{
siteEntityBuilder.DropColumn("TenantId");
}
} }
protected override void Down(MigrationBuilder migrationBuilder) 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) if (folder.ModifiedOn >= lastIndexedOn)
{ {
changed = true; changed = true;
removed = folder.IsDeleted.Value;
} }
var files = _fileRepository.GetFiles(folder.FolderId); var files = _fileRepository.GetFiles(folder.FolderId);
@@ -78,7 +77,7 @@ namespace Oqtane.Modules.Admin.Files.Manager
Permissions = $"{EntityNames.Folder}:{folder.FolderId}", Permissions = $"{EntityNames.Folder}:{folder.FolderId}",
ContentModifiedBy = file.ModifiedBy, ContentModifiedBy = file.ModifiedBy,
ContentModifiedOn = file.ModifiedOn, ContentModifiedOn = file.ModifiedOn,
IsDeleted = (removed || file.IsDeleted.Value) IsDeleted = (removed)
}; };
searchContents.Add(searchContent); searchContents.Add(searchContent);
} }

View File

@@ -27,38 +27,45 @@ namespace Oqtane.Pages
_logger = logger; _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")) && 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; var validuser = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name); if (!User.Identity.IsAuthenticated)
if (identityuser != null)
{ {
var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token); IdentityUser identityuser = await _identityUserManager.FindByNameAsync(name);
if (result.Succeeded) if (identityuser != null)
{ {
await _identitySignInManager.SignInAsync(identityuser, false); var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name); if (result.Succeeded)
validuser = true; {
returnurl = "/"; await _identitySignInManager.SignInAsync(identityuser, false);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Link Successful For User {Username}", name);
validuser = true;
}
} }
} }
if (!validuser) if (!validuser)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Link Failed For User {Username}", name); _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 else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Login Link Attempt For User {Username}", name); _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)); return LocalRedirect(Url.Content("~" + returnurl));

View File

@@ -1,8 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
@@ -11,7 +9,6 @@ using Oqtane.Extensions;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Managers; using Oqtane.Managers;
using Oqtane.Shared; using Oqtane.Shared;
using Radzen.Blazor.Markdown;
namespace Oqtane.Pages namespace Oqtane.Pages
{ {

View File

@@ -10,6 +10,7 @@ using Oqtane.Infrastructure;
using Oqtane.Managers; using Oqtane.Managers;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.UI;
namespace Oqtane.Pages namespace Oqtane.Pages
{ {
@@ -103,7 +104,7 @@ namespace Oqtane.Pages
{ {
identityuser = null; identityuser = null;
var requestOptionsJson = await _identitySignInManager.MakePasskeyRequestOptionsAsync(identityuser); 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 else
{ {
@@ -129,6 +130,7 @@ namespace Oqtane.Pages
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Passkey Login Failed - Invalid Credential"); _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 else

View File

@@ -72,7 +72,6 @@ namespace Oqtane.Repository
public File AddFile(File file) public File AddFile(File file)
{ {
using var db = _dbContextFactory.CreateDbContext(); using var db = _dbContextFactory.CreateDbContext();
file.IsDeleted = false;
db.File.Add(file); db.File.Add(file);
db.SaveChanges(); db.SaveChanges();
file.Folder = _folderRepository.GetFolder(file.FolderId); file.Folder = _folderRepository.GetFolder(file.FolderId);

View File

@@ -51,7 +51,6 @@ namespace Oqtane.Repository
public Folder AddFolder(Folder folder) public Folder AddFolder(Folder folder)
{ {
using var db = _dbContextFactory.CreateDbContext(); using var db = _dbContextFactory.CreateDbContext();
folder.IsDeleted = false;
db.Folder.Add(folder); db.Folder.Add(folder);
db.SaveChanges(); db.SaveChanges();
_permissions.UpdatePermissions(folder.SiteId, EntityNames.Folder, folder.FolderId, folder.PermissionList); _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);
UrlMapping GetUrlMapping(int urlMappingId, bool tracking); UrlMapping GetUrlMapping(int urlMappingId, bool tracking);
UrlMapping GetUrlMapping(int siteId, string url); UrlMapping GetUrlMapping(int siteId, string url);
UrlMapping GetUrlMapping(int siteId, string url, string referrer);
void DeleteUrlMapping(int urlMappingId); void DeleteUrlMapping(int urlMappingId);
int DeleteUrlMappings(int siteId, int age); int DeleteUrlMappings(int siteId, int age);
} }
@@ -78,6 +79,11 @@ namespace Oqtane.Repository
} }
public UrlMapping GetUrlMapping(int siteId, string url) 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(); using var db = _dbContextFactory.CreateDbContext();
url = (url.StartsWith("/")) ? url.Substring(1) : url; url = (url.StartsWith("/")) ? url.Substring(1) : url;
@@ -93,6 +99,7 @@ namespace Oqtane.Repository
urlMapping.Url = url; urlMapping.Url = url;
urlMapping.MappedUrl = ""; urlMapping.MappedUrl = "";
urlMapping.Requests = 1; urlMapping.Requests = 1;
urlMapping.Referrer = referrer;
urlMapping.CreatedOn = DateTime.UtcNow; urlMapping.CreatedOn = DateTime.UtcNow;
urlMapping.RequestedOn = DateTime.UtcNow; urlMapping.RequestedOn = DateTime.UtcNow;
try try
@@ -109,6 +116,10 @@ namespace Oqtane.Repository
{ {
urlMapping.Requests += 1; urlMapping.Requests += 1;
urlMapping.RequestedOn = DateTime.UtcNow; urlMapping.RequestedOn = DateTime.UtcNow;
if (!string.IsNullOrEmpty(referrer))
{
urlMapping.Referrer = referrer;
}
urlMapping = UpdateUrlMapping(urlMapping); urlMapping = UpdateUrlMapping(urlMapping);
} }
return urlMapping; return urlMapping;

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Models namespace Oqtane.Models
@@ -55,13 +56,6 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public string Description { get; set; } 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> /// <summary>
/// Object reference to the <see cref="Folder"/> object. /// Object reference to the <see cref="Folder"/> object.
/// Use this if you need to determine what <see cref="Site"/> the file belongs to. /// Use this if you need to determine what <see cref="Site"/> the file belongs to.
@@ -74,5 +68,16 @@ namespace Oqtane.Models
/// </summary> /// </summary>
[NotMapped] [NotMapped]
public string Url { get; set; } 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> /// </summary>
public string CacheControl { get; set; } 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> /// <summary>
/// TODO: todoc what would this contain? /// TODO: todoc what would this contain?
/// </summary> /// </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 #endregion
} }
} }

View File

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

View File

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

View File

@@ -4,8 +4,8 @@ namespace Oqtane.Shared
{ {
public class Constants public class Constants
{ {
public static readonly string Version = "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"; 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 PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client"; public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater"; public const string UpdaterPackageId = "Oqtane.Updater";

View File

@@ -11,5 +11,6 @@ namespace Oqtane.Shared {
public const string RemoteFailure = "RemoteFailure"; public const string RemoteFailure = "RemoteFailure";
public const string ReviewClaims = "ReviewClaims"; public const string ReviewClaims = "ReviewClaims";
public const string LoginLinkFailed = "LoginLinkFailed"; 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 # 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! # Try It Now!
@@ -111,6 +111,9 @@ Connect with other developers, get support, and share ideas by joining the Oqtan
# Roadmap # Roadmap
This project is open source, and therefore is a work in progress... 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) [10.0.1](https://github.com/oqtane/oqtane.framework/releases/tag/v10.0.1) (Dec 15, 2025)
- [x] Stabilization improvements - [x] Stabilization improvements

View File

@@ -220,7 +220,7 @@
"apiVersion": "2024-04-01", "apiVersion": "2024-04-01",
"name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]",
"properties": { "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": [ "dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]"