Merge remote-tracking branch 'oqtane/dev' into dev

This commit is contained in:
Mark Davis 2024-11-15 09:52:50 -08:00
commit 365f87828f
152 changed files with 2739 additions and 1340 deletions

3
.gitignore vendored
View File

@ -22,6 +22,9 @@ Oqtane.Server/Packages
Oqtane.Server/wwwroot/Content Oqtane.Server/wwwroot/Content
Oqtane.Server/wwwroot/Packages/*.log Oqtane.Server/wwwroot/Packages/*.log
Oqtane.Server/wwwroot/_content/*
!Oqtane.Server/wwwroot/_content/Placeholder.txt
Oqtane.Server/wwwroot/Modules/* Oqtane.Server/wwwroot/Modules/*
!Oqtane.Server/wwwroot/Modules/Oqtane.Modules.* !Oqtane.Server/wwwroot/Modules/Oqtane.Modules.*
!Oqtane.Server/wwwroot/Modules/Templates !Oqtane.Server/wwwroot/Modules/Templates

View File

@ -51,6 +51,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IUrlMappingService, UrlMappingService>(); services.AddScoped<IUrlMappingService, UrlMappingService>();
services.AddScoped<IVisitorService, VisitorService>(); services.AddScoped<IVisitorService, VisitorService>();
services.AddScoped<ISyncService, SyncService>(); services.AddScoped<ISyncService, SyncService>();
services.AddScoped<ILocalizationCookieService, LocalizationCookieService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();

View File

@ -15,7 +15,7 @@
<div class="row"> <div class="row">
<div class="mx-auto text-center"> <div class="mx-auto text-center">
<img src="oqtane-black.png" /> <img src="oqtane-black.png" />
<div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET 8)</div> <div style="font-weight: bold">@SharedLocalizer["Version"] @Constants.Version (.NET 9)</div>
</div> </div>
</div> </div>
<hr class="app-rule" /> <hr class="app-rule" />
@ -156,129 +156,130 @@
private List<SiteTemplate> _templates; private List<SiteTemplate> _templates;
private string _template = Constants.DefaultSiteTemplate; private string _template = Constants.DefaultSiteTemplate;
private bool _register = true; private bool _register = true;
private string _message = string.Empty; private string _message = string.Empty;
private string _loadingDisplay = "display: none;"; private string _loadingDisplay = "display: none;";
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
// include CSS // include CSS
var content = $"<link rel=\"stylesheet\" href=\"{Constants.BootstrapStylesheetUrl}\" integrity=\"{Constants.BootstrapStylesheetIntegrity}\" crossorigin=\"anonymous\" type=\"text/css\"/>"; var content = $"<link rel=\"stylesheet\" href=\"{Constants.BootstrapStylesheetUrl}\" integrity=\"{Constants.BootstrapStylesheetIntegrity}\" crossorigin=\"anonymous\" type=\"text/css\"/>";
SiteState.AppendHeadContent(content); SiteState.AppendHeadContent(content);
_togglePassword = SharedLocalizer["ShowPassword"]; _togglePassword = SharedLocalizer["ShowPassword"];
_toggleConfirmPassword = SharedLocalizer["ShowPassword"]; _toggleConfirmPassword = SharedLocalizer["ShowPassword"];
_databases = await DatabaseService.GetDatabasesAsync(); _databases = await DatabaseService.GetDatabasesAsync();
if (_databases.Exists(item => item.IsDefault)) if (_databases.Exists(item => item.IsDefault))
{ {
_databaseName = _databases.Find(item => item.IsDefault).Name; _databaseName = _databases.Find(item => item.IsDefault).Name;
} }
else else
{ {
_databaseName = "LocalDB"; _databaseName = "LocalDB";
} }
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
_templates = await SiteTemplateService.GetSiteTemplatesAsync(); _templates = await SiteTemplateService.GetSiteTemplatesAsync();
} }
private void DatabaseChanged(ChangeEventArgs eventArgs) private void DatabaseChanged(ChangeEventArgs eventArgs)
{ {
try try
{ {
_databaseName = (string)eventArgs.Value; _databaseName = (string)eventArgs.Value;
_showConnectionString = false; _showConnectionString = false;
LoadDatabaseConfigComponent(); LoadDatabaseConfigComponent();
} }
catch catch
{ {
_message = Localizer["Error.DbConfig.Load"]; _message = Localizer["Error.DbConfig.Load"];
} }
} }
private void LoadDatabaseConfigComponent() private void LoadDatabaseConfigComponent()
{ {
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
if (database != null) if (database != null)
{ {
_databaseConfigType = Type.GetType(database.ControlType); _databaseConfigType = Type.GetType(database.ControlType);
DatabaseConfigComponent = builder => DatabaseConfigComponent = builder =>
{ {
builder.OpenComponent(0, _databaseConfigType); builder.OpenComponent(0, _databaseConfigType);
builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); }); builder.AddComponentReferenceCapture(1, inst => { _databaseConfig = Convert.ChangeType(inst, _databaseConfigType); });
builder.CloseComponent(); builder.CloseComponent();
}; };
} }
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
// include JavaScript // include JavaScript
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.IncludeScript("", Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous", "", "head"); await interop.IncludeScript("", Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous", "", "head");
} }
} }
private async Task Install() private async Task Install()
{ {
var connectionString = String.Empty; var connectionString = String.Empty;
if (_showConnectionString) if (_showConnectionString)
{ {
connectionString = _connectionString; connectionString = _connectionString;
} }
else else
{ {
if (_databaseConfig is IDatabaseConfigControl databaseConfigControl) if (_databaseConfig is IDatabaseConfigControl databaseConfigControl)
{ {
connectionString = databaseConfigControl.GetConnectionString(); connectionString = databaseConfigControl.GetConnectionString();
} }
} }
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@"))
{ {
if (await UserService.ValidatePasswordAsync(_hostPassword)) var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword);
{ if (result.Succeeded)
_loadingDisplay = ""; {
StateHasChanged(); _loadingDisplay = "";
StateHasChanged();
Uri uri = new Uri(NavigationManager.Uri); Uri uri = new Uri(NavigationManager.Uri);
var database = _databases.SingleOrDefault(d => d.Name == _databaseName); var database = _databases.SingleOrDefault(d => d.Name == _databaseName);
var config = new InstallConfig var config = new InstallConfig
{ {
DatabaseType = database.DBType, DatabaseType = database.DBType,
ConnectionString = connectionString, ConnectionString = connectionString,
Aliases = uri.Authority, Aliases = uri.Authority,
HostUsername = _hostUsername, HostUsername = _hostUsername,
HostPassword = _hostPassword, HostPassword = _hostPassword,
HostEmail = _hostEmail, HostEmail = _hostEmail,
HostName = _hostUsername, HostName = _hostUsername,
TenantName = TenantNames.Master, TenantName = TenantNames.Master,
IsNewTenant = true, IsNewTenant = true,
SiteName = Constants.DefaultSite, SiteName = Constants.DefaultSite,
Register = _register, Register = _register,
SiteTemplate = _template, SiteTemplate = _template,
RenderMode = RenderModes.Static, RenderMode = RenderModes.Static,
Runtime = Runtimes.Server Runtime = Runtimes.Server
}; };
var installation = await InstallationService.Install(config); var installation = await InstallationService.Install(config);
if (installation.Success) if (installation.Success)
{ {
NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true); NavigationManager.NavigateTo(uri.Scheme + "://" + uri.Authority, true);
} }
else else
{ {
_message = installation.Message; _message = installation.Message;
_loadingDisplay = "display: none;"; _loadingDisplay = "display: none;";
} }
} }
else else
{ {
_message = Localizer["Message.Password.Invalid"]; _message = string.Join("<br />", result.Errors.Select(i => !string.IsNullOrEmpty(i.Value) ? i.Value : Localizer[i.Key]));
} }
} }
else else

View File

@ -1,7 +1,6 @@
@namespace Oqtane.Modules.Admin.Languages @namespace Oqtane.Modules.Admin.Languages
@inherits ModuleBase @inherits ModuleBase
@using System.Globalization @using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ILocalizationService LocalizationService @inject ILocalizationService LocalizationService
@inject ILanguageService LanguageService @inject ILanguageService LanguageService
@ -94,7 +93,6 @@ else
var language = new Language var language = new Language
{ {
SiteId = PageState.Page.SiteId, SiteId = PageState.Page.SiteId,
Name = CultureInfo.GetCultureInfo(_code).DisplayName,
Code = _code, Code = _code,
IsDefault = (_default == null ? false : Boolean.Parse(_default)) IsDefault = (_default == null ? false : Boolean.Parse(_default))
}; };
@ -130,7 +128,7 @@ else
{ {
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
} }
} }

View File

@ -1,7 +1,6 @@
@namespace Oqtane.Modules.Admin.Languages @namespace Oqtane.Modules.Admin.Languages
@inherits ModuleBase @inherits ModuleBase
@using System.Globalization @using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject ILocalizationService LocalizationService @inject ILocalizationService LocalizationService
@inject ILanguageService LanguageService @inject ILanguageService LanguageService
@ -103,7 +102,7 @@ else
{ {
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
} }
} }

View File

@ -8,74 +8,77 @@
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<AuthorizeView Roles="@RoleNames.Registered"> @if (PageState.User != null)
<Authorizing> {
<text>...</text> <ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
</Authorizing> }
<Authorized> else
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" /> {
</Authorized> @if (!twofactor)
<NotAuthorized> {
@if (!twofactor) <form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
{ <div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate> @if (_allowexternallogin)
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))"> {
@if (_allowexternallogin) <button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
{ <br />
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br /> <br />
} }
@if (_allowsitelogin) @if (_allowsitelogin)
{ {
<div class="form-group"> <div class="form-group">
<Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label> <Label Class="control-label" For="username" HelpText="Please enter your Username" ResourceKey="Username">Username:</Label>
<input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required /> <input id="username" type="text" @ref="username" class="form-control" placeholder="@Localizer["Username.Placeholder"]" @bind="@_username" required />
</div> </div>
<div class="form-group mt-2"> <div class="form-group mt-2">
<Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label> <Label Class="control-label" For="password" HelpText="Please enter your Password" ResourceKey="Password">Password:</Label>
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required /> <input id="password" type="@_passwordtype" name="Password" class="form-control" placeholder="@Localizer["Password.Placeholder"]" @bind="@_password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div> </div>
</div> </div>
<div class="form-group mt-2"> <div class="form-group mt-2">
@if (!_alwaysremember) @if (!_alwaysremember)
{
<div class="form-check">
<input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
}
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br /><br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{ {
<br /><br /> <div class="form-check">
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink> <input id="remember" type="checkbox" class="form-check-input" @bind="@_remember" />
<Label Class="control-label" For="remember" HelpText="Specify if you would like to be signed back in automatically the next time you visit this site" ResourceKey="Remember">Remember Me?</Label>
</div>
} }
</div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<br />
<br />
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
@if (PageState.Site.AllowRegistration)
{
<br />
<br />
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
} }
</div> }
</form> </div>
} </form>
else }
{ else
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate> {
<div class="container Oqtane-Modules-Admin-Login"> <form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<div class="form-group"> <div class="container Oqtane-Modules-Admin-Login">
<Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label> <div class="form-group">
<input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required /> <Label Class="control-label" For="code" HelpText="Please enter the secure verification code which was sent to you by email" ResourceKey="Code">Verification Code:</Label>
</div> <input id="code" class="form-control" @bind="@_code" placeholder="@Localizer["Code.Placeholder"]" maxlength="6" required />
<br /> </div>
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button> <br />
<button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
</div> <button type="button" class="btn btn-secondary" @onclick="Reset">@SharedLocalizer["Cancel"]</button>
</form> </div>
} </form>
</NotAuthorized> }
</AuthorizeView> }
@code { @code {
private bool _allowsitelogin = true; private bool _allowsitelogin = true;
@ -204,9 +207,9 @@
user = await UserService.VerifyTwoFactorAsync(user, _code); user = await UserService.VerifyTwoFactorAsync(user, _code);
} }
if (user.IsAuthenticated) if (user != null && user.IsAuthenticated)
{ {
await logger.LogInformation(LogFunction.Security, "Login Successful For Username {Username}", _username); 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 // return url is not specified if user navigated directly to login page
var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path; var returnurl = (!string.IsNullOrEmpty(PageState.ReturnUrl)) ? PageState.ReturnUrl : PageState.Alias.Path;
@ -228,7 +231,7 @@
} }
else else
{ {
if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || user.TwoFactorRequired) if (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "required" || (user != null && user.TwoFactorRequired))
{ {
twofactor = true; twofactor = true;
validated = false; validated = false;
@ -239,12 +242,12 @@
if (!twofactor) if (!twofactor)
{ {
await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Login Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error); AddModuleMessage(Localizer["Error.Login.Fail"], MessageType.Error);
} }
else else
{ {
await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username); await logger.LogInformation(LogFunction.Security, "Two Factor Verification Failed For Username {Username}", _username);
AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error); AddModuleMessage(Localizer["Error.TwoFactor.Fail"], MessageType.Error);
} }
} }
} }

View File

@ -27,7 +27,7 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label> <Label Class="col-sm-3" For="description" HelpText="Enter a short description for the module" ResourceKey="Description">Description: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<textarea id="description" class="form-control" @bind="@_description" rows="3" maxlength="2000" required></textarea> <textarea id="description" class="form-control" @bind="@_description" rows="3" maxlength="2000"></textarea>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -118,6 +118,7 @@
{ {
if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-") if (IsValid(_owner) && IsValid(_module) && _owner != _module && _template != "-")
{ {
if (string.IsNullOrEmpty(_description)) _description = _module;
if (IsValidXML(_description)) if (IsValidXML(_description))
{ {
var template = _templates.FirstOrDefault(item => item.Name == _template); var template = _templates.FirstOrDefault(item => item.Name == _template);

View File

@ -1,7 +1,6 @@
@namespace Oqtane.Modules.Admin.ModuleDefinitions @namespace Oqtane.Modules.Admin.ModuleDefinitions
@inherits ModuleBase @inherits ModuleBase
@using System.Globalization @using System.Globalization
@using Microsoft.AspNetCore.Localization
@inject IModuleDefinitionService ModuleDefinitionService @inject IModuleDefinitionService ModuleDefinitionService
@inject IPackageService PackageService @inject IPackageService PackageService
@inject ILanguageService LanguageService @inject ILanguageService LanguageService

View File

@ -15,7 +15,7 @@
</div> </div>
<button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button> <button type="button" class="btn btn-success" @onclick="ExportModule">@Localizer["Export"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
@code { @code {
private string _content = string.Empty; private string _content = string.Empty;

View File

@ -17,7 +17,7 @@
</div> </div>
<button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button> <button type="button" class="btn btn-success" @onclick="ImportModule">@Localizer["Import"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@PageState.ReturnUrl">@SharedLocalizer["Cancel"]</NavLink>
</form> </form>
@code { @code {

View File

@ -167,7 +167,6 @@
{ {
SetModuleTitle(Localizer["ModuleSettings.Title"]); SetModuleTitle(Localizer["ModuleSettings.Title"]);
_module = ModuleState.ModuleDefinition.Name;
_title = ModuleState.Title; _title = ModuleState.Title;
_moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; _moduleSettingsTitle = Localizer["ModuleSettings.Heading"];
_pane = ModuleState.Pane; _pane = ModuleState.Pane;
@ -186,6 +185,7 @@
if (ModuleState.ModuleDefinition != null) if (ModuleState.ModuleDefinition != null)
{ {
_module = ModuleState.ModuleDefinition.Name;
_permissionNames = ModuleState.ModuleDefinition?.PermissionNames; _permissionNames = ModuleState.ModuleDefinition?.PermissionNames;
if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType)) if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType))

View File

@ -156,7 +156,14 @@
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required> <select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes) @foreach (var theme in _themes)
{ {
<option value="@theme.TypeName">@theme.Name</option> @if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
} }
</select> </select>
</div> </div>

View File

@ -172,7 +172,14 @@
<select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required> <select id="theme" class="form-select" value="@_themetype" @onchange="(e => ThemeChanged(e))" required>
@foreach (var theme in _themes) @foreach (var theme in _themes)
{ {
<option value="@theme.TypeName">@theme.Name</option> @if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
} }
</select> </select>
</div> </div>
@ -262,7 +269,14 @@
<select id="theme" class="form-select" @bind="@_themetype" required> <select id="theme" class="form-select" @bind="@_themetype" required>
@foreach (var theme in _themes) @foreach (var theme in _themes)
{ {
<option value="@theme.TypeName">@theme.Name</option> @if (theme.TypeName == PageState.Site.DefaultThemeType)
{
<option value="@theme.TypeName">*@theme.Name*</option>
}
else
{
<option value="@theme.TypeName">@theme.Name</option>
}
} }
</select> </select>
</div> </div>

View File

@ -22,7 +22,7 @@ else
} }
else else
{ {
<Pager Items="@_pages.Where(item => item.IsDeleted)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage"> <Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
@ -50,7 +50,7 @@ else
} }
else else
{ {
<Pager Items="@_modules.Where(item => item.IsDeleted)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule"> <Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>

View File

@ -11,65 +11,64 @@
{ {
if (!_userCreated) if (!_userCreated)
{ {
<AuthorizeView Roles="@RoleNames.Registered"> if (PageState.User != null)
<Authorizing> {
<text>...</text> <ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
</Authorizing> }
<Authorized> else
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" /> {
</Authorized> <ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<NotAuthorized> <form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" /> <div class="container">
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate> <div class="row mb-1 align-items-center">
<div class="container"> <Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
<div class="row mb-1 align-items-center"> <div class="col-sm-9">
<Label Class="col-sm-3" For="username" HelpText="Your username. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label> <input id="username" class="form-control" @bind="@_username" maxlength="256" required />
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" maxlength="256" required />
</div>
</div> </div>
<div class="row mb-1 align-items-center"> </div>
<Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label> <div class="row mb-1 align-items-center">
<div class="col-sm-9"> <Label Class="col-sm-3" For="password" HelpText="Please choose a sufficiently secure password and enter it here" ResourceKey="Password"></Label>
<div class="input-group"> <div class="col-sm-9">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required /> <div class="input-group">
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button> <input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" required />
</div> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div> </div>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Enter your password again to confirm it matches the value entered above" ResourceKey="Confirm"></Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" required />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglepassword</button>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Your email address where you wish to receive notifications" ResourceKey="Email"></Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" maxlength="256" required />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="Your full name" ResourceKey="DisplayName"></Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" maxlength="50" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
@if (_allowsitelogin)
{
<br /> <br />
<button type="button" class="btn btn-primary" @onclick="Register">@Localizer["Register"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <br />
@if (_allowsitelogin) <NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
{ }
<br /><br /> </form>
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink> }
}
</form>
</NotAuthorized>
</AuthorizeView>
} }
} }
else else

View File

@ -63,7 +63,7 @@
private string _enabled = "True"; private string _enabled = "True";
private string _lastIndexedOn = ""; private string _lastIndexedOn = "";
private string _ignorePages = ""; private string _ignorePages = "";
private string _ignoreEntities = ""; private string _ignoreEntities = "File";
private string _minimumWordLength = "3"; private string _minimumWordLength = "3";
private string _ignoreWords = "the,be,to,of,and,a,i,in,that,have,it,for,not,on,with,he,as,you,do,at,this,but,his,by,from,they,we,say,her,she,or,an,will,my,one,all,would,there,their,what,so,up,out,if,about,who,get,which,go,me,when,make,can,like,time,no,just,him,know,take,people,into,year,your,good,some,could,them,see,other,than,then,now,look,only,come,its,over,think,also,back,after,use,two,how,our,work,first,well,way,even,new,want,because,any,these,give,day,most,us"; private string _ignoreWords = "the,be,to,of,and,a,i,in,that,have,it,for,not,on,with,he,as,you,do,at,this,but,his,by,from,they,we,say,her,she,or,an,will,my,one,all,would,there,their,what,so,up,out,if,about,who,get,which,go,me,when,make,can,like,time,no,just,him,know,take,people,into,year,your,good,some,could,them,see,other,than,then,now,look,only,come,its,over,think,also,back,after,use,two,how,our,work,first,well,way,even,new,want,because,any,these,give,day,most,us";
@ -85,7 +85,7 @@
{ {
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "Search_SearchProvider", _searchProvider); settings = SettingService.SetSetting(settings, "Search_SearchProvider", _searchProvider);
settings = SettingService.SetSetting(settings, "Search_Enabled", _enabled, true); settings = SettingService.SetSetting(settings, "Search_Enabled", _enabled);
settings = SettingService.SetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn, true); settings = SettingService.SetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn, true);
settings = SettingService.SetSetting(settings, "Search_IgnorePages", _ignorePages, true); settings = SettingService.SetSetting(settings, "Search_IgnorePages", _ignorePages, true);
settings = SettingService.SetSetting(settings, "Search_IgnoreEntities", _ignoreEntities, true); settings = SettingService.SetSetting(settings, "Search_IgnoreEntities", _ignoreEntities, true);
@ -106,9 +106,7 @@
try try
{ {
_lastIndexedOn = DateTime.MinValue.ToString(); _lastIndexedOn = DateTime.MinValue.ToString();
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); await Save();
settings = SettingService.SetSetting(settings, "Search_LastIndexedOn", _lastIndexedOn, true);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Message.Reindex"], MessageType.Success); AddModuleMessage(Localizer["Message.Reindex"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -207,14 +207,14 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label> <Label Class="col-sm-3" For="username" HelpText="Enter the username for your SMTP account" ResourceKey="SmtpUsername">Username: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" class="form-control" @bind="@_smtpusername" /> <input id="username" class="form-control" @bind="@_smtpusername" autocomplete="off"/>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label> <Label Class="col-sm-3" For="password" HelpText="Enter the password for your SMTP account" ResourceKey="SmtpPassword">Password: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" /> <input id="password" type="@_smtppasswordtype" class="form-control" @bind="@_smtppassword" autocomplete="off"/>
<button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button> <button type="button" class="btn btn-secondary" @onclick="@ToggleSMTPPassword" tabindex="-1">@_togglesmtppassword</button>
</div> </div>
</div> </div>
@ -376,7 +376,7 @@
<Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation"> <Section Name="TenantInformation" Heading="Database" ResourceKey="TenantInformation">
<div class="container"> <div class="container">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The name of the database used for the site" ResourceKey="Tenant">Database: </Label> <Label Class="col-sm-3" For="tenant" HelpText="The name of the database used for the site. Note that this is not the physical database name but rather the tenant name which is used within the framework to identify a database." ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="tenant" class="form-control" @bind="@_tenant" readonly /> <input id="tenant" class="form-control" @bind="@_tenant" readonly />
</div> </div>
@ -388,7 +388,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="connectionstring" HelpText="The connection information for the database" ResourceKey="ConnectionString">Connection: </Label> <Label Class="col-sm-3" For="connectionstring" HelpText="The name of the connection string in appsettings.json which will be used to connect to the database" ResourceKey="ConnectionString">Connection: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="connectionstring" class="form-control" @bind="@_connectionstring" readonly /> <input id="connectionstring" class="form-control" @bind="@_connectionstring" readonly />
</div> </div>
@ -571,7 +571,7 @@
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
_database = _databases.Find(item => item.DBType == tenant.DBType)?.Name; _database = _databases.Find(item => item.DBType == tenant.DBType && item.Name != "LocalDB")?.Name;
_connectionstring = tenant.DBConnectionString; _connectionstring = tenant.DBConnectionString;
} }
} }

View File

@ -109,7 +109,7 @@ else
<hr class="app-rule" /> <hr class="app-rule" />
</div> </div>
<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 name for the database" ResourceKey="TenantName">Name: </Label> <Label Class="col-sm-3" For="name" HelpText="Enter the name for the database. Note that this will be the tenant name which is used within the framework to identify the database." ResourceKey="TenantName">Name: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="name" class="form-control" @bind="@_tenantName" maxlength="100" required /> <input id="name" class="form-control" @bind="@_tenantName" maxlength="100" required />
</div> </div>

View File

@ -83,24 +83,15 @@ else
{ {
@if (_connection != "-") @if (_connection != "-")
{ {
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
@if (_databases != null)
{
<select id="databasetype" class="form-select" @bind="@_databasetype" required>
<option value="-">&lt;@Localizer["Type.Select"]&gt;</option>
@foreach (var database in _databases)
{
<option value="@database.Name">@Localizer[@database.Name]</option>
}
</select>
}
</div>
</div>
@if (!string.IsNullOrEmpty(_tenant)) @if (!string.IsNullOrEmpty(_tenant))
{ {
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="databasetype" HelpText="The database type" ResourceKey="DatabaseType">Type: </Label>
<div class="col-sm-9">
<input id="databasetype" class="form-control" @bind="@_databasetype" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label> <Label Class="col-sm-3" For="tenant" HelpText="The database using this connection" ResourceKey="Tenant">Database: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="tenant" class="form-control" @bind="@_tenant" readonly /> <input id="tenant" class="form-control" @bind="@_tenant" readonly />
@ -204,12 +195,12 @@ else
{ {
_connectionstring = _connections[_connection].ToString(); _connectionstring = _connections[_connection].ToString();
_tenant = ""; _tenant = "";
_databasetype = "-"; _databasetype = "";
var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection); var tenant = _tenants.FirstOrDefault(item => item.DBConnectionString == _connection);
if (tenant != null) if (tenant != null)
{ {
_tenant = tenant.Name; _tenant = tenant.Name;
_databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType).Name; _databasetype = _databases.FirstOrDefault(item => item.DBType == tenant.DBType && item.Name != "LocalDB").Name;
} }
} }
else else

View File

@ -54,6 +54,8 @@
} }
else else
{ {
AddModuleMessage(Localizer["Disclaimer.Text"], MessageType.Warning);
List<Package> packages = await PackageService.GetPackagesAsync("framework", "", "", ""); List<Package> packages = await PackageService.GetPackagesAsync("framework", "", "", "");
if (packages != null) if (packages != null)
{ {
@ -97,13 +99,16 @@
{ {
try try
{ {
ShowProgressIndicator();
await PackageService.DownloadPackageAsync(packageid, version); await PackageService.DownloadPackageAsync(packageid, version);
await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version); await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version);
HideProgressIndicator();
AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success); AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Downloading Framework Package {Error}", ex.Message); await logger.LogError(ex, "Error Downloading Framework Package {Error}", ex.Message);
HideProgressIndicator();
AddModuleMessage(Localizer["Error.Framework.Download"], MessageType.Error); AddModuleMessage(Localizer["Error.Framework.Download"], MessageType.Error);
} }
} }

View File

@ -9,6 +9,8 @@
@inject INotificationService NotificationService @inject INotificationService NotificationService
@inject IFileService FileService @inject IFileService FileService
@inject IFolderService FolderService @inject IFolderService FolderService
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -84,6 +86,7 @@
<br /> <br />
<button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="Save">@SharedLocalizer["Save"]</button>
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button> <button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
<button type="button" class="btn btn-danger" @onclick="Logout">@Localizer["Logout Everywhere"]</button>
</TabPanel> </TabPanel>
<TabPanel Name="Profile" ResourceKey="Profile"> <TabPanel Name="Profile" ResourceKey="Profile">
<div class="container"> <div class="container">
@ -518,6 +521,32 @@
} }
} }
private async Task Logout()
{
await logger.LogInformation("User Logout Everywhere For Username {Username}", PageState.User?.Username);
var url = NavigateUrl(""); // home page
if (PageState.Runtime == Shared.Runtime.Hybrid)
{
if (PageState.User != null)
{
// hybrid apps utilize an interactive logout
await UserService.LogoutUserEverywhereAsync(PageState.User);
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
authstateprovider.NotifyAuthenticationChanged();
NavigationManager.NavigateTo(url, true);
}
}
else
{
// post to the Logout page to complete the logout process
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, returnurl = url, everywhere = true };
var interop = new Interop(jsRuntime);
await interop.SubmitForm(Utilities.TenantUrl(PageState.Alias, "/pages/logout/"), fields);
}
}
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in profiles)

View File

@ -182,13 +182,31 @@ else
</div> </div>
</Section> </Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings"> <Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="provider" HelpText="Select the external login provider" ResourceKey="Provider">Provider:</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="provider" class="form-select" value="@_provider" @onchange="(e => ProviderChanged(e))">
@foreach (var provider in Shared.ExternalLoginProviders.Providers)
{
<option value="@provider.Name">@Localizer[provider.Name]</option>
}
</select>
@if (!string.IsNullOrEmpty(_providerurl))
{
<a href="@_providerurl" class="btn btn-secondary" target="_new">@Localizer["Info"]</a>
}
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label> <Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))"> <select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>@Localizer["Not Specified"]</option> <option value="" selected>&lt;@Localizer["Not Specified"]&gt;</option>
<option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OpenID Connect"]</option> <option value="@AuthenticationProviderTypes.OpenIDConnect">@Localizer["OIDC"]</option>
<option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth 2.0"]</option> <option value="@AuthenticationProviderTypes.OAuth2">@Localizer["OAuth2"]</option>
</select> </select>
</div> </div>
</div> </div>
@ -333,12 +351,29 @@ else
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the role claim provided by the provider" ResourceKey="RoleClaimType">Role Claim:</Label> <Label Class="col-sm-3" For="roleclaimtype" HelpText="The name of the roles claim provided by the provider" ResourceKey="RoleClaimType">Roles Claim:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" /> <input id="roleclaimtype" class="form-control" @bind="@_roleclaimtype" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="roleclaimmappings" HelpText="Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles." ResourceKey="RoleClaimMappings">Role Claim Mappings:</Label>
<div class="col-sm-9">
<input id="roleclaimmappings" class="form-control" @bind="@_roleclaimmappings" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="synchronizeroles" HelpText="This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider" ResourceKey="SynchronizeRoles">Synchronize Roles?</Label>
<div class="col-sm-9">
<div class="input-group">
<select id="synchronizeroles" class="form-select" @bind="@_synchronizeroles" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label> <Label Class="col-sm-3" For="profileclaimtypes" HelpText="A comma delimited list of user profile claims provided by the provider, as well as mappings to your user profile definition. For example if the provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'." ResourceKey="ProfileClaimTypes">User Profile Claims:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" /> <input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
@ -435,6 +470,8 @@ else
private string _maximumfailures; private string _maximumfailures;
private string _lockoutduration; private string _lockoutduration;
private string _provider;
private string _providerurl;
private string _providertype; private string _providertype;
private string _providername; private string _providername;
private string _authority; private string _authority;
@ -457,6 +494,8 @@ else
private string _nameclaimtype; private string _nameclaimtype;
private string _emailclaimtype; private string _emailclaimtype;
private string _roleclaimtype; private string _roleclaimtype;
private string _roleclaimmappings;
private string _synchronizeroles;
private string _profileclaimtypes; private string _profileclaimtypes;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
@ -500,31 +539,7 @@ else
_maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5"); _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5");
_lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString(); _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString();
_providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", ""); LoadExternalLoginSettings(settings);
_providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
_authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
_metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", "");
_authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", "");
_tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", "");
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false");
_externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external");
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
_secret = SettingService.GetSetting(settings, "JwtOptions:Secret", ""); _secret = SettingService.GetSetting(settings, "JwtOptions:Secret", "");
_togglesecret = SharedLocalizer["ShowPassword"]; _togglesecret = SharedLocalizer["ShowPassword"];
@ -534,6 +549,39 @@ else
} }
} }
private void LoadExternalLoginSettings(Dictionary<string, string> settings)
{
_provider = SettingService.GetSetting(settings, "ExternalLogin:Provider", "<Custom>");
_providerurl = SettingService.GetSetting(settings, "ExternalLogin:ProviderUrl", "");
_providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", "");
_providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", "");
_authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", "");
_metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", "");
_authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", "");
_tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", "");
_userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", "");
_clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", "");
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_authresponsetype = SettingService.GetSetting(settings, "ExternalLogin:AuthResponseType", "code");
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_reviewclaims = SettingService.GetSetting(settings, "ExternalLogin:ReviewClaims", "false");
_externalloginurl = Utilities.TenantUrl(PageState.Alias, "/pages/external");
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "sub");
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
_roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", "");
_synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
}
private async Task LoadUsersAsync(bool load) private async Task LoadUsersAsync(bool load)
{ {
if (load) if (load)
@ -546,103 +594,121 @@ else
users = users.OrderBy(u => u.User.DisplayName).ToList(); users = users.OrderBy(u => u.User.DisplayName).ToList();
} }
} }
} }
private async Task DeleteUser(UserRole UserRole) private async Task DeleteUser(UserRole UserRole)
{ {
try try
{ {
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User); await logger.LogInformation("User Deleted {User}", UserRole.User);
await LoadUsersAsync(true); await LoadUsersAsync(true);
StateHasChanged(); StateHasChanged();
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(ex.Message, MessageType.Error);
} }
} }
private async Task SaveSiteSettings() private async Task SaveSiteSettings()
{ {
try try
{ {
var site = PageState.Site; var site = PageState.Site;
site.AllowRegistration = bool.Parse(_allowregistration); site.AllowRegistration = bool.Parse(_allowregistration);
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false);
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true);
settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true);
settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false); settings = SettingService.SetSetting(settings, "LoginOptions:AlwaysRemember", _alwaysremember, false);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredLength", _minimumlength, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", _uniquecharacters, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireDigit", _requiredigit, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireUppercase", _requireupper, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireLowercase", _requirelower, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true);
settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false); settings = SettingService.SetSetting(settings, "ExternalLogin:Provider", _provider, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false); settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false);
settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true); settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true); settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:AuthResponseType", _authresponsetype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true); settings = SettingService.SetSetting(settings, "ExternalLogin:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ReviewClaims", _reviewclaims, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:NameClaimType", _nameclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimType", _roleclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true); settings = SettingService.SetSetting(settings, "JwtOptions:Audience", _audience, true);
settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true); settings = SettingService.SetSetting(settings, "JwtOptions:Lifetime", _lifetime, true);
} }
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
await SettingService.ClearSiteSettingsCacheAsync(); await SettingService.ClearSiteSettingsCacheAsync();
if (!string.IsNullOrEmpty(_secret)) if (!string.IsNullOrEmpty(_secret))
{ {
SiteState.AuthorizationToken = await UserService.GetTokenAsync(); SiteState.AuthorizationToken = await UserService.GetTokenAsync();
} }
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message); await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error); AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
} }
finally
{
await ScrollToPageTop();
}
}
private void ProviderChanged(ChangeEventArgs e)
{
_provider = (string)e.Value;
var provider = Shared.ExternalLoginProviders.Providers.FirstOrDefault(item => item.Name == _provider);
if (provider != null)
{
LoadExternalLoginSettings(provider.Settings);
}
StateHasChanged();
} }
private void ProviderTypeChanged(ChangeEventArgs e) private void ProviderTypeChanged(ChangeEventArgs e)
{ {
_providertype = (string)e.Value; _providertype = (string)e.Value;
if (string.IsNullOrEmpty(_providername)) if (string.IsNullOrEmpty(_providername))

View File

@ -196,7 +196,7 @@
else else
{ {
FolderId = -1; FolderId = -1;
_message = "Folder Path " + Folder + "Does Not Exist"; _message = "Folder Path " + Folder + " Does Not Exist";
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
} }
@ -226,9 +226,9 @@
} }
else else
{ {
FileId = -1; // file does not exist _message = "FileId " + FileId.ToString() + " Does Not Exist";
_message = "FileId " + FileId.ToString() + "Does Not Exist";
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
FileId = -1; // file does not exist
} }
} }
@ -359,12 +359,6 @@
} }
if (restricted == "") if (restricted == "")
{ {
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
try try
{ {
// upload the files // upload the files
@ -374,7 +368,21 @@
if (PageState.Runtime == Shared.Runtime.Hybrid) if (PageState.Runtime == Shared.Runtime.Hybrid)
{ {
jwt = await UserService.GetTokenAsync(); jwt = await UserService.GetTokenAsync();
if (string.IsNullOrEmpty(jwt))
{
await logger.LogInformation("File Upload Failed From .NET MAUI Due To Missing Security Token. Token Options Must Be Set In User Settings.");
_message = "Security Token Not Specified";
_messagetype = MessageType.Error;
return;
}
} }
if (!ShowProgress)
{
_uploading = true;
StateHasChanged();
}
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt); await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt);
// uploading is asynchronous so we need to poll to determine if uploads are completed // uploading is asynchronous so we need to poll to determine if uploads are completed
@ -387,7 +395,7 @@
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion var megabits = (size / 1048576.0) * 8; // binary conversion
var uploadspeed = 2; // 2 Mbps (3G ranges from 300Kbps to 3Mbps) var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
var uploadtime = (megabits / uploadspeed); // seconds var uploadtime = (megabits / uploadspeed); // seconds
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds) var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds

View File

@ -10,13 +10,16 @@
{ {
<NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink> <NavLink class="ms-2" href="@NavigateUrl("admin/log")">View Details</NavLink>
} }
@if (ModuleState.RenderMode == RenderModes.Static) @if (ModuleState != null)
{ {
<a href="@NavigationManager.Uri" class="btn-close" data-dismiss="alert" aria-label="close"></a> @if (ModuleState.RenderMode == RenderModes.Static)
} {
else <a href="@NavigationManager.Uri" class="btn-close" data-dismiss="alert" aria-label="close"></a>
{ }
<button type="button" class="btn-close" data-dismiss="alert" aria-label="close" @onclick="CloseMessage"></button> else
{
<button type="button" class="btn-close" data-dismiss="alert" aria-label="close" @onclick="CloseMessage"></button>
}
} }
</div> </div>
} }

View File

@ -452,9 +452,9 @@
_displayPages = int.Parse(DisplayPages); _displayPages = int.Parse(DisplayPages);
} }
if (PageState.QueryString.ContainsKey("page")) if (PageState.QueryString.ContainsKey("page") && int.TryParse(PageState.QueryString["page"], out int page))
{ {
_page = int.Parse(PageState.QueryString["page"]); _page = page;
} }
else else
{ {

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>5.2.2</Version> <Version>6.0.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -12,7 +12,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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -22,11 +22,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
@ -13,13 +12,13 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop; using Microsoft.JSInterop;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Modules; using Oqtane.Modules;
using Oqtane.Services; using Oqtane.Services;
using Oqtane.Shared;
using Oqtane.UI; using Oqtane.UI;
namespace Oqtane.Client namespace Oqtane.Client
@ -258,7 +257,7 @@ namespace Oqtane.Client
var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>(); var jsRuntime = serviceProvider.GetRequiredService<IJSRuntime>();
var interop = new Interop(jsRuntime); var interop = new Interop(jsRuntime);
var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName); var localizationCookie = await interop.GetCookie(CookieRequestCultureProvider.DefaultCookieName);
var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICultures?[0].Value; var culture = CookieRequestCultureProvider.ParseCookieValue(localizationCookie)?.UICulture.Name;
var localizationService = serviceProvider.GetRequiredService<ILocalizationService>(); var localizationService = serviceProvider.GetRequiredService<ILocalizationService>();
var cultures = await localizationService.GetCulturesAsync(false); var cultures = await localizationService.GetCulturesAsync(false);

View File

@ -183,4 +183,7 @@
<data name="Template" xml:space="preserve"> <data name="Template" xml:space="preserve">
<value>Select a site template</value> <value>Select a site template</value>
</data> </data>
<data name="Message.Username.Invalid" xml:space="preserve">
<value>The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits.</value>
</data>
</root> </root>

View File

@ -139,7 +139,7 @@
<value>Ignore Entities: </value> <value>Ignore Entities: </value>
</data> </data>
<data name="IgnoreEntities.HelpText" xml:space="preserve"> <data name="IgnoreEntities.HelpText" xml:space="preserve">
<value>Comma delimited list of entities which should be ignored</value> <value>Comma delimited list of entities which should be ignored. By default File entities are ignored.</value>
</data> </data>
<data name="MinimumWordLength.Text" xml:space="preserve"> <data name="MinimumWordLength.Text" xml:space="preserve">
<value>Word Length: </value> <value>Word Length: </value>
@ -154,7 +154,7 @@
<value>Comma delimited list of words which should be ignored</value> <value>Comma delimited list of words which should be ignored</value>
</data> </data>
<data name="Success.Save" xml:space="preserve"> <data name="Success.Save" xml:space="preserve">
<value>Search Settings Saved Successfully</value> <value>Search Settings Saved Successfully. You Will Need Reindex For Your Changes To Be Reflected In The Search Results.</value>
</data> </data>
<data name="Error.Save" xml:space="preserve"> <data name="Error.Save" xml:space="preserve">
<value>Error Saving Search Settings</value> <value>Error Saving Search Settings</value>

View File

@ -163,7 +163,7 @@
<value>Enter the site name</value> <value>Enter the site name</value>
</data> </data>
<data name="Tenant.HelpText" xml:space="preserve"> <data name="Tenant.HelpText" xml:space="preserve">
<value>The name of the database used for the site</value> <value>The name of the database used for the site. Note that this is not the physical database name but rather the tenant name which is used within the framework to identify a database.</value>
</data> </data>
<data name="Aliases.HelpText" xml:space="preserve"> <data name="Aliases.HelpText" xml:space="preserve">
<value>The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value> <value>The urls for the site. This can include domain names (ie. domain.com), subdomains (ie. sub.domain.com) or virtual folders (ie. domain.com/folder).</value>
@ -307,7 +307,7 @@
<value>Type:</value> <value>Type:</value>
</data> </data>
<data name="ConnectionString.HelpText" xml:space="preserve"> <data name="ConnectionString.HelpText" xml:space="preserve">
<value>The connection information for the database</value> <value>The name of the connection string in appsettings.json which will be used to connect to the database</value>
</data> </data>
<data name="Database.HelpText" xml:space="preserve"> <data name="Database.HelpText" xml:space="preserve">
<value>The type of database</value> <value>The type of database</value>

View File

@ -187,7 +187,7 @@
<value>Select the database for the site</value> <value>Select the database for the site</value>
</data> </data>
<data name="TenantName.HelpText" xml:space="preserve"> <data name="TenantName.HelpText" xml:space="preserve">
<value>Enter the name for the database</value> <value>Enter the name for the database. Note that this will be the tenant name which is used within the framework to identify the database.</value>
</data> </data>
<data name="DatabaseType.HelpText" xml:space="preserve"> <data name="DatabaseType.HelpText" xml:space="preserve">
<value>Select the database type</value> <value>Select the database type</value>

View File

@ -150,4 +150,7 @@
<data name="Localhost.Text" xml:space="preserve"> <data name="Localhost.Text" xml:space="preserve">
<value>You Cannot Perform A System Update In A Development Environment</value> <value>You Cannot Perform A System Update In A Development Environment</value>
</data> </data>
<data name="Disclaimer.Text" xml:space="preserve">
<value>Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. Also Note That The System Update Capability Is Not Recommended When Using Microsoft Azure Due To The Limitations Of That Environment. </value>
</data>
</root> </root>

View File

@ -243,4 +243,7 @@
<data name="NoNotificationsSent.Text" xml:space="preserve"> <data name="NoNotificationsSent.Text" xml:space="preserve">
<value>No notifications have been sent</value> <value>No notifications have been sent</value>
</data> </data>
<data name="Logout Everywhere" xml:space="preserve">
<value>Logout Everywhere</value>
</data>
</root> </root>

View File

@ -385,10 +385,22 @@
<value>Parameters:</value> <value>Parameters:</value>
</data> </data>
<data name="RoleClaimType.HelpText" xml:space="preserve"> <data name="RoleClaimType.HelpText" xml:space="preserve">
<value>Optionally provide the type name of the role claim provided by the identity provider. These roles will be used in addition to any internal user roles assigned within the site.</value> <value>Optionally provide the type name of the roles claim provided by the identity provider (the standard default is 'roles'). If role names from the identity provider do not exactly match your site role names, please use the Role Claim Mappings.</value>
</data> </data>
<data name="RoleClaimType.Text" xml:space="preserve"> <data name="RoleClaimType.Text" xml:space="preserve">
<value>Role Claim:</value> <value>Roles Claim:</value>
</data>
<data name="RoleClaimMappings.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of role names provided by the identity provider, as well as mappings to your site roles. For example if the identity provider includes an 'Admin' role name and you want it to map to the 'Administrators' site role you should specify 'Admin:Administrators'.</value>
</data>
<data name="RoleClaimMappings.Text" xml:space="preserve">
<value>Role Claim Mappings:</value>
</data>
<data name="SynchronizeRoles.HelpText" xml:space="preserve">
<value>This option will add or remove role assignments so that the site roles exactly match the roles provided by the identity provider for a user</value>
</data>
<data name="SynchronizeRoles.Text" xml:space="preserve">
<value>Synchronize Roles?</value>
</data> </data>
<data name="ProfileClaimTypes.HelpText" xml:space="preserve"> <data name="ProfileClaimTypes.HelpText" xml:space="preserve">
<value>Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value> <value>Optionally provide a comma delimited list of user profile claim type names provided by the identity provider, as well as mappings to your user profile definition. For example if the identity provider includes a 'given_name' claim and you have a 'FirstName' user profile definition you should specify 'given_name:FirstName'.</value>
@ -459,13 +471,28 @@
<data name="ReviewClaims.Text" xml:space="preserve"> <data name="ReviewClaims.Text" xml:space="preserve">
<value>Review Claims?</value> <value>Review Claims?</value>
</data> </data>
<data name="ReviewClaims.HelpText" xml:space="preserve"> <data name="ReviewClaims.HelpText" xml:space="preserve">
<value>This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled.</value> <value>This option will record the full list of Claims returned by the Provider in the Event Log. It should only be used for testing purposes. External Login will be restricted when this option is enabled.</value>
</data> </data>
<data name="NameClaimType.HelpText" xml:space="preserve"> <data name="NameClaimType.HelpText" xml:space="preserve">
<value>Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'.</value> <value>Optionally specify the type name of the user's name claim provided by the identity provider. The typical value is 'name'.</value>
</data> </data>
<data name="NameClaimType.Text" xml:space="preserve"> <data name="NameClaimType.Text" xml:space="preserve">
<value>Name Claim:</value> <value>Name Claim:</value>
</data> </data>
<data name="Provider.HelpText" xml:space="preserve">
<value>Select the external login provider</value>
</data>
<data name="Provider.Text" xml:space="preserve">
<value>Provider:</value>
</data>
<data name="Info" xml:space="preserve">
<value>Info</value>
</data>
<data name="OAuth2" xml:space="preserve">
<value>OAuth 2.0</value>
</data>
<data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value>
</data>
</root> </root>

View File

@ -0,0 +1,17 @@
using System.Threading.Tasks;
namespace Oqtane.Services
{
/// <summary>
/// Service to set localization cookie
/// </summary>
public interface ILocalizationCookieService
{
/// <summary>
/// Set the localization cookie
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
Task SetLocalizationCookieAsync(string culture);
}
}

View File

@ -75,6 +75,13 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task LogoutUserAsync(User user); Task LogoutUserAsync(User user);
/// <summary>
/// Logout a <see cref="User"/>
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
Task LogoutUserEverywhereAsync(User user);
/// <summary> /// <summary>
/// Update e-mail verification status of a user. /// Update e-mail verification status of a user.
/// </summary> /// </summary>
@ -106,6 +113,15 @@ namespace Oqtane.Services
/// <returns></returns> /// <returns></returns>
Task<User> VerifyTwoFactorAsync(User user, string token); Task<User> VerifyTwoFactorAsync(User user, string token);
/// <summary>
/// Validate identity user info.
/// </summary>
/// <param name="username"></param>
/// <param name="email"></param>
/// <param name="password"></param>
/// <returns></returns>
Task<UserValidateResult> ValidateUserAsync(string username, string email, string password);
/// <summary> /// <summary>
/// Validate a users password against the password policy /// Validate a users password against the password policy
/// </summary> /// </summary>

View File

@ -0,0 +1,18 @@
using System.Net.Http;
using System.Threading.Tasks;
using Oqtane.Documentation;
using Oqtane.Shared;
namespace Oqtane.Services
{
[PrivateApi("Don't show in the documentation, as everything should use the Interface")]
public class LocalizationCookieService : ServiceBase, ILocalizationCookieService
{
public LocalizationCookieService(HttpClient http, SiteState siteState) : base(http, siteState) { }
public Task SetLocalizationCookieAsync(string culture)
{
return Task.CompletedTask; // only used in server side rendering
}
}
}

View File

@ -4,7 +4,6 @@ using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Net.Http.Headers;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Services namespace Oqtane.Services
@ -28,9 +27,9 @@ namespace Oqtane.Services
private HttpClient GetHttpClient(string AuthorizationToken) private HttpClient GetHttpClient(string AuthorizationToken)
{ {
var httpClient = _httpClientFactory.CreateClient("Remote"); var httpClient = _httpClientFactory.CreateClient("Remote");
if (!httpClient.DefaultRequestHeaders.Contains(HeaderNames.Authorization) && !string.IsNullOrEmpty(AuthorizationToken)) if (!httpClient.DefaultRequestHeaders.Contains("Authorization") && !string.IsNullOrEmpty(AuthorizationToken))
{ {
httpClient.DefaultRequestHeaders.Add(HeaderNames.Authorization, "Bearer " + AuthorizationToken); httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer " + AuthorizationToken);
} }
return httpClient; return httpClient;
} }

View File

@ -61,10 +61,14 @@ namespace Oqtane.Services
public async Task LogoutUserAsync(User user) public async Task LogoutUserAsync(User user)
{ {
// best practices recommend post is preferrable to get for logout
await PostJsonAsync($"{Apiurl}/logout", user); await PostJsonAsync($"{Apiurl}/logout", user);
} }
public async Task LogoutUserEverywhereAsync(User user)
{
await PostJsonAsync($"{Apiurl}/logouteverywhere", user);
}
public async Task<User> VerifyEmailAsync(User user, string token) public async Task<User> VerifyEmailAsync(User user, string token)
{ {
return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user); return await PostJsonAsync<User>($"{Apiurl}/verify?token={token}", user);
@ -85,6 +89,11 @@ namespace Oqtane.Services
return await PostJsonAsync<User>($"{Apiurl}/twofactor?token={token}", user); return await PostJsonAsync<User>($"{Apiurl}/twofactor?token={token}", user);
} }
public async Task<UserValidateResult> ValidateUserAsync(string username, string email, string password)
{
return await GetJsonAsync<UserValidateResult>($"{Apiurl}/validateuser?username={WebUtility.UrlEncode(username)}&email={WebUtility.UrlEncode(email)}&password={WebUtility.UrlEncode(password)}");
}
public async Task<bool> ValidatePasswordAsync(string password) public async Task<bool> ValidatePasswordAsync(string password)
{ {
return await GetJsonAsync<bool>($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}"); return await GetJsonAsync<bool>($"{Apiurl}/validate/{WebUtility.UrlEncode(password)}");

View File

@ -9,7 +9,7 @@
<div class="row flex-xl-nowrap gx-0"> <div class="row flex-xl-nowrap gx-0">
<div class="sidebar"> <div class="sidebar">
<nav class="navbar"> <nav class="navbar">
<Logo /> <Logo UseSiteNameAsFallback="true" />
<Menu Orientation="Vertical" /> <Menu Orientation="Vertical" />
</nav> </nav>
</div> </div>

View File

@ -11,9 +11,6 @@ using System.Net;
using Microsoft.Extensions.Localization; using Microsoft.Extensions.Localization;
using Oqtane.UI; using Oqtane.UI;
// ReSharper disable UnassignedGetOnlyAutoProperty
// ReSharper disable MemberCanBePrivate.Global
namespace Oqtane.Themes.Controls namespace Oqtane.Themes.Controls
{ {
public class ModuleActionsBase : ComponentBase public class ModuleActionsBase : ComponentBase
@ -92,20 +89,21 @@ namespace Oqtane.Themes.Controls
return actionList; return actionList;
} }
private async Task<string> EditUrlAsync(string url, int moduleId, string import)
{
await Task.Yield();
return Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, moduleId, import, "");
}
protected async Task ModuleAction(ActionViewModel action) protected async Task ModuleAction(ActionViewModel action)
{ {
if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList)) if (PageState.EditMode && UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, ModuleState.PermissionList))
{ {
PageModule pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); var url = NavigationManager.Uri.Substring(NavigationManager.BaseUri.Length - 1);
if (!url.Contains("edit="))
string url = Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "edit=true&refresh"); {
url += (!url.Contains("?") ? "?" : "&") + "edit=true";
}
if (!url.Contains("refresh="))
{
url += (!url.Contains("?") ? "?" : "&") + "refresh=true";
}
var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId);
if (action.Action != null) if (action.Action != null)
{ {
url = await action.Action(url, pagemodule); url = await action.Action(url, pagemodule);
@ -115,31 +113,10 @@ namespace Oqtane.Themes.Controls
} }
} }
private async Task<string> MoveToPane(string url, string newPane, PageModule pagemodule) private Task<string> Settings(string url, PageModule pagemodule)
{ {
string oldPane = pagemodule.Pane; url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, pagemodule.ModuleId, "Settings", "returnurl=" + WebUtility.UrlEncode(url));
pagemodule.Pane = newPane; return Task.FromResult(url);
pagemodule.Order = int.MaxValue; // add to bottom of pane
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, oldPane);
return url;
}
private async Task<string> DeleteModule(string url, PageModule pagemodule)
{
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
return url;
}
private async Task<string> Settings(string url, PageModule pagemodule)
{
await Task.Yield();
var returnurl = Utilities.NavigateUrl(PageState.Alias.Path, PageState.Page.Path, "edit=true");
url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, pagemodule.ModuleId, "Settings", "returnurl=" + WebUtility.UrlEncode(returnurl));
return url;
} }
private async Task<string> Publish(string url, PageModule pagemodule) private async Task<string> Publish(string url, PageModule pagemodule)
@ -174,6 +151,20 @@ namespace Oqtane.Themes.Controls
return url; return url;
} }
private async Task<string> DeleteModule(string url, PageModule pagemodule)
{
pagemodule.IsDeleted = true;
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
return url;
}
private Task<string> EditUrlAsync(string url, int moduleId, string import)
{
url = Utilities.EditUrl(PageState.Alias.Path, PageState.Page.Path, moduleId, import, "returnurl=" + WebUtility.UrlEncode(url));
return Task.FromResult(url);
}
private async Task<string> MoveTop(string url, PageModule pagemodule) private async Task<string> MoveTop(string url, PageModule pagemodule)
{ {
pagemodule.Order = 0; pagemodule.Order = 0;
@ -206,6 +197,17 @@ namespace Oqtane.Themes.Controls
return url; return url;
} }
private async Task<string> MoveToPane(string url, string newPane, PageModule pagemodule)
{
string oldPane = pagemodule.Pane;
pagemodule.Pane = newPane;
pagemodule.Order = int.MaxValue; // add to bottom of pane
await PageModuleService.UpdatePageModuleAsync(pagemodule);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane);
await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, oldPane);
return url;
}
public class ActionViewModel public class ActionViewModel
{ {
public string Icon { get; set; } public string Icon { get; set; }

View File

@ -147,8 +147,7 @@
{ {
if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered)) if (PageState.Page.IsPersonalizable && PageState.User != null && UserSecurity.IsAuthorized(PageState.User, RoleNames.Registered))
{ {
PageState.EditMode = true; NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString()));
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + ((PageState.EditMode) ? "true" : "false")));
} }
} }
} }

View File

@ -454,7 +454,7 @@
{ {
foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission)) foreach (var permission in PageState.Page.PermissionList.Where(item => item.PermissionName == pagePermission))
{ {
permissions.Add(new Permission { SiteId = siteId, EntityName = EntityNames.Module, PermissionName = modulePermission, RoleId = permission.RoleId, UserId = permission.UserId, IsAuthorized = permission.IsAuthorized }); permissions.Add(new Permission { SiteId = siteId, EntityName = EntityNames.Module, PermissionName = modulePermission, RoleName = permission.RoleName, UserId = permission.UserId, IsAuthorized = permission.IsAuthorized });
} }
return permissions; return permissions;
} }

View File

@ -1,10 +1,9 @@
@using System.Globalization @using System.Globalization
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Http
@using Oqtane.Models @using Oqtane.Models
@namespace Oqtane.Themes.Controls @namespace Oqtane.Themes.Controls
@inherits ThemeControlBase @inherits ThemeControlBase
@inject ILanguageService LanguageService @inject ILanguageService LanguageService
@inject ILocalizationCookieService LocalizationCookieService
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@if (_supportedCultures?.Count() > 1) @if (_supportedCultures?.Count() > 1)
@ -22,7 +21,7 @@
} }
else else
{ {
<a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)">@culture.DisplayName</a> <a class="dropdown-item @(CultureInfo.CurrentUICulture.Name == culture.Name ? "active" : String.Empty)" href="@NavigateUrl(PageState.Page.Path, "culture=" + culture.Name)" data-enhance-nav="false">@culture.DisplayName</a>
} }
} }
</div> </div>
@ -38,25 +37,20 @@
[Parameter] [Parameter]
public string ButtonClass { get; set; } = "btn-outline-secondary"; public string ButtonClass { get; set; } = "btn-outline-secondary";
[CascadingParameter] protected override async Task OnParametersSetAsync()
HttpContext HttpContext { get; set; }
protected override void OnParametersSet()
{ {
MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty; MenuAlignment = DropdownAlignment.ToLower() == "right" ? "dropdown-menu-end" : string.Empty;
var languages = PageState.Languages; _supportedCultures = PageState.Languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
_supportedCultures = languages.Select(l => new Culture { Name = l.Code, DisplayName = l.Name });
if (PageState.QueryString.ContainsKey("culture")) if (PageState.QueryString.ContainsKey("culture"))
{ {
var culture = PageState.QueryString["culture"]; var culture = PageState.QueryString["culture"];
if (_supportedCultures.Any(item => item.Name == culture)) if (_supportedCultures.Any(item => item.Name == culture))
{ {
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); await LocalizationCookieService.SetLocalizationCookieAsync(culture);
HttpContext.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, new CookieOptions { Path = "/", Expires = DateTimeOffset.UtcNow.AddYears(365) });
} }
NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""), forceLoad: true); NavigationManager.NavigateTo(NavigationManager.Uri.Replace($"?culture={culture}", ""));
} }
} }
@ -66,8 +60,8 @@
{ {
var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)); var localizationCookieValue = CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture));
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360); await interop.SetCookie(CookieRequestCultureProvider.DefaultCookieName, localizationCookieValue, 360, true, "Lax");
NavigationManager.NavigateTo(NavigationManager.Uri, forceLoad: true); NavigationManager.NavigateTo(NavigationManager.Uri, true);
} }
} }
} }

View File

@ -4,31 +4,28 @@
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
<span class="app-login"> <span class="app-login">
<AuthorizeView Roles="@RoleNames.Registered"> @if (PageState.User != null)
<Authorizing> {
<text>...</text> @if (PageState.Runtime == Runtime.Hybrid)
</Authorizing> {
<Authorized> <button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
@if (PageState.Runtime == Runtime.Hybrid) }
{ else
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button> {
} <form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
else <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
{ <input type="hidden" name="returnurl" value="@returnurl" />
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm"> <button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> </form>
<input type="hidden" name="returnurl" value="@returnurl" /> }
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button> }
</form> else
} {
</Authorized> @if (ShowLogin)
<NotAuthorized> {
@if (ShowLogin) <a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a>
{ }
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a> }
}
</NotAuthorized>
</AuthorizeView>
</span> </span>
@code @code

View File

@ -8,4 +8,19 @@
<img class="img-fluid" src="@Utilities.FileUrl(PageState.Alias, PageState.Site.LogoFileId.Value)" alt="@PageState.Site.Name" /> <img class="img-fluid" src="@Utilities.FileUrl(PageState.Alias, PageState.Site.LogoFileId.Value)" alt="@PageState.Site.Name" />
</a> </a>
</span> </span>
} }
else
{
if (UseSiteNameAsFallback)
{
<span class="app-logo">
<a class="navbar-brand" href="@PageState.Alias.Path">@PageState.Site.Name</a>
</span>
}
}
@code {
[Parameter]
public bool UseSiteNameAsFallback { get; set; } = false; // indicates if the site name should be displayed in scenarios where a site does not have a logo defined
}

View File

@ -18,7 +18,7 @@
placeholder="@Localizer["SearchPlaceHolder"]" placeholder="@Localizer["SearchPlaceHolder"]"
aria-label="Search" /> aria-label="Search" />
} }
<button type="submit" class="btn btn-search"> <button type="submit" class="btn btn-search" aria-label="Search Button">
<span class="oi oi-magnifying-glass align-middle"></span> <span class="oi oi-magnifying-glass align-middle"></span>
</button> </button>
</form> </form>

View File

@ -6,20 +6,17 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
<span class="app-profile"> <span class="app-profile">
<AuthorizeView Roles="@RoleNames.Registered"> @if (PageState.User != null)
<Authorizing> {
<text>...</text> <a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@PageState.User.Username</a>
</Authorizing> }
<Authorized> else
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@context.User.Identity.Name</a> {
</Authorized> @if (ShowRegister && PageState.Site.AllowRegistration)
<NotAuthorized> {
@if (ShowRegister && PageState.Site.AllowRegistration) <a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a>
{ }
<a href="@NavigateUrl("register", "returnurl=" + _returnurl)" class="btn btn-primary">@Localizer["Register"]</a> }
}
</NotAuthorized>
</AuthorizeView>
</span> </span>
@code { @code {

View File

@ -4,7 +4,7 @@
<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 /><Menu Orientation="Horizontal" /> <Logo UseSiteNameAsFallback="true" /><Menu Orientation="Horizontal" />
<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

@ -16,13 +16,18 @@ namespace Oqtane.UI
_jsRuntime = jsRuntime; _jsRuntime = jsRuntime;
} }
public Task SetCookie(string name, string value, int days) public async Task SetCookie(string name, string value, int days)
{
await SetCookie(name, value, days, true, "Lax");
}
public Task SetCookie(string name, string value, int days, bool secure, string sameSite)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.setCookie", "Oqtane.Interop.setCookie",
name, value, days); name, value, days, secure, sameSite);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch

View File

@ -1,7 +1,5 @@
@namespace Oqtane.UI @namespace Oqtane.UI
@using Microsoft.AspNetCore.Http
@inject IInstallationService InstallationService @inject IInstallationService InstallationService
@inject IJSRuntime JSRuntime
@inject SiteState SiteState @inject SiteState SiteState
@if (_initialized) @if (_initialized)
@ -48,9 +46,6 @@
[Parameter] [Parameter]
public string Platform { get; set; } = ""; public string Platform { get; set; } = "";
[CascadingParameter]
HttpContext HttpContext { get; set; }
private bool _initialized = false; private bool _initialized = false;
private bool _installed = false; private bool _installed = false;
private string _display = "display: none;"; private string _display = "display: none;";
@ -61,9 +56,8 @@
{ {
SiteState.AntiForgeryToken = AntiForgeryToken; SiteState.AntiForgeryToken = AntiForgeryToken;
SiteState.AuthorizationToken = AuthorizationToken; SiteState.AuthorizationToken = AuthorizationToken;
SiteState.RemoteIPAddress = (_pageState != null) ? _pageState.RemoteIPAddress : "";
SiteState.Platform = Platform; SiteState.Platform = Platform;
SiteState.IsPrerendering = (HttpContext != null) ? true : false; SiteState.IsPrerendering = !RendererInfo.IsInteractive;
if (Runtime == Runtimes.Hybrid) if (Runtime == Runtimes.Hybrid)
{ {
@ -80,6 +74,7 @@
{ {
_pageState = PageState; _pageState = PageState;
SiteState.Alias = PageState.Alias; SiteState.Alias = PageState.Alias;
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : "";
_installed = true; _installed = true;
} }
} }

View File

@ -1,6 +1,5 @@
@using System.Diagnostics.CodeAnalysis @using System.Diagnostics.CodeAnalysis
@using System.Net @using System.Net
@using Microsoft.AspNetCore.Http
@using System.Globalization @using System.Globalization
@using System.Security.Claims @using System.Security.Claims
@namespace Oqtane.UI @namespace Oqtane.UI
@ -157,7 +156,7 @@
// verify user is authenticated for current site // verify user is authenticated for current site
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync(); var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == "sitekey" && item.Value == SiteState.Alias.SiteKey)) if (authState.User.Identity.IsAuthenticated && authState.User.Claims.Any(item => item.Type == Constants.SiteKeyClaimType && item.Value == SiteState.Alias.SiteKey))
{ {
// get user // get user
var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value); var userid = int.Parse(authState.User.Claims.First(item => item.Type == ClaimTypes.NameIdentifier).Value);
@ -287,10 +286,10 @@
} }
// load additional metadata for current page // load additional metadata for current page
page = ProcessPage(page, site, user, SiteState.Alias); page = ProcessPage(page, site, user, SiteState.Alias, action);
// load additional metadata for modules // load additional metadata for modules
(page, modules) = ProcessModules(page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias); (page, modules) = ProcessModules(site, page, modules, moduleid, action, (!string.IsNullOrEmpty(page.DefaultContainerType)) ? page.DefaultContainerType : site.DefaultContainerType, SiteState.Alias);
// populate page state (which acts as a client-side cache for subsequent requests) // populate page state (which acts as a client-side cache for subsequent requests)
_pagestate = new PageState _pagestate = new PageState
@ -366,7 +365,7 @@
} }
} }
private Page ProcessPage(Page page, Site site, User user, Alias alias) private Page ProcessPage(Page page, Site site, User user, Alias alias, string action)
{ {
try try
{ {
@ -403,6 +402,16 @@
page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace); page.Resources = ManagePageResources(page.Resources, themeobject.Resources, ResourceLevel.Page, alias, "Themes", themetype.Namespace);
} }
} }
// theme settings components are dynamically loaded within the framework Page Management module
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
var settingsType = Type.GetType(theme.ThemeSettingsType);
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace);
}
}
if (!string.IsNullOrEmpty(panes)) if (!string.IsNullOrEmpty(panes))
{ {
@ -426,7 +435,7 @@
return page; return page;
} }
private (Page Page, List<Module> Modules) ProcessModules(Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias) private (Page Page, List<Module> Modules) ProcessModules(Site site, Page page, List<Module> modules, int moduleid, string action, string defaultcontainertype, Alias alias)
{ {
var paneindex = new Dictionary<string, int>(); var paneindex = new Dictionary<string, int>();
@ -494,15 +503,40 @@
module.Prerender = moduleobject.Prerender; module.Prerender = moduleobject.Prerender;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{ {
// settings components are embedded within a framework settings module // module settings component
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); var settingsType = "";
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
settingsType = module.ModuleDefinition.SettingsType;
}
else
{
// legacy support - module settings type determined by convention
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
}
moduletype = Type.GetType(settingsType, false, true);
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace); page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
} }
// container settings component
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == page.ThemeType));
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
moduletype = Type.GetType(theme.ContainerSettingsType);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace);
}
}
} }
// additional metadata needed for admin components // additional metadata needed for admin components

View File

@ -20,6 +20,13 @@
return; return;
} }
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
// set page title // set page title
if (!string.IsNullOrEmpty(PageState.Page.Title)) if (!string.IsNullOrEmpty(PageState.Page.Title))
{ {
@ -44,7 +51,6 @@
} }
// head content // head content
AddHeadContent(headcontent, PageState.Site.HeadContent);
if (!string.IsNullOrEmpty(PageState.Site.HeadContent)) if (!string.IsNullOrEmpty(PageState.Site.HeadContent))
{ {
headcontent = AddHeadContent(headcontent, PageState.Site.HeadContent); headcontent = AddHeadContent(headcontent, PageState.Site.HeadContent);
@ -66,30 +72,24 @@
{ {
if (!string.IsNullOrEmpty(content)) if (!string.IsNullOrEmpty(content))
{ {
if (PageState.RenderMode == RenderModes.Interactive) var elements = content.Split('<', StringSplitOptions.RemoveEmptyEntries);
foreach (var element in elements)
{ {
// remove scripts if (PageState.RenderMode == RenderModes.Static || (!element.ToLower().StartsWith("script") && !element.ToLower().StartsWith("/script")))
var index = content.IndexOf("<script");
while (index >= 0)
{ {
content = content.Remove(index, content.IndexOf("</script>") + 9 - index); if (!headcontent.Contains("<" + element) || element.StartsWith("/"))
index = content.IndexOf("<script"); {
headcontent += "<" + element;
}
} }
} }
headcontent += content + "\n"; headcontent += "\n";
} }
return headcontent; return headcontent;
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
// force authenticated user to provide email address (email may be missing if using external login)
if (PageState.User != null && PageState.User.IsAuthenticated && string.IsNullOrEmpty(PageState.User.Email) && PageState.Route.PagePath != "profile")
{
NavigationManager.NavigateTo(Utilities.NavigateUrl(PageState.Alias.Path, "profile", "returnurl=" + WebUtility.UrlEncode(PageState.Route.PathAndQuery)));
return;
}
if (!firstRender) if (!firstRender)
{ {
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script")) if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>5.2.2</Version> <Version>6.0.0</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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="8.0.5" /> <PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.0-preview" />
<PackageReference Include="MySql.Data" Version="9.0.0" /> <PackageReference Include="MySql.Data" Version="9.1.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -42,7 +42,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" /> <MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')"> <Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')">

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>5.2.2</Version> <Version>6.0.0</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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -25,17 +25,18 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn> <NoWarn>1701;1702;EF1001;AD0001;NU1608</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn>1701;1702;EF1001;AD0001</NoWarn> <NoWarn>1701;1702;EF1001;AD0001;NU1608</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" /> <PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0-rc.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -43,7 +44,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" /> <PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(PostgreSQLFiles)" Outputs="@(PostgreSQLFiles->'%(DestinationPath)')"> <Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(PostgreSQLFiles)" Outputs="@(PostgreSQLFiles->'%(DestinationPath)')">

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>5.2.2</Version> <Version>6.0.0</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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -41,7 +41,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" /> <SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqlServerFiles)" Outputs="@(SqlServerFiles->'%(DestinationPath)')"> <Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqlServerFiles)" Outputs="@(SqlServerFiles->'%(DestinationPath)')">

View File

@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>5.2.2</Version> <Version>6.0.0</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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -41,7 +41,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net8.0\%(Filename)%(Extension)" /> <SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqliteFiles)" Outputs="@(SqliteFiles->'%(DestinationPath)')"> <Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(SqliteFiles)" Outputs="@(SqliteFiles->'%(DestinationPath)')">

View File

@ -5,7 +5,10 @@ public partial class App : Application
public App() public App()
{ {
InitializeComponent(); InitializeComponent();
MainPage = new MainPage();
} }
protected override Window CreateWindow(IActivationState activationState)
{
return new Window(new MainPage());
}
} }

View File

@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks> <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET --> <!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> --> <!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> --> <!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>5.2.2</Version> <Version>6.0.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -14,7 +14,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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace> <RootNamespace>Oqtane.Maui</RootNamespace>
@ -28,19 +28,21 @@
<!-- App Identifier --> <!-- App Identifier -->
<ApplicationId>com.oqtane.maui</ApplicationId> <ApplicationId>com.oqtane.maui</ApplicationId>
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>5.2.2</ApplicationDisplayVersion> <ApplicationDisplayVersion>6.0.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">14.0</SupportedOSPlatformVersion> <WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
</PropertyGroup> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon --> <!-- App Icon -->
@ -65,23 +67,22 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" /> <PackageReference Include="System.Net.Http.Json" Version="9.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" /> <PackageReference Include="Microsoft.Maui.Controls" Version="9.0.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.80" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.0" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.80" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.80" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Oqtane.Client"> <Reference Include="Oqtane.Client">
<HintPath>..\Oqtane.Server\bin\Debug\net8.0\Oqtane.Client.dll</HintPath> <HintPath>..\Oqtane.Server\bin\Debug\net9.0\Oqtane.Client.dll</HintPath>
</Reference> </Reference>
<Reference Include="Oqtane.Shared"> <Reference Include="Oqtane.Shared">
<HintPath>..\Oqtane.Server\bin\Debug\net8.0\Oqtane.Shared.dll</HintPath> <HintPath>..\Oqtane.Server\bin\Debug\net9.0\Oqtane.Shared.dll</HintPath>
</Reference> </Reference>
</ItemGroup> </ItemGroup>

View File

@ -1,7 +1,7 @@
{ {
"profiles": { "profiles": {
"Windows Machine": { "Windows Machine": {
"commandName": "MsixPackage", "commandName": "Project",
"nativeDebugging": false "nativeDebugging": false
} }
} }

View File

@ -35,6 +35,9 @@ app {
} }
/* Action Dialog */ /* Action Dialog */
.app-actiondialog{
position: absolute;
}
.app-actiondialog .modal { .app-actiondialog .modal {
position: fixed; /* Stay in place */ position: fixed; /* Stay in place */
z-index: 9999; /* Sit on top */ z-index: 9999; /* Sit on top */
@ -230,5 +233,41 @@ app {
} }
.app-form-inline { .app-form-inline {
display: inline-block; display: inline;
} }
.app-search{
display: inline-block;
position: relative;
}
.app-search input + button{
background: none;
border: none;
position: absolute;
right: 0;
top: 0;
}
.app-search input + button .oi{
top: 0;
}
.app-search-noinput {
display: inline-block;
position: relative;
}
.app-search-noinput button {
background: none;
border: none;
color: var(--bs-heading-color);
}
.app-search-noinput button:hover {
color: var(--bs-heading-color);
}
/* Text Editor */
.text-area-editor > textarea {
width: 100%;
min-height: 250px;
}
.app-logo .navbar-brand {
padding: 5px 20px 5px 20px;
}

View File

@ -1,11 +1,18 @@
var Oqtane = Oqtane || {}; var Oqtane = Oqtane || {};
Oqtane.Interop = { Oqtane.Interop = {
setCookie: function (name, value, days) { setCookie: function (name, value, days, secure, sameSite) {
var d = new Date(); var d = new Date();
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toUTCString(); var expires = "expires=" + d.toUTCString();
document.cookie = name + "=" + value + ";" + expires + ";path=/"; var cookieString = name + "=" + value + ";" + expires + ";path=/";
if (secure) {
cookieString += "; secure";
}
if (sameSite === "Lax" || sameSite === "Strict" || sameSite === "None") {
cookieString += "; SameSite=" + sameSite;
}
document.cookie = cookieString;
}, },
getCookie: function (name) { getCookie: function (name) {
name = name + "="; name = name + "=";
@ -198,7 +205,9 @@ Oqtane.Interop = {
} }
promises.push(new Promise((resolve, reject) => { promises.push(new Promise((resolve, reject) => {
if (loadjs.isDefined(bundles[b])) { if (loadjs.isDefined(bundles[b])) {
resolve(true); loadjs.ready(bundles[b], () => {
resolve(true);
});
} }
else { else {
loadjs(urls, bundles[b], { loadjs(urls, bundles[b], {
@ -206,18 +215,25 @@ Oqtane.Interop = {
returnPromise: true, returnPromise: true,
before: function (path, element) { before: function (path, element) {
for (let s = 0; s < scripts.length; s++) { for (let s = 0; s < scripts.length; s++) {
if (path === scripts[s].href && scripts[s].integrity !== '') { if (path === scripts[s].href) {
element.integrity = scripts[s].integrity; if (scripts[s].integrity !== '') {
} element.integrity = scripts[s].integrity;
if (path === scripts[s].href && scripts[s].crossorigin !== '') { }
element.crossOrigin = scripts[s].crossorigin; if (scripts[s].crossorigin !== '') {
} element.crossOrigin = scripts[s].crossorigin;
if (path === scripts[s].href && scripts[s].es6module === true) { }
element.type = "module"; if (scripts[s].es6module === true) {
} element.type = "module";
if (path === scripts[s].href && scripts[s].location === 'body') { }
document.body.appendChild(element); if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) {
return false; // return false to bypass default DOM insertion mechanism for (var key in scripts[s].dataAttributes) {
element.setAttribute(key, scripts[s].dataAttributes[key]);
}
}
if (scripts[s].location === 'body') {
document.body.appendChild(element);
return false; // return false to bypass default DOM insertion mechanism
}
} }
} }
} }
@ -286,41 +302,49 @@ Oqtane.Interop = {
}, },
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) {
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var files = fileinput.files;
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id); var progressbar = document.getElementById('ProgressBar_' + id);
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;"); progressinfo.setAttribute("style", "display: inline;");
progressinfo.innerHTML = '';
progressbar.setAttribute("style", "width: 100%; display: inline;"); progressbar.setAttribute("style", "width: 100%; display: inline;");
progressbar.value = 0;
} }
var files = fileinput.files;
var totalSize = 0;
for (var i = 0; i < files.length; i++) { for (var i = 0; i < files.length; i++) {
var FileChunk = []; totalSize = totalSize + files[i].size;
var file = files[i]; }
var MaxFileSizeMB = 1;
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
var FileStreamPos = 0;
var EndPos = BufferChunkSize;
var Size = file.size;
while (FileStreamPos < Size) { var maxChunkSizeMB = 1;
FileChunk.push(file.slice(FileStreamPos, EndPos)); var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
FileStreamPos = EndPos; var uploadedSize = 0;
EndPos = FileStreamPos + BufferChunkSize;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
} }
var TotalParts = FileChunk.length; var totalParts = fileChunk.length;
var PartCount = 0; var partCount = 0;
while (Chunk = FileChunk.shift()) { while (chunk = fileChunk.shift()) {
PartCount++; partCount++;
var FileName = file.name + ".part_" + PartCount.toString().padStart(3, '0') + "_" + TotalParts.toString().padStart(3, '0'); var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData(); var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken); data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder); data.append('folder', folder);
data.append('formfile', Chunk, FileName); data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open('POST', posturl, true); request.open('POST', posturl, true);
if (jwt !== "") { if (jwt !== "") {
@ -328,28 +352,36 @@ Oqtane.Interop = {
request.withCredentials = true; request.withCredentials = true;
} }
request.upload.onloadstart = function (e) { request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
progressinfo.innerHTML = file.name + ' 0%'; if (files.length === 1) {
progressbar.value = 0; progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
} }
}; };
request.upload.onprogress = function (e) { request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil((e.loaded / e.total) * 100); var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
progressbar.value = (percent / 100); progressbar.value = (percent / 100);
} }
}; };
request.upload.onloadend = function (e) { request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' 100%'; uploadedSize = uploadedSize + e.total;
progressbar.value = 1; var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
} }
}; };
request.upload.onerror = function() { request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText; if (files.length === 1) {
progressbar.value = 0; progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
} }
}; };
request.send(data); request.send(data);
@ -385,11 +417,20 @@ Oqtane.Interop = {
} }
}, },
scrollTo: function (top, left, behavior) { scrollTo: function (top, left, behavior) {
window.scrollTo({ const modal = document.querySelector('.modal');
top: top, if (modal) {
left: left, modal.scrollTo({
behavior: behavior top: top,
}); left: left,
behavior: behavior
});
} else {
window.scrollTo({
top: top,
left: left,
behavior: behavior
});
}
}, },
scrollToId: function (id) { scrollToId: function (id) {
var element = document.getElementById(id); var element = document.getElementById(id);

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package>
<metadata> <metadata>
<id>Oqtane.Client</id> <id>Oqtane.Client</id>
<version>5.2.2</version> <version>6.0.0</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,14 +12,15 @@
<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/v5.2.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>
<files> <files>
<file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.dll" target="lib\net8.0" /> <file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.dll" target="lib\net9.0" />
<file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.pdb" target="lib\net8.0" /> <file src="..\Oqtane.Client\bin\Release\net8.0\Oqtane.Client.pdb" target="lib\net9.0" />
<file src="icon.png" target="" /> <file src="icon.png" target="" />
<file src="readme.md" target="" />
</files> </files>
</package> </package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package>
<metadata> <metadata>
<id>Oqtane.Framework</id> <id>Oqtane.Framework</id>
<version>5.2.2</version> <version>6.0.0</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,13 +11,14 @@
<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/v5.2.2/Oqtane.Framework.5.2.2.Upgrade.zip</projectUrl> <projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v6.0.0/Oqtane.Framework.6.0.0.Upgrade.zip</projectUrl>
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</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>
</metadata> </metadata>
<files> <files>
<file src="icon.png" target="" /> <file src="icon.png" target="" />
<file src="readme.md" target="" />
</files> </files>
</package> </package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package>
<metadata> <metadata>
<id>Oqtane.Server</id> <id>Oqtane.Server</id>
<version>5.2.2</version> <version>6.0.0</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,14 +12,15 @@
<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/v5.2.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>
<files> <files>
<file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.dll" target="lib\net8.0" /> <file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.dll" target="lib\net9.0" />
<file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.pdb" target="lib\net8.0" /> <file src="..\Oqtane.Server\bin\Release\net8.0\Oqtane.Server.pdb" target="lib\net9.0" />
<file src="icon.png" target="" /> <file src="icon.png" target="" />
<file src="readme.md" target="" />
</files> </files>
</package> </package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package>
<metadata> <metadata>
<id>Oqtane.Shared</id> <id>Oqtane.Shared</id>
<version>5.2.2</version> <version>6.0.0</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,14 +12,15 @@
<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/v5.2.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>
<files> <files>
<file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.dll" target="lib\net8.0" /> <file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.dll" target="lib\net9.0" />
<file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.pdb" target="lib\net8.0" /> <file src="..\Oqtane.Shared\bin\Release\net8.0\Oqtane.Shared.pdb" target="lib\net9.0" />
<file src="icon.png" target="" /> <file src="icon.png" target="" />
<file src="readme.md" target="" />
</files> </files>
</package> </package>

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd"> <package>
<metadata> <metadata>
<id>Oqtane.Updater</id> <id>Oqtane.Updater</id>
<version>5.2.2</version> <version>6.0.0</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,13 +12,14 @@
<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/v5.2.2</releaseNotes> <releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</releaseNotes>
<readme>readme.md</readme> <readme>readme.md</readme>
<icon>icon.png</icon> <icon>icon.png</icon>
<tags>oqtane</tags> <tags>oqtane</tags>
</metadata> </metadata>
<files> <files>
<file src="..\Oqtane.Updater\bin\Release\net8.0\publish\*.*" target="lib\net8.0" /> <file src="..\Oqtane.Updater\bin\Release\net8.0\publish\*.*" target="lib\net9.0" />
<file src="icon.png" target="" /> <file src="icon.png" target="" />
<file src="readme.md" target="" />
</files> </files>
</package> </package>

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.2.Install.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.0.Install.zip" -Force

Binary file not shown.

View File

@ -6,14 +6,22 @@ nuget.exe pack Oqtane.Client.nuspec
nuget.exe pack Oqtane.Server.nuspec nuget.exe pack Oqtane.Server.nuspec
nuget.exe pack Oqtane.Shared.nuspec nuget.exe pack Oqtane.Shared.nuspec
nuget.exe pack Oqtane.Framework.nuspec nuget.exe pack Oqtane.Framework.nuspec
del /F/Q/S "..\Oqtane.Server\bin\Release\net8.0\publish" > NUL del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net8.0\publish" rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish"
dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release
del /F/Q/S "..\Oqtane.Server\bin\Release\net8.0\publish\wwwroot\Content" > NUL del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" > NUL
rmdir /Q/S "..\Oqtane.Server\bin\Release\net8.0\publish\wwwroot\Content" rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content"
setlocal ENABLEDELAYEDEXPANSION setlocal ENABLEDELAYEDEXPANSION
set retain=Placeholder.txt
for /D %%i in ("..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\_content\*") do (
set /A found=0
for %%j in (%retain%) do (
if "%%~nxi" == "%%j" set /A found=1
)
if not !found! == 1 rmdir /Q/S "%%i"
)
set retain=Oqtane.Modules.Admin.Login,Oqtane.Modules.HtmlText set retain=Oqtane.Modules.Admin.Login,Oqtane.Modules.HtmlText
for /D %%i in ("..\Oqtane.Server\bin\Release\net8.0\publish\wwwroot\Modules\*") do ( for /D %%i in ("..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Modules\*") do (
set /A found=0 set /A found=0
for %%j in (%retain%) do ( for %%j in (%retain%) do (
if "%%~nxi" == "%%j" set /A found=1 if "%%~nxi" == "%%j" set /A found=1
@ -21,18 +29,18 @@ if "%%~nxi" == "%%j" set /A found=1
if not !found! == 1 rmdir /Q/S "%%i" if not !found! == 1 rmdir /Q/S "%%i"
) )
set retain=Oqtane.Themes.BlazorTheme,Oqtane.Themes.OqtaneTheme set retain=Oqtane.Themes.BlazorTheme,Oqtane.Themes.OqtaneTheme
for /D %%i in ("..\Oqtane.Server\bin\Release\net8.0\publish\wwwroot\Themes\*") do ( for /D %%i in ("..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Themes\*") do (
set /A found=0 set /A found=0
for %%j in (%retain%) do ( for %%j in (%retain%) do (
if "%%~nxi" == "%%j" set /A found=1 if "%%~nxi" == "%%j" set /A found=1
) )
if not !found! == 1 rmdir /Q/S "%%i" if not !found! == 1 rmdir /Q/S "%%i"
) )
del "..\Oqtane.Server\bin\Release\net8.0\publish\appsettings.json" del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
ren "..\Oqtane.Server\bin\Release\net8.0\publish\appsettings.release.json" "appsettings.json" ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1" C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1"
del "..\Oqtane.Server\bin\Release\net8.0\publish\appsettings.json" del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
del "..\Oqtane.Server\bin\Release\net8.0\publish\web.config" del "..\Oqtane.Server\bin\Release\net9.0\publish\web.config"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\upgrade.ps1" C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\upgrade.ps1"
dotnet clean -c Release ..\Oqtane.Updater.sln dotnet clean -c Release ..\Oqtane.Updater.sln
dotnet build -c Release ..\Oqtane.Updater.sln dotnet build -c Release ..\Oqtane.Updater.sln

View File

@ -1 +1 @@
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.2.Upgrade.zip" -Force Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.0.0.Upgrade.zip" -Force

View File

@ -179,10 +179,6 @@
ManageScripts(resources, alias); ManageScripts(resources, alias);
// generate scripts // generate scripts
if (_renderMode == RenderModes.Interactive && _runtime == Runtimes.Server)
{
_scripts += CreateReconnectScript();
}
if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null) if (site.PwaIsEnabled && site.PwaAppIconFileId != null && site.PwaSplashIconFileId != null)
{ {
_scripts += CreatePWAScript(alias, site, route); _scripts += CreatePWAScript(alias, site, route);
@ -196,28 +192,29 @@
_bodyResources += ParseScripts(site.BodyContent); _bodyResources += ParseScripts(site.BodyContent);
// set culture if not specified // set culture if not specified
string culture = Context.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName]; string cultureCookie = Context.Request.Cookies[Shared.CookieRequestCultureProvider.DefaultCookieName];
if (culture == null) if (cultureCookie == null)
{ {
// get default language for site // get default language for site
if (site.Languages.Any()) if (site.Languages.Any())
{ {
// use default language if specified otherwise use first language in collection // use default language if specified otherwise use first language in collection
culture = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code; cultureCookie = (site.Languages.Where(l => l.IsDefault).SingleOrDefault() ?? site.Languages.First()).Code;
} }
else else
{ {
culture = LocalizationManager.GetDefaultCulture(); // fallback language
cultureCookie = LocalizationManager.GetDefaultCulture();
} }
SetLocalizationCookie(culture); // convert language code to culture cookie format (ie. "c=en|uic=en")
cultureCookie = Shared.CookieRequestCultureProvider.MakeCookieValue(new Models.RequestCulture(cultureCookie));
SetLocalizationCookie(cultureCookie);
} }
// set language for page // set language for page
if (!string.IsNullOrEmpty(culture)) if (!string.IsNullOrEmpty(cultureCookie))
{ {
// localization cookie value in form of c=en|uic=en _language = Shared.CookieRequestCultureProvider.ParseCookieValue(cultureCookie).Culture.Name;
_language = culture.Split('|')[0];
_language = _language.Replace("c=", "");
} }
// create initial PageState // create initial PageState
@ -494,25 +491,6 @@
"</script>" + Environment.NewLine; "</script>" + Environment.NewLine;
} }
private string CreateReconnectScript()
{
return Environment.NewLine +
"<script>" + Environment.NewLine +
" // Interactive Blazor Server Reconnect" + Environment.NewLine +
" new MutationObserver((mutations, observer) => {" + Environment.NewLine +
" if (document.querySelector('#components-reconnect-modal h5 a')) {" + Environment.NewLine +
" async function attemptReload() {" + Environment.NewLine +
" await fetch('');" + Environment.NewLine +
" location.reload();" + Environment.NewLine +
" }" + Environment.NewLine +
" observer.disconnect();" + Environment.NewLine +
" attemptReload();" + Environment.NewLine +
" setInterval(attemptReload, 5000);" + Environment.NewLine +
" }" + Environment.NewLine +
" }).observe(document.body, { childList: true, subtree: true });" + Environment.NewLine +
"</script>" + Environment.NewLine;
}
private string CreateScrollPositionScript() private string CreateScrollPositionScript()
{ {
return Environment.NewLine + return Environment.NewLine +
@ -522,7 +500,7 @@
" let currentUrl = window.location.pathname;" + Environment.NewLine + " let currentUrl = window.location.pathname;" + Environment.NewLine +
" Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine + " Blazor.addEventListener('enhancedload', () => {" + Environment.NewLine +
" let newUrl = window.location.pathname;" + Environment.NewLine + " let newUrl = window.location.pathname;" + Environment.NewLine +
" if (currentUrl != newUrl) {" + Environment.NewLine + " if (currentUrl !== newUrl || window.location.hash === '#top') {" + Environment.NewLine +
" window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine + " window.scrollTo({ top: 0, left: 0, behavior: 'instant' });" + Environment.NewLine +
" }" + Environment.NewLine + " }" + Environment.NewLine +
" currentUrl = newUrl;" + Environment.NewLine + " currentUrl = newUrl;" + Environment.NewLine +
@ -534,9 +512,9 @@
private string ParseScripts(string content) private string ParseScripts(string content)
{ {
// iterate scripts
var scripts = ""; var scripts = "";
if (!string.IsNullOrEmpty(content)) // in interactive render mode, parse scripts from content and inject into page
if (_renderMode == RenderModes.Interactive && !string.IsNullOrEmpty(content))
{ {
var index = content.IndexOf("<script"); var index = content.IndexOf("<script");
while (index >= 0) while (index >= 0)
@ -602,19 +580,19 @@
} }
} }
private void SetLocalizationCookie(string culture) private void SetLocalizationCookie(string cookieValue)
{ {
var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions var cookieOptions = new Microsoft.AspNetCore.Http.CookieOptions
{ {
Expires = DateTimeOffset.UtcNow.AddYears(1), Expires = DateTimeOffset.UtcNow.AddYears(1),
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, // Set SameSite attribute
Secure = true, // Ensure the cookie is only sent over HTTPS Secure = true, // Ensure the cookie is only sent over HTTPS
HttpOnly = true // Optional: Helps mitigate XSS attacks HttpOnly = false // cookie is updated using JS Interop in Interactive render mode
}; };
Context.Response.Cookies.Append( Context.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName, Shared.CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)), cookieValue,
cookieOptions cookieOptions
); );
} }
@ -644,6 +622,16 @@
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode); resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode);
} }
} }
// theme settings components are dynamically loaded within the framework Page Management module
if (page.Path == "admin/pages" && action.ToLower() == "edit" && theme != null && !string.IsNullOrEmpty(theme.ThemeSettingsType))
{
var settingsType = Type.GetType(theme.ThemeSettingsType);
if (settingsType != null)
{
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode);
}
}
foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid)) foreach (Module module in modules.Where(item => item.PageId == page.PageId || item.ModuleId == moduleid))
{ {
@ -686,25 +674,49 @@
// ensure component exists and implements IModuleControl // ensure component exists and implements IModuleControl
module.ModuleType = ""; module.ModuleType = "";
Type moduletype = Type.GetType(typename, false, true); // case insensitive var moduletype = Type.GetType(typename, false, true); // case insensitive
if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl))) if (moduletype != null && moduletype.GetInterfaces().Contains(typeof(IModuleControl)))
{ {
module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name module.ModuleType = Utilities.GetFullTypeName(moduletype.AssemblyQualifiedName); // get actual type name
} }
if (moduletype != null && module.ModuleType != "") if (moduletype != null && module.ModuleType != "")
{ {
var obj = Activator.CreateInstance(moduletype) as IModuleControl; var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (obj != null) if (moduleobject != null)
{ {
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
{ {
// settings components are embedded within a framework settings module // module settings component
moduletype = Type.GetType(module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action), false, true); var settingsType = "";
if (!string.IsNullOrEmpty(module.ModuleDefinition.SettingsType))
{
// module settings type explicitly declared in IModule interface
settingsType = module.ModuleDefinition.SettingsType;
}
else
{
// legacy support - module settings type determined by convention
settingsType = module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, action);
}
moduletype = Type.GetType(settingsType, false, true);
if (moduletype != null) if (moduletype != null)
{ {
obj = Activator.CreateInstance(moduletype) as IModuleControl; moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, obj.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
}
// container settings component
if (theme != null && !string.IsNullOrEmpty(theme.ContainerSettingsType))
{
moduletype = Type.GetType(theme.ContainerSettingsType);
if (moduletype != null)
{
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode);
}
} }
} }
} }

View File

@ -17,11 +17,10 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Extensions; using Oqtane.Extensions;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Formats.Png;
using System.Net.Http; using System.Net.Http;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using System.IO.Compression; using System.IO.Compression;
using Oqtane.Services;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@ -38,7 +37,9 @@ namespace Oqtane.Controllers
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly ISettingRepository _settingRepository; private readonly ISettingRepository _settingRepository;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) private readonly IImageService _imageService;
public FileController(IWebHostEnvironment environment, IFileRepository files, IFolderRepository folders, IUserPermissions userPermissions, ISettingRepository settingRepository, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService)
{ {
_environment = environment; _environment = environment;
_files = files; _files = files;
@ -48,6 +49,7 @@ namespace Oqtane.Controllers
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_settingRepository = settingRepository; _settingRepository = settingRepository;
_imageService = imageService;
} }
// GET: api/<controller>?folder=x // GET: api/<controller>?folder=x
@ -425,11 +427,11 @@ namespace Oqtane.Controllers
// POST api/<controller>/upload // POST api/<controller>/upload
[EnableCors(Constants.MauiCorsPolicy)] [EnableCors(Constants.MauiCorsPolicy)]
[HttpPost("upload")] [HttpPost("upload")]
public async Task UploadFile(string folder, IFormFile formfile) public async Task<IActionResult> UploadFile(string folder, IFormFile formfile)
{ {
if (formfile == null || formfile.Length <= 0) if (formfile == null || formfile.Length <= 0)
{ {
return; return NoContent();
} }
// ensure filename is valid // ensure filename is valid
@ -437,7 +439,7 @@ namespace Oqtane.Controllers
if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token)))) if (!formfile.FileName.IsPathOrFileValid() || !formfile.FileName.Contains(token) || !HasValidFileExtension(formfile.FileName.Substring(0, formfile.FileName.IndexOf(token))))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return; return NoContent();
} }
string folderPath = ""; string folderPath = "";
@ -492,6 +494,8 @@ namespace Oqtane.Controllers
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
return NoContent();
} }
private async Task<string> MergeFile(string folder, string filename) private async Task<string> MergeFile(string folder, string filename)
@ -513,7 +517,7 @@ namespace Oqtane.Controllers
bool success = true; bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{ {
foreach (string filepart in fileparts) foreach (string filepart in fileparts.Order())
{ {
try try
{ {
@ -679,22 +683,18 @@ namespace Oqtane.Controllers
var filepath = _files.GetFilePath(file); var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath)) if (System.IO.File.Exists(filepath))
{ {
// validation
if (!Enum.TryParse(mode, true, out ResizeMode _)) mode = "crop";
if (!Enum.TryParse(position, true, out AnchorPositionMode _)) position = "center";
if (!Color.TryParseHex("#" + background, out _)) background = "transparent";
if (!int.TryParse(rotate, out _)) rotate = "0";
rotate = (int.Parse(rotate) < 0 || int.Parse(rotate) > 360) ? "0" : rotate;
if (!bool.TryParse(recreate, out _)) recreate = "false"; if (!bool.TryParse(recreate, out _)) recreate = "false";
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + ".png"); string format = "png";
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate)) if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate))
{ {
// user has edit access to folder or folder supports the image size being created // user has edit access to folder or folder supports the image size being created
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) || if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) ||
(!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString())))) (!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString()))))
{ {
imagepath = CreateImage(filepath, width, height, mode, position, background, rotate, imagepath); imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotate, format, imagepath);
} }
else else
{ {
@ -741,70 +741,6 @@ namespace Oqtane.Controllers
return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null; return System.IO.File.Exists(errorPath) ? PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)) : null;
} }
private string CreateImage(string filepath, int width, int height, string mode, string position, string background, string rotate, string imagepath)
{
try
{
using (var stream = new FileStream(filepath, FileMode.Open, FileAccess.Read))
{
stream.Position = 0;
using (var image = Image.Load(stream))
{
int.TryParse(rotate, out int angle);
Enum.TryParse(mode, true, out ResizeMode resizemode);
Enum.TryParse(position, true, out AnchorPositionMode anchorpositionmode);
PngEncoder encoder;
if (background != "transparent")
{
image.Mutate(x => x
.AutoOrient() // auto orient the image
.Rotate(angle)
.Resize(new ResizeOptions
{
Mode = resizemode,
Position = anchorpositionmode,
Size = new Size(width, height),
PadColor = Color.ParseHex("#" + background)
}));
encoder = new PngEncoder();
}
else
{
image.Mutate(x => x
.AutoOrient() // auto orient the image
.Rotate(angle)
.Resize(new ResizeOptions
{
Mode = resizemode,
Position = anchorpositionmode,
Size = new Size(width, height)
}));
encoder = new PngEncoder
{
ColorType = PngColorType.RgbWithAlpha,
TransparentColorMode = PngTransparentColorMode.Preserve,
BitDepth = PngBitDepth.Bit8,
CompressionLevel = PngCompressionLevel.BestSpeed
};
}
image.Save(imagepath, encoder);
}
}
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, ex, "Error Creating Image For File {FilePath} {Width} {Height} {Mode} {Rotate} {Error}", filepath, width, height, mode, rotate, ex.Message);
imagepath = "";
}
return imagepath;
}
private string GetFolderPath(string folder) private string GetFolderPath(string folder)
{ {
return Utilities.PathCombine(_environment.ContentRootPath, folder); return Utilities.PathCombine(_environment.ContentRootPath, folder);

View File

@ -55,6 +55,10 @@ namespace Oqtane.Controllers
else else
{ {
languages = _languages.GetLanguages(SiteId).ToList(); languages = _languages.GetLanguages(SiteId).ToList();
foreach (Language language in languages)
{
language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName;
}
if (!string.IsNullOrEmpty(packagename)) if (!string.IsNullOrEmpty(packagename))
{ {
foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), $"{packagename}*{Constants.SatelliteAssemblyExtension}", SearchOption.AllDirectories))
@ -85,6 +89,7 @@ namespace Oqtane.Controllers
var language = _languages.GetLanguage(id); var language = _languages.GetLanguage(id);
if (language != null && language.SiteId == _alias.SiteId) if (language != null && language.SiteId == _alias.SiteId)
{ {
language.Name = CultureInfo.GetCultureInfo(language.Code).DisplayName;
return language; return language;
} }
else else

View File

@ -351,9 +351,9 @@ namespace Oqtane.Controllers
return new Dictionary<string, object>() return new Dictionary<string, object>()
{ {
{ "FrameworkVersion", Constants.Version }, { "FrameworkVersion", Constants.Version },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Client.dll</HintPath></Reference>" }, { "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Server.dll</HintPath></Reference>" }, { "ServerReference", $"<Reference Include=\"Oqtane.Server\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Server.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Shared.dll</HintPath></Reference>" }, { "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
}; };
}); });
} }

View File

@ -8,10 +8,6 @@ using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using System.Net; using System.Net;
using System.Reflection.Metadata;
using Microsoft.Extensions.Localization;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Linq;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {

View File

@ -189,7 +189,7 @@ namespace Oqtane.Controllers
public void Delete(string entityName, int entityId, string settingName) public void Delete(string entityName, int entityId, string settingName)
{ {
Setting setting = _settings.GetSetting(entityName, entityId, settingName); Setting setting = _settings.GetSetting(entityName, entityId, settingName);
if (IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit)) if (setting != null && IsAuthorized(setting.EntityName, setting.EntityId, PermissionNames.Edit))
{ {
_settings.DeleteSetting(setting.EntityName, setting.SettingId); _settings.DeleteSetting(setting.EntityName, setting.SettingId);
AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Delete); AddSyncEvent(setting.EntityName, setting.EntityId, setting.SettingId, SyncEventActions.Delete);
@ -199,7 +199,7 @@ namespace Oqtane.Controllers
{ {
if (entityName != EntityNames.Visitor) if (entityName != EntityNames.Visitor)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Delete, "User Not Authorized To Delete Setting {Setting}", setting); _logger.Log(LogLevel.Error, this, LogFunction.Delete, "Setting Does Not Exist Or User Not Authorized To Delete Setting For Entity {EntityName} Id {EntityId} Name {SettingName}", entityName, entityId, settingName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

@ -267,8 +267,8 @@ namespace Oqtane.Controllers
return new Dictionary<string, object>() return new Dictionary<string, object>()
{ {
{ "FrameworkVersion", Constants.Version }, { "FrameworkVersion", Constants.Version },
{ "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Client.dll</HintPath></Reference>" }, { "ClientReference", $"<Reference Include=\"Oqtane.Client\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Client.dll</HintPath></Reference>" },
{ "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net8.0\\Oqtane.Shared.dll</HintPath></Reference>" }, { "SharedReference", $"<Reference Include=\"Oqtane.Shared\"><HintPath>..\\..\\{rootFolder}\\Oqtane.Server\\bin\\Debug\\net9.0\\Oqtane.Shared.dll</HintPath></Reference>" },
}; };
}); });
} }

View File

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Oqtane.Shared; using Oqtane.Shared;
using System;
using System.Net; using System.Net;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
@ -120,11 +119,15 @@ namespace Oqtane.Controllers
filtered = new User(); filtered = new User();
// public properties // public properties
filtered.SiteId = user.SiteId;
filtered.UserId = user.UserId; filtered.UserId = user.UserId;
filtered.Username = user.Username; filtered.Username = user.Username;
filtered.DisplayName = user.DisplayName; filtered.DisplayName = user.DisplayName;
// restricted properties
filtered.Password = ""; filtered.Password = "";
filtered.TwoFactorCode = ""; filtered.TwoFactorCode = "";
filtered.SecurityStamp = "";
// include private properties if authenticated user is accessing their own user account os is an administrator // include private properties if authenticated user is accessing their own user account os is an administrator
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId)
@ -260,8 +263,24 @@ namespace Oqtane.Controllers
[Authorize] [Authorize]
public async Task Logout([FromBody] User user) public async Task Logout([FromBody] User user)
{ {
await HttpContext.SignOutAsync(Constants.AuthenticationScheme); if (_userPermissions.GetUser(User).UserId == user.UserId)
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : ""); {
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
}
}
// POST api/<controller>/logout
[HttpPost("logouteverywhere")]
[Authorize]
public async Task LogoutEverywhere([FromBody] User user)
{
if (_userPermissions.GetUser(User).UserId == user.UserId)
{
await _userManager.LogoutUserEverywhere(user);
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout Everywhere {Username}", (user != null) ? user.Username : "");
}
} }
// POST api/<controller>/verify // POST api/<controller>/verify
@ -328,6 +347,13 @@ namespace Oqtane.Controllers
return user; return user;
} }
// GET api/<controller>/validate/x
[HttpGet("validateuser")]
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
return await _userManager.ValidateUser(username, email, password);
}
// GET api/<controller>/validate/x // GET api/<controller>/validate/x
[HttpGet("validate/{password}")] [HttpGet("validate/{password}")]
public async Task<bool> Validate(string password) public async Task<bool> Validate(string password)
@ -382,6 +408,7 @@ namespace Oqtane.Controllers
} }
if (roles != "") roles = ";" + roles; if (roles != "") roles = ";" + roles;
user.Roles = roles; user.Roles = roles;
user.SecurityStamp = User.SecurityStamp();
} }
return user; return user;
} }

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
using Oqtane.Databases.Interfaces; using Oqtane.Databases.Interfaces;
using Oqtane.Interfaces;
namespace Oqtane.Repository.Databases.Interfaces namespace Oqtane.Repository.Databases.Interfaces
{ {

View File

@ -1,6 +1,5 @@
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using Oqtane.Models;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Extensions namespace Oqtane.Extensions
@ -41,9 +40,9 @@ namespace Oqtane.Extensions
public static string SiteKey(this ClaimsPrincipal claimsPrincipal) public static string SiteKey(this ClaimsPrincipal claimsPrincipal)
{ {
if (claimsPrincipal.HasClaim(item => item.Type == "sitekey")) if (claimsPrincipal.HasClaim(item => item.Type == Constants.SiteKeyClaimType))
{ {
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == "sitekey").Value; return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SiteKeyClaimType).Value;
} }
else else
{ {
@ -71,6 +70,18 @@ namespace Oqtane.Extensions
return -1; return -1;
} }
public static string SecurityStamp(this ClaimsPrincipal claimsPrincipal)
{
if (claimsPrincipal.HasClaim(item => item.Type == Constants.SecurityStampClaimType))
{
return claimsPrincipal.Claims.FirstOrDefault(item => item.Type == Constants.SecurityStampClaimType).Value;
}
else
{
return "";
}
}
public static bool IsOnlyInRole(this ClaimsPrincipal claimsPrincipal, string role) public static bool IsOnlyInRole(this ClaimsPrincipal claimsPrincipal, string role)
{ {
var identity = claimsPrincipal.Identities.FirstOrDefault(item => item.AuthenticationType == Constants.AuthenticationScheme); var identity = claimsPrincipal.Identities.FirstOrDefault(item => item.AuthenticationType == Constants.AuthenticationScheme);

View File

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Oqtane.Databases.Interfaces; using Oqtane.Databases.Interfaces;
// ReSharper disable ConvertToUsingDeclaration // ReSharper disable ConvertToUsingDeclaration
@ -9,7 +10,8 @@ namespace Oqtane.Extensions
{ {
public static DbContextOptionsBuilder UseOqtaneDatabase([NotNull] this DbContextOptionsBuilder optionsBuilder, IDatabase database, string connectionString) public static DbContextOptionsBuilder UseOqtaneDatabase([NotNull] this DbContextOptionsBuilder optionsBuilder, IDatabase database, string connectionString)
{ {
database.UseDatabase(optionsBuilder, connectionString); database.UseDatabase(optionsBuilder, connectionString)
.ConfigureWarnings(warnings => warnings.Log(RelationalEventId.PendingModelChangesWarning));
return optionsBuilder; return optionsBuilder;
} }

View File

@ -102,6 +102,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<ISearchResultsService, SearchResultsService>(); services.AddScoped<ISearchResultsService, SearchResultsService>();
services.AddScoped<ISearchService, SearchService>(); services.AddScoped<ISearchService, SearchService>();
services.AddScoped<ISearchProvider, DatabaseSearchProvider>(); services.AddScoped<ISearchProvider, DatabaseSearchProvider>();
services.AddScoped<IImageService, ImageService>();
// providers // providers
services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>(); services.AddScoped<ITextEditor, Oqtane.Modules.Controls.QuillJSTextEditor>();
@ -112,8 +113,11 @@ namespace Microsoft.Extensions.DependencyInjection
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services) internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{ {
// repositories // services
services.AddTransient<ISiteService, ServerSiteService>(); services.AddTransient<ISiteService, ServerSiteService>();
services.AddTransient<ILocalizationCookieService, ServerLocalizationCookieService>();
// repositories
services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>(); services.AddTransient<IModuleDefinitionRepository, ModuleDefinitionRepository>();
services.AddTransient<IThemeRepository, ThemeRepository>(); services.AddTransient<IThemeRepository, ThemeRepository>();
services.AddTransient<IAliasRepository, AliasRepository>(); services.AddTransient<IAliasRepository, AliasRepository>();
@ -129,7 +133,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<IPermissionRepository, PermissionRepository>(); services.AddTransient<IPermissionRepository, PermissionRepository>();
services.AddTransient<ISettingRepository, SettingRepository>(); services.AddTransient<ISettingRepository, SettingRepository>();
services.AddTransient<ILogRepository, LogRepository>(); services.AddTransient<ILogRepository, LogRepository>();
services.AddTransient<ILocalizationManager, LocalizationManager>();
services.AddTransient<IJobRepository, JobRepository>(); services.AddTransient<IJobRepository, JobRepository>();
services.AddTransient<IJobLogRepository, JobLogRepository>(); services.AddTransient<IJobLogRepository, JobLogRepository>();
services.AddTransient<INotificationRepository, NotificationRepository>(); services.AddTransient<INotificationRepository, NotificationRepository>();
@ -152,12 +155,12 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient<ILogManager, LogManager>(); services.AddTransient<ILogManager, LogManager>();
services.AddTransient<IUpgradeManager, UpgradeManager>(); services.AddTransient<IUpgradeManager, UpgradeManager>();
services.AddTransient<IUserManager, UserManager>(); services.AddTransient<IUserManager, UserManager>();
services.AddTransient<ILocalizationManager, LocalizationManager>();
// obsolete - replaced by ITenantManager
services.AddTransient<ITenantResolver, TenantResolver>();
services.AddTransient<ITokenReplace, TokenReplace>(); services.AddTransient<ITokenReplace, TokenReplace>();
// obsolete
services.AddTransient<ITenantResolver, TenantResolver>(); // replaced by ITenantManager
return services; return services;
} }
@ -169,6 +172,7 @@ namespace Microsoft.Extensions.DependencyInjection
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.LoginPath = "/login"; // overrides .NET Identity default of /Account/Login
options.Events.OnRedirectToLogin = context => options.Events.OnRedirectToLogin = context =>
{ {
context.Response.StatusCode = (int)HttpStatusCode.Forbidden; context.Response.StatusCode = (int)HttpStatusCode.Forbidden;

View File

@ -527,28 +527,63 @@ namespace Oqtane.Extensions
// manage user // manage user
if (user != null) if (user != null)
{ {
// create claims identity
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
identity.Label = ExternalLoginStatus.Success;
// update user // update user
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user); _users.UpdateUser(user);
// external roles // manage roles
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{ {
if (claimsPrincipal.Claims.Any(item => item.Type == ClaimTypes.Role)) // external roles
if (claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{ {
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == ClaimTypes.Role)) var _roles = httpContext.RequestServices.GetRequiredService<IRoleRepository>();
var roles = _roles.GetRoles(user.SiteId).ToList(); // global roles excluded ie. host users cannot be added/deleted
var mappings = httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimMappings", "").Split(',');
foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "")))
{ {
if (!identity.Claims.Any(item => item.Type == ClaimTypes.Role && item.Value == claim.Value)) var rolename = claim.Value;
if (mappings.Any(item => item.StartsWith(rolename + ":")))
{ {
identity.AddClaim(new Claim(ClaimTypes.Role, claim.Value)); rolename = mappings.First(item => item.StartsWith(rolename + ":")).Split(':')[1];
}
var role = roles.FirstOrDefault(item => item.Name == rolename);
if (role != null)
{
if (!userRoles.Any(item => item.RoleId == role.RoleId && item.UserId == user.UserId))
{
var userRole = new UserRole();
userRole.RoleId = role.RoleId;
userRole.UserId = user.UserId;
_userRoles.AddUserRole(userRole);
}
} }
} }
if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:SynchronizeRoles", "false")))
{
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
foreach (var userRole in userRoles)
{
var role = roles.FirstOrDefault(item => item.RoleId == userRole.RoleId);
if (role != null)
{
var rolename = role.Name;
if (mappings.Any(item => item.EndsWith(":" + rolename)))
{
rolename = mappings.First(item => item.EndsWith(":" + rolename)).Split(':')[0];
}
if (!claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", "") && item.Value == rolename))
{
_userRoles.DeleteUserRole(userRole.UserRoleId);
}
}
}
}
userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
} }
else else
{ {
@ -556,6 +591,12 @@ namespace Oqtane.Extensions
} }
} }
// create claims identity
identityuser = await _identityUserManager.FindByNameAsync(user.Username);
user.SecurityStamp = identityuser.SecurityStamp;
identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles);
identity.Label = ExternalLoginStatus.Success;
// user profile claims // user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{ {
@ -604,13 +645,13 @@ namespace Oqtane.Extensions
} }
} }
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName); _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
} }
} }
else // claims invalid else // claims invalid
{ {
identity.Label = ExternalLoginStatus.MissingClaims; identity.Label = ExternalLoginStatus.MissingClaims;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Saitisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return All Of The Claims Types Specified Or Email Address Does Not Satisfy Domain Filter. The Actual Claims Returned Were {Claims}. Login Was Denied.", claims);
} }
return identity; return identity;

View File

@ -155,7 +155,7 @@ namespace Oqtane.Infrastructure
// add new site // add new site
if (install.TenantName != TenantNames.Master && install.ConnectionString.Contains("=")) if (install.TenantName != TenantNames.Master && install.ConnectionString.Contains("="))
{ {
_configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, false); _configManager.AddOrUpdateSetting($"{SettingKeys.ConnectionStringsSection}:{install.TenantName}", install.ConnectionString, true);
} }
if (install.TenantName == TenantNames.Master && !install.ConnectionString.Contains("=")) if (install.TenantName == TenantNames.Master && !install.ConnectionString.Contains("="))
{ {
@ -375,7 +375,6 @@ namespace Oqtane.Infrastructure
AddEFMigrationsHistory(sql, _configManager.GetSetting($"{SettingKeys.ConnectionStringsSection}:{tenant.DBConnectionString}", ""), tenant.DBType, tenant.Version, false); AddEFMigrationsHistory(sql, _configManager.GetSetting($"{SettingKeys.ConnectionStringsSection}:{tenant.DBConnectionString}", ""), tenant.DBType, tenant.Version, false);
// push latest model into database // push latest model into database
tenantDbContext.Database.Migrate(); tenantDbContext.Database.Migrate();
result.Success = true;
} }
} }
catch (Exception ex) catch (Exception ex)
@ -384,35 +383,35 @@ namespace Oqtane.Infrastructure
_filelogger.LogError(Utilities.LogMessage(this, result.Message)); _filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
// execute any version specific upgrade logic if (string.IsNullOrEmpty(result.Message))
var version = tenant.Version;
var index = Array.FindIndex(versions, item => item == version);
if (index != (versions.Length - 1))
{ {
try // execute any version specific upgrade logic
var version = tenant.Version;
var index = Array.FindIndex(versions, item => item == version);
if (index != (versions.Length - 1))
{ {
for (var i = (index + 1); i < versions.Length; i++) try
{ {
upgrades.Upgrade(tenant, versions[i]); for (var i = (index + 1); i < versions.Length; i++)
{
upgrades.Upgrade(tenant, versions[i]);
}
tenant.Version = versions[versions.Length - 1];
db.Entry(tenant).State = EntityState.Modified;
db.SaveChanges();
}
catch (Exception ex)
{
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
tenant.Version = versions[versions.Length - 1];
db.Entry(tenant).State = EntityState.Modified;
db.SaveChanges();
}
catch (Exception ex)
{
result.Message = "An Error Occurred Executing Upgrade Logic On Tenant " + tenant.Name + ". " + ex.ToString();
_filelogger.LogError(Utilities.LogMessage(this, result.Message));
} }
} }
} }
} }
} }
if (string.IsNullOrEmpty(result.Message)) result.Success = string.IsNullOrEmpty(result.Message);
{
result.Success = true;
}
return result; return result;
} }
@ -588,7 +587,7 @@ namespace Oqtane.Infrastructure
// add host role // add host role
var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0; var hostRoleId = roles.GetRoles(user.SiteId, true).FirstOrDefault(item => item.Name == RoleNames.Host)?.RoleId ?? 0;
var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null }; var userRole = new UserRole { UserId = user.UserId, RoleId = hostRoleId, EffectiveDate = null, ExpiryDate = null, IgnoreSecurityStamp = true };
userRoles.AddUserRole(userRole); userRoles.AddUserRole(userRole);
} }
} }

View File

@ -89,9 +89,9 @@ namespace Oqtane.Infrastructure
} }
// validate recipient // validate recipient
if (string.IsNullOrEmpty(notification.ToEmail)) if (string.IsNullOrEmpty(notification.ToEmail) || !MailAddress.TryCreate(notification.ToEmail, out _))
{ {
log += "Recipient Missing For NotificationId: " + notification.NotificationId + "<br />"; log += $"NotificationId: {notification.NotificationId} - Has Missing Or Invalid Recipient {notification.ToEmail}<br />";
notification.IsDeleted = true; notification.IsDeleted = true;
notificationRepository.UpdateNotification(notification); notificationRepository.UpdateNotification(notification);
} }

View File

@ -59,8 +59,15 @@ namespace Oqtane.Infrastructure
var currentTime = DateTime.UtcNow; var currentTime = DateTime.UtcNow;
var lastIndexedOn = Convert.ToDateTime(siteSettings.GetValue(SearchLastIndexedOnSetting, DateTime.MinValue.ToString())); var lastIndexedOn = Convert.ToDateTime(siteSettings.GetValue(SearchLastIndexedOnSetting, DateTime.MinValue.ToString()));
if (lastIndexedOn == DateTime.MinValue)
{
// reset index
log += $"*Site Index Reset*<br />";
await searchService.DeleteSearchContentsAsync(site.SiteId);
}
var ignorePages = siteSettings.GetValue(SearchIgnorePagesSetting, "").Split(','); var ignorePages = siteSettings.GetValue(SearchIgnorePagesSetting, "").Split(',');
var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "").Split(','); var ignoreEntities = siteSettings.GetValue(SearchIgnoreEntitiesSetting, "File").Split(',');
var pages = pageRepository.GetPages(site.SiteId); var pages = pageRepository.GetPages(site.SiteId);
var pageModules = pageModuleRepository.GetPageModules(site.SiteId); var pageModules = pageModuleRepository.GetPageModules(site.SiteId);

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
@ -20,8 +21,9 @@ namespace Oqtane.Infrastructure
private readonly IHttpContextAccessor _accessor; private readonly IHttpContextAccessor _accessor;
private readonly IUserRoleRepository _userRoles; private readonly IUserRoleRepository _userRoles;
private readonly INotificationRepository _notifications; private readonly INotificationRepository _notifications;
private readonly ILogger<LogManager> _filelogger;
public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications) public LogManager(ILogRepository logs, ITenantManager tenantManager, IConfigManager config, IUserPermissions userPermissions, IHttpContextAccessor accessor, IUserRoleRepository userRoles, INotificationRepository notifications, ILogger<LogManager> filelogger)
{ {
_logs = logs; _logs = logs;
_tenantManager = tenantManager; _tenantManager = tenantManager;
@ -30,24 +32,25 @@ namespace Oqtane.Infrastructure
_accessor = accessor; _accessor = accessor;
_userRoles = userRoles; _userRoles = userRoles;
_notifications = notifications; _notifications = notifications;
_filelogger = filelogger;
} }
public void Log(LogLevel level, object @class, LogFunction function, string message, params object[] args) public void Log(Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
{ {
Log(-1, level, @class, function, null, message, args); Log(-1, level, @class, function, null, message, args);
} }
public void Log(LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args) public void Log(Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
{ {
Log(-1, level, @class, function, exception, message, args); Log(-1, level, @class, function, exception, message, args);
} }
public void Log(int siteId, LogLevel level, object @class, LogFunction function, string message, params object[] args) public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, string message, params object[] args)
{ {
Log(siteId, level, @class, function, null, message, args); Log(siteId, level, @class, function, null, message, args);
} }
public void Log(int siteId, LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args) public void Log(int siteId, Shared.LogLevel level, object @class, LogFunction function, Exception exception, string message, params object[] args)
{ {
Log log = new Log(); Log log = new Log();
@ -60,7 +63,6 @@ namespace Oqtane.Infrastructure
log.SiteId = alias.SiteId; log.SiteId = alias.SiteId;
} }
} }
if (log.SiteId == -1) return; // logs must be site specific
log.PageId = null; log.PageId = null;
log.ModuleId = null; log.ModuleId = null;
@ -92,7 +94,7 @@ namespace Oqtane.Infrastructure
log.Feature = log.Category; log.Feature = log.Category;
} }
log.Function = Enum.GetName(typeof(LogFunction), function); log.Function = Enum.GetName(typeof(LogFunction), function);
log.Level = Enum.GetName(typeof(LogLevel), level); log.Level = Enum.GetName(typeof(Shared.LogLevel), level);
if (exception != null) if (exception != null)
{ {
log.Exception = exception.ToString(); log.Exception = exception.ToString();
@ -112,14 +114,14 @@ namespace Oqtane.Infrastructure
public void Log(Log log) public void Log(Log log)
{ {
LogLevel minlevel = LogLevel.Information; var minlevel = Shared.LogLevel.Information;
var section = _config.GetSection("Logging:LogLevel:Default"); var section = _config.GetSection("Logging:LogLevel:Default");
if (section.Exists()) if (section.Exists())
{ {
minlevel = Enum.Parse<LogLevel>(section.Value); minlevel = Enum.Parse<Shared.LogLevel>(section.Value);
} }
if (Enum.Parse<LogLevel>(log.Level) >= minlevel) if (Enum.Parse<Shared.LogLevel>(log.Level) >= minlevel)
{ {
log.LogDate = DateTime.UtcNow; log.LogDate = DateTime.UtcNow;
log.Server = Environment.MachineName; log.Server = Environment.MachineName;
@ -127,12 +129,19 @@ namespace Oqtane.Infrastructure
log = ProcessStructuredLog(log); log = ProcessStructuredLog(log);
try try
{ {
_logs.AddLog(log); if (log.SiteId != -1)
SendNotification(log); {
_logs.AddLog(log);
SendNotification(log);
}
else // use file logger as fallback when site cannot be determined
{
_filelogger.Log(GetLogLevel(log.Level), "[" + log.Category + "] " + log.Message);
}
} }
catch catch
{ {
// an error occurred writing to the database // an error occurred writing the log
} }
} }
} }
@ -156,17 +165,11 @@ namespace Oqtane.Infrastructure
names.Add(message.Substring(index + 1, message.IndexOf("}", index) - index - 1)); names.Add(message.Substring(index + 1, message.IndexOf("}", index) - index - 1));
if (values.Length > (names.Count - 1)) if (values.Length > (names.Count - 1))
{ {
if (values[names.Count - 1] == null) var value = (values[names.Count - 1] == null) ? "null" : values[names.Count - 1].ToString();
{ message = message.Replace("{" + names[names.Count - 1] + "}", value);
message = message.Replace("{" + names[names.Count - 1] + "}", "null");
}
else
{
message = message.Replace("{" + names[names.Count - 1] + "}", values[names.Count - 1].ToString());
}
} }
} }
index = message.IndexOf("{", index + 1); index = (index < message.Length - 1) ? message.IndexOf("{", index + 1) : -1;
} }
// rebuild properties into dictionary // rebuild properties into dictionary
Dictionary<string, object> propertyDictionary = new Dictionary<string, object>(); Dictionary<string, object> propertyDictionary = new Dictionary<string, object>();
@ -195,13 +198,13 @@ namespace Oqtane.Infrastructure
private void SendNotification(Log log) private void SendNotification(Log log)
{ {
LogLevel notifylevel = LogLevel.Error; Shared.LogLevel notifylevel = Shared.LogLevel.Error;
var section = _config.GetSection("Logging:LogLevel:Notify"); var section = _config.GetSection("Logging:LogLevel:Notify");
if (section.Exists()) if (section.Exists())
{ {
notifylevel = Enum.Parse<LogLevel>(section.Value); notifylevel = Enum.Parse<Shared.LogLevel>(section.Value);
} }
if (Enum.Parse<LogLevel>(log.Level) >= notifylevel) if (Enum.Parse<Shared.LogLevel>(log.Level) >= notifylevel)
{ {
var subject = $"Site {log.Level} Notification"; var subject = $"Site {log.Level} Notification";
string body = $"Log Message: {log.Message}"; string body = $"Log Message: {log.Message}";
@ -220,5 +223,26 @@ namespace Oqtane.Infrastructure
} }
} }
} }
private Microsoft.Extensions.Logging.LogLevel GetLogLevel(string level)
{
switch (Enum.Parse<Shared.LogLevel>(level))
{
case Shared.LogLevel.Trace:
return Microsoft.Extensions.Logging.LogLevel.Trace;
case Shared.LogLevel.Debug:
return Microsoft.Extensions.Logging.LogLevel.Debug;
case Shared.LogLevel.Information:
return Microsoft.Extensions.Logging.LogLevel.Information;
case Shared.LogLevel.Warning:
return Microsoft.Extensions.Logging.LogLevel.Warning;
case Shared.LogLevel.Error:
return Microsoft.Extensions.Logging.LogLevel.Error;
case Shared.LogLevel.Critical:
return Microsoft.Extensions.Logging.LogLevel.Critical;
default:
return Microsoft.Extensions.Logging.LogLevel.None;
}
}
} }
} }

View File

@ -3,8 +3,7 @@ using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Oqtane.Extensions; using Oqtane.Extensions;
using Oqtane.Models; using Oqtane.Managers;
using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Shared; using Oqtane.Shared;
@ -59,19 +58,18 @@ namespace Oqtane.Infrastructure
if (userid != null && username != null) if (userid != null && username != null)
{ {
// create user identity var _users = context.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
var user = new User var user = _users.GetUser(userid, alias.SiteId); // cached
if (user != null && !user.IsDeleted)
{ {
UserId = int.Parse(userid), var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user);
Username = username context.User = new ClaimsPrincipal(claimsidentity);
}; logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
}
// set claims identity (note jwt already contains the roles - we are reloading to ensure most accurate permissions) else
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository; {
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList()); logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validated But User {Username} Does Not Exist Or Is Deleted", user.Username);
context.User = new ClaimsPrincipal(claimsidentity); }
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For UserId {UserId} And Username {Username}", user.UserId, user.Username);
} }
else else
{ {

View File

@ -13,11 +13,13 @@ namespace Oqtane.Managers
Task<User> UpdateUser(User user); Task<User> UpdateUser(User user);
Task DeleteUser(int userid, int siteid); Task DeleteUser(int userid, int siteid);
Task<User> LoginUser(User user, bool setCookie, bool isPersistent); Task<User> LoginUser(User user, bool setCookie, bool isPersistent);
Task LogoutUserEverywhere(User user);
Task<User> VerifyEmail(User user, string token); Task<User> VerifyEmail(User user, string token);
Task ForgotPassword(User user); Task ForgotPassword(User user);
Task<User> ResetPassword(User user, string token); Task<User> ResetPassword(User user, string token);
User VerifyTwoFactor(User user, string token); User VerifyTwoFactor(User user, string token);
Task<User> LinkExternalAccount(User user, string token, string type, string key, string name); Task<User> LinkExternalAccount(User user, string token, string type, string key, string name);
Task<UserValidateResult> ValidateUser(string username, string email, string password);
Task<bool> ValidatePassword(string password); Task<bool> ValidatePassword(string password);
Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify); Task<Dictionary<string, string>> ImportUsers(int siteId, string filePath, bool notify);
} }

View File

@ -64,6 +64,7 @@ namespace Oqtane.Managers
{ {
user.SiteId = siteid; user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId); user.Roles = GetUserRoles(user.UserId, user.SiteId);
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId) user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
} }
@ -230,6 +231,7 @@ namespace Oqtane.Managers
{ {
identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password); identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
await _identityUserManager.UpdateAsync(identityuser); await _identityUserManager.UpdateAsync(identityuser);
await _identityUserManager.UpdateSecurityStampAsync(identityuser); // will force user to sign in again
} }
else else
{ {
@ -240,7 +242,8 @@ namespace Oqtane.Managers
if (user.Email != identityuser.Email) if (user.Email != identityuser.Email)
{ {
await _identityUserManager.SetEmailAsync(identityuser, user.Email); identityuser.Email = user.Email;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
// if email address changed and it is not confirmed, verification is required for new email address // if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed) if (!user.EmailConfirmed)
@ -262,7 +265,6 @@ namespace Oqtane.Managers
user = _users.UpdateUser(user); user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload); _syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
_cache.Remove($"user:{user.UserId}:{alias.SiteKey}");
user.Password = ""; // remove sensitive information user.Password = ""; // remove sensitive information
_logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user); _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
} }
@ -370,7 +372,7 @@ namespace Oqtane.Managers
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = LastIPAddress; user.LastIPAddress = LastIPAddress;
_users.UpdateUser(user); _users.UpdateUser(user);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress);
if (setCookie) if (setCookie)
{ {
@ -417,6 +419,16 @@ namespace Oqtane.Managers
return user; return user;
} }
public async Task LogoutUserEverywhere(User user)
{
var identityuser = await _identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
await _identityUserManager.UpdateSecurityStampAsync(identityuser);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
}
}
public async Task<User> VerifyEmail(User user, string token) public async Task<User> VerifyEmail(User user, string token)
{ {
@ -528,6 +540,30 @@ namespace Oqtane.Managers
return user; return user;
} }
public async Task<UserValidateResult> ValidateUser(string username, string email, string password)
{
var validateResult = new UserValidateResult { Succeeded = true };
//validate username
var allowedChars = _identityUserManager.Options.User.AllowedUserNameCharacters;
if (string.IsNullOrWhiteSpace(username) || (!string.IsNullOrEmpty(allowedChars) && username.Any(c => !allowedChars.Contains(c))))
{
validateResult.Succeeded = false;
validateResult.Errors.Add("Message.Username.Invalid", string.Empty);
}
//validate password
var passwordValidator = new PasswordValidator<IdentityUser>();
var passwordResult = await passwordValidator.ValidateAsync(_identityUserManager, null, password);
if (!passwordResult.Succeeded)
{
validateResult.Succeeded = false;
validateResult.Errors.Add("Message.Password.Invalid", string.Empty);
}
return validateResult;
}
public async Task<bool> ValidatePassword(string password) public async Task<bool> ValidatePassword(string password)
{ {
var validator = new PasswordValidator<IdentityUser>(); var validator = new PasswordValidator<IdentityUser>();

View File

@ -319,6 +319,19 @@ namespace Oqtane.Migrations.EntityBuilders
schema: Schema); schema: Schema);
} }
public virtual void AddForeignKey(string foreignKeyName, string columnName, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDelete)
{
_migrationBuilder.AddForeignKey(
name: RewriteName(foreignKeyName),
table: RewriteName(EntityTableName),
column: RewriteName(columnName),
principalTable: RewriteName(principalTable),
principalColumn: RewriteName(principalColumn),
principalSchema: RewriteName(principalSchema),
onDelete: onDelete,
schema: Schema);
}
/// <summary> /// <summary>
/// Creates a Migration to add an Index to the Entity (table) /// Creates a Migration to add an Index to the Entity (table)
/// </summary> /// </summary>
@ -368,6 +381,7 @@ namespace Oqtane.Migrations.EntityBuilders
column: foreignKey.Column, column: foreignKey.Column,
principalTable: RewriteName(foreignKey.PrincipalTable), principalTable: RewriteName(foreignKey.PrincipalTable),
principalColumn: RewriteName(foreignKey.PrincipalColumn), principalColumn: RewriteName(foreignKey.PrincipalColumn),
principalSchema: RewriteName(foreignKey.PrincipalSchema),
onDelete: foreignKey.OnDeleteAction); onDelete: foreignKey.OnDeleteAction);
} }
@ -381,6 +395,7 @@ namespace Oqtane.Migrations.EntityBuilders
column: RewriteName(foreignKey.ColumnName), column: RewriteName(foreignKey.ColumnName),
principalTable: RewriteName(foreignKey.PrincipalTable), principalTable: RewriteName(foreignKey.PrincipalTable),
principalColumn: RewriteName(foreignKey.PrincipalColumn), principalColumn: RewriteName(foreignKey.PrincipalColumn),
principalSchema: RewriteName(foreignKey.PrincipalSchema),
onDelete: foreignKey.OnDeleteAction, onDelete: foreignKey.OnDeleteAction,
schema: Schema); schema: Schema);
} }

View File

@ -16,6 +16,16 @@ namespace Oqtane.Migrations
OnDeleteAction = onDeleteAction; OnDeleteAction = onDeleteAction;
} }
public ForeignKey(string name, Expression<Func<TEntityBuilder, object>> column, string principalTable, string principalColumn, string principalSchema, ReferentialAction onDeleteAction)
{
Name = name;
Column = column;
PrincipalTable = principalTable;
PrincipalColumn = principalColumn;
PrincipalSchema = principalSchema;
OnDeleteAction = onDeleteAction;
}
public string Name { get; } public string Name { get; }
public Expression<Func<TEntityBuilder, object>> Column { get;} public Expression<Func<TEntityBuilder, object>> Column { get;}
@ -34,6 +44,8 @@ namespace Oqtane.Migrations
public string PrincipalColumn { get; } public string PrincipalColumn { get; }
public string PrincipalSchema { get; } = "";
} }
} }

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.05.02.04.01")]
public class RemoveLanguageName : MultiDatabaseMigration
{
public RemoveLanguageName(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var languageEntityBuilder = new LanguageEntityBuilder(migrationBuilder, ActiveDatabase);
languageEntityBuilder.DropColumn("Name");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -45,10 +45,25 @@ namespace Oqtane.Modules.Admin.Files.Manager
var path = folder.Path + file.Name; var path = folder.Path + file.Name;
var body = ""; var body = "";
if (DocumentExtensions.Contains(Path.GetExtension(file.Name))) if (System.IO.File.Exists(_fileRepository.GetFilePath(file)))
{ {
// get the contents of the file // only non-binary files can be indexed
body = System.IO.File.ReadAllText(_fileRepository.GetFilePath(file)); if (DocumentExtensions.Contains(Path.GetExtension(file.Name)))
{
// get the contents of the file
try
{
body = System.IO.File.ReadAllText(_fileRepository.GetFilePath(file));
}
catch
{
// could not read the file
}
}
}
else
{
removed = true; // file does not exist on disk
} }
var searchContent = new SearchContent var searchContent = new SearchContent

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>5.2.2</Version> <Version>6.0.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,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/v5.2.2</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -33,21 +33,21 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" /> <EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.62" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.8" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-preview2.24304.8" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0">
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.8" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.71" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="8.0.8" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.8" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.9" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" /> <ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />

View File

@ -1,5 +1,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Net; using System.Net;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
@ -14,6 +15,7 @@ using Oqtane.Infrastructure;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security; using Oqtane.Security;
using Oqtane.Services;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Pages namespace Oqtane.Pages
@ -28,8 +30,10 @@ namespace Oqtane.Pages
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
private readonly Alias _alias; private readonly Alias _alias;
private readonly IImageService _imageService;
private readonly ISettingRepository _settingRepository;
public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) public FilesModel(IWebHostEnvironment environment, IFileRepository files, IUserPermissions userPermissions, IUrlMappingRepository urlMappings, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager, IImageService imageService, ISettingRepository settingRepository)
{ {
_environment = environment; _environment = environment;
_files = files; _files = files;
@ -38,111 +42,228 @@ namespace Oqtane.Pages
_syncManager = syncManager; _syncManager = syncManager;
_logger = logger; _logger = logger;
_alias = tenantManager.GetAlias(); _alias = tenantManager.GetAlias();
_imageService = imageService;
_settingRepository = settingRepository;
} }
public IActionResult OnGet(string path) public IActionResult OnGet(string path)
{ {
if (!string.IsNullOrEmpty(path)) if (string.IsNullOrWhiteSpace(path))
{
path = path.Replace("\\", "/");
var folderpath = "";
var filename = "";
bool download = false;
if (Request.Query.ContainsKey("download"))
{
download = true;
}
var segments = path.Split('/');
if (segments.Length > 0)
{
filename = segments[segments.Length - 1].ToLower();
if (segments.Length > 1)
{
folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/";
}
}
Models.File file;
if (folderpath == "id/" && int.TryParse(filename, out int fileid))
{
file = _files.GetFile(fileid, false);
}
else
{
file = _files.GetFile(_alias.SiteId, folderpath, filename);
}
if (file != null)
{
if (file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
// calculate ETag using last modified date and file size
var etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16);
var header = "";
if (HttpContext.Request.Headers.ContainsKey(HeaderNames.IfNoneMatch))
{
header = HttpContext.Request.Headers[HeaderNames.IfNoneMatch].ToString();
}
if (!header.Equals(etag))
{
var filepath = _files.GetFilePath(file);
if (System.IO.File.Exists(filepath))
{
if (download)
{
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
return PhysicalFile(filepath, file.GetMimeType(), file.Name);
}
else
{
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
return PhysicalFile(filepath, file.GetMimeType());
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
}
else
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
return Content(String.Empty);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
}
else
{
// look for url mapping
var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
var url = urlMapping.MappedUrl;
if (!url.StartsWith("http"))
{
var uri = new Uri(HttpContext.Request.GetEncodedUrl());
url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url;
}
return RedirectPermanent(url);
}
}
}
else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt - Path Not Specified For Site {SiteId}", _alias.SiteId);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
} }
path = path.Replace("\\", "/");
var folderpath = "";
var filename = "";
bool download = false;
if (Request.Query.ContainsKey("download"))
{
download = true;
}
var segments = path.Split('/');
if (segments.Length > 0)
{
filename = segments[segments.Length - 1].ToLower();
if (segments.Length > 1)
{
folderpath = string.Join("/", segments, 0, segments.Length - 1).ToLower() + "/";
}
}
Models.File file;
if (folderpath == "id/" && int.TryParse(filename, out int fileid))
{
file = _files.GetFile(fileid, false);
}
else
{
file = _files.GetFile(_alias.SiteId, folderpath, filename);
}
if (file == null)
{
// look for url mapping
var urlMapping = _urlMappings.GetUrlMapping(_alias.SiteId, "files/" + folderpath + filename);
if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl))
{
var url = urlMapping.MappedUrl;
if (!url.StartsWith("http"))
{
var uri = new Uri(HttpContext.Request.GetEncodedUrl());
url = uri.Scheme + "://" + uri.Authority + ((!string.IsNullOrEmpty(_alias.Path)) ? "/" + _alias.Path : "") + "/" + url;
}
// appends the query string to the redirect url
if (Request.QueryString.HasValue && !string.IsNullOrWhiteSpace(Request.QueryString.Value))
{
if (url.Contains('?'))
{
url += "&";
}
else
{
url += "?";
}
url += Request.QueryString.Value.Substring(1);
}
return RedirectPermanent(url);
}
return BrokenFile();
}
if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
}
string etag;
string downloadName = file.Name;
string filepath = _files.GetFilePath(file);
var etagValue = file.ModifiedOn.Ticks ^ file.Size;
bool isRequestingImageManipulation = false;
int width = 0;
int height = 0;
if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0)
{
isRequestingImageManipulation = true;
etagValue ^= (width * 31);
}
if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0)
{
isRequestingImageManipulation = true;
etagValue ^= (height * 17);
}
Request.Query.TryGetValue("mode", out var mode);
Request.Query.TryGetValue("position", out var position);
Request.Query.TryGetValue("background", out var background);
if (width > 0 || height > 0)
{
if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode();
if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode();
if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode();
}
int rotate;
if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0)
{
isRequestingImageManipulation = true;
etagValue ^= (rotate * 13);
}
if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString()))
{
isRequestingImageManipulation = true;
etagValue ^= format.ToString().GetHashCode();
}
etag = Convert.ToString(etagValue, 16);
var header = "";
if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch))
{
header = ifNoneMatch.ToString();
}
if (header.Equals(etag))
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
return Content(String.Empty);
}
if (!System.IO.File.Exists(filepath))
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return BrokenFile();
}
if (isRequestingImageManipulation)
{
var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue;
_ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles;
if (!_ImageFiles.Split(',').Contains(file.Extension.ToLower()))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Is Not An Image {File}", file);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
}
Request.Query.TryGetValue("recreate", out var recreate);
if (!bool.TryParse(recreate, out _)) recreate = "false";
if (!_imageService.GetAvailableFormats().Contains(format.ToString())) format = "png";
if (width == 0 && height == 0)
{
width = file.ImageWidth;
height = file.ImageHeight;
}
string imagepath = filepath.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
if (!System.IO.File.Exists(imagepath) || bool.Parse(recreate))
{
// user has edit access to folder or folder supports the image size being created
if (_userPermissions.IsAuthorized(User, PermissionNames.Edit, file.Folder.PermissionList) ||
(!string.IsNullOrEmpty(file.Folder.ImageSizes) && (file.Folder.ImageSizes == "*" || file.Folder.ImageSizes.ToLower().Split(",").Contains(width.ToString() + "x" + height.ToString()))))
{
imagepath = _imageService.CreateImage(filepath, width, height, mode, position, background, rotateStr, format, imagepath);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Invalid Image Size For Folder {Folder} {Width} {Height}", file.Folder, width, height);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
}
}
if (string.IsNullOrWhiteSpace(imagepath))
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, "Error Displaying Image For File {File} {Width} {Height}", file, widthStr, heightStr);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return BrokenFile();
}
downloadName = file.Name.Replace(Path.GetExtension(filepath), "." + width.ToString() + "x" + height.ToString() + "." + format);
filepath = imagepath;
}
if (!System.IO.File.Exists(filepath))
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath);
HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
return BrokenFile();
}
if (download)
{
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
return PhysicalFile(filepath, file.GetMimeType(), downloadName);
}
else
{
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
return PhysicalFile(filepath, file.GetMimeType());
}
}
private PhysicalFileResult BrokenFile()
{
// broken link // broken link
string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png"); string errorPath = Path.Combine(Utilities.PathCombine(_environment.ContentRootPath, "wwwroot/images"), "error.png");
return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath)); return PhysicalFile(errorPath, MimeUtilities.GetMimeType(errorPath));

View File

@ -23,7 +23,7 @@ namespace Oqtane.Pages
_syncManager = syncManager; _syncManager = syncManager;
} }
public async Task<IActionResult> OnPostAsync(string returnurl) public async Task<IActionResult> OnPostAsync(string returnurl, string everywhere)
{ {
if (HttpContext.User != null) if (HttpContext.User != null)
{ {
@ -31,6 +31,10 @@ namespace Oqtane.Pages
var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId); var user = _userManager.GetUser(HttpContext.User.Identity.Name, alias.SiteId);
if (user != null) if (user != null)
{ {
if (everywhere == "true")
{
await _userManager.LogoutUserEverywhere(user);
}
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, SyncEventActions.Reload); _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, SyncEventActions.Reload);
} }

View File

@ -235,9 +235,9 @@ namespace Oqtane.Providers
return text; return text;
} }
public Task ResetIndex() public Task DeleteSearchContent(int siteId)
{ {
_searchContentRepository.DeleteAllSearchContent(); _searchContentRepository.DeleteAllSearchContent(siteId);
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@ -40,7 +40,7 @@ namespace Oqtane.Providers
} }
else else
{ {
return true; return authState.User.SecurityStamp() == user.SecurityStamp;
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More