commit
65c1b04772
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -22,10 +22,14 @@ Oqtane.Server/Packages
|
||||||
Oqtane.Server/wwwroot/Content
|
Oqtane.Server/wwwroot/Content
|
||||||
Oqtane.Server/wwwroot/Packages/*.log
|
Oqtane.Server/wwwroot/Packages/*.log
|
||||||
|
|
||||||
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
|
||||||
|
Oqtane.Server/wwwroot/Modules/Templates/*
|
||||||
|
!Oqtane.Server/wwwroot/Modules/Templates/External
|
||||||
|
|
||||||
Oqtane.Server/wwwroot/Themes
|
Oqtane.Server/wwwroot/Themes/*
|
||||||
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
|
!Oqtane.Server/wwwroot/Themes/Oqtane.Themes.*
|
||||||
!Oqtane.Server/wwwroot/Themes/Templates
|
!Oqtane.Server/wwwroot/Themes/Templates
|
||||||
|
Oqtane.Server/wwwroot/Themes/Templates/*
|
||||||
|
Oqtane.Server/wwwroot/Themes/Templates/External
|
||||||
|
|
|
@ -162,7 +162,7 @@
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
// include CSS
|
// include CSS
|
||||||
var content = "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/css/bootstrap.min.css\" integrity=\"sha512-t4GWSVZO1eC8BM339Xd7Uphw5s17a86tIZIj8qRxhnKub6WoyhnrxeCIMeAqBPgdZGlCcG2PrZjMc+Wr78+5Xg==\" 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"];
|
||||||
|
@ -217,7 +217,7 @@
|
||||||
{
|
{
|
||||||
// include JavaScript
|
// include JavaScript
|
||||||
var interop = new Interop(JSRuntime);
|
var interop = new Interop(JSRuntime);
|
||||||
await interop.IncludeScript("", "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.0/js/bootstrap.bundle.min.js", "sha512-VK2zcvntEufaimc+efOYi622VN5ZacdnufnmX7zIhCPmjhKnOi9ZDMtg1/ug5l183f19gG1/cBstPO4D8N/Img==", "anonymous", "", "head");
|
await interop.IncludeScript("", Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous", "", "head");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,14 +8,12 @@
|
||||||
@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>
|
|
||||||
</Authorizing>
|
|
||||||
<Authorized>
|
|
||||||
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
|
<ModuleMessage Message="@Localizer["Info.SignedIn"]" Type="MessageType.Info" />
|
||||||
</Authorized>
|
}
|
||||||
<NotAuthorized>
|
else
|
||||||
|
{
|
||||||
@if (!twofactor)
|
@if (!twofactor)
|
||||||
{
|
{
|
||||||
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
<form @ref="login" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
||||||
|
@ -23,7 +21,9 @@
|
||||||
@if (_allowexternallogin)
|
@if (_allowexternallogin)
|
||||||
{
|
{
|
||||||
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
|
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">@Localizer["Use"] @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
|
||||||
<br /><br />
|
<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
}
|
}
|
||||||
@if (_allowsitelogin)
|
@if (_allowsitelogin)
|
||||||
{
|
{
|
||||||
|
@ -49,11 +49,15 @@
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</button>
|
<button type="button" class="btn btn-primary" @onclick="Login">@SharedLocalizer["Login"]</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>
|
||||||
<br /><br />
|
<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
|
<button type="button" class="btn btn-secondary" @onclick="Forgot">@Localizer["ForgotPassword"]</button>
|
||||||
@if (PageState.Site.AllowRegistration)
|
@if (PageState.Site.AllowRegistration)
|
||||||
{
|
{
|
||||||
<br /><br />
|
<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
|
<NavLink href="@NavigateUrl("register")">@Localizer["Register"]</NavLink>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,8 +78,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
@inject IStringLocalizer<Settings> Localizer
|
@inject IStringLocalizer<Settings> Localizer
|
||||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||||
|
|
||||||
|
@if (_initialized)
|
||||||
|
{
|
||||||
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
||||||
<TabStrip>
|
<TabStrip>
|
||||||
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
|
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
|
||||||
|
@ -128,10 +130,12 @@
|
||||||
<br />
|
<br />
|
||||||
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
|
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon"></AuditInfo>
|
||||||
</form>
|
</form>
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
|
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
|
||||||
|
|
||||||
|
private bool _initialized = false;
|
||||||
private ElementReference form;
|
private ElementReference form;
|
||||||
private bool validated = false;
|
private bool validated = false;
|
||||||
private List<ThemeControl> _containers = new List<ThemeControl>();
|
private List<ThemeControl> _containers = new List<ThemeControl>();
|
||||||
|
@ -163,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;
|
||||||
|
@ -182,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))
|
||||||
|
@ -231,6 +235,8 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SaveModule()
|
private async Task SaveModule()
|
||||||
|
|
|
@ -155,9 +155,16 @@
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<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)
|
||||||
|
{
|
||||||
|
@if (theme.TypeName == PageState.Site.DefaultThemeType)
|
||||||
|
{
|
||||||
|
<option value="@theme.TypeName">*@theme.Name*</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<option value="@theme.TypeName">@theme.Name</option>
|
<option value="@theme.TypeName">@theme.Name</option>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -171,9 +171,16 @@
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<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)
|
||||||
|
{
|
||||||
|
@if (theme.TypeName == PageState.Site.DefaultThemeType)
|
||||||
|
{
|
||||||
|
<option value="@theme.TypeName">*@theme.Name*</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<option value="@theme.TypeName">@theme.Name</option>
|
<option value="@theme.TypeName">@theme.Name</option>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -261,9 +268,16 @@
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<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)
|
||||||
|
{
|
||||||
|
@if (theme.TypeName == PageState.Site.DefaultThemeType)
|
||||||
|
{
|
||||||
|
<option value="@theme.TypeName">*@theme.Name*</option>
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
<option value="@theme.TypeName">@theme.Name</option>
|
<option value="@theme.TypeName">@theme.Name</option>
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,14 +11,12 @@
|
||||||
{
|
{
|
||||||
if (!_userCreated)
|
if (!_userCreated)
|
||||||
{
|
{
|
||||||
<AuthorizeView Roles="@RoleNames.Registered">
|
if (PageState.User != null)
|
||||||
<Authorizing>
|
{
|
||||||
<text>...</text>
|
|
||||||
</Authorizing>
|
|
||||||
<Authorized>
|
|
||||||
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
|
<ModuleMessage Message="@Localizer["Info.Registration.Exists"]" Type="MessageType.Info" />
|
||||||
</Authorized>
|
}
|
||||||
<NotAuthorized>
|
else
|
||||||
|
{
|
||||||
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
|
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
|
||||||
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
<form @ref="form" class="@(validated ? "was-validated" : "needs-validation")" novalidate>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -64,12 +62,13 @@
|
||||||
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
|
<button type="button" class="btn btn-secondary" @onclick="Cancel">@SharedLocalizer["Cancel"]</button>
|
||||||
@if (_allowsitelogin)
|
@if (_allowsitelogin)
|
||||||
{
|
{
|
||||||
<br /><br />
|
<br />
|
||||||
|
|
||||||
|
<br />
|
||||||
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
|
<NavLink href="@NavigateUrl("login")">@Localizer["Login"]</NavLink>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
</NotAuthorized>
|
}
|
||||||
</AuthorizeView>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
@inject ISettingService SettingService
|
@inject ISettingService SettingService
|
||||||
@inject IStringLocalizer<Index> Localizer
|
@inject IStringLocalizer<Index> Localizer
|
||||||
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
@inject IStringLocalizer<SharedResources> SharedLocalizer
|
||||||
|
@attribute [StreamRendering] // attribute allows the progress indicator to be displayed
|
||||||
|
|
||||||
<div class="search-result-container">
|
<div class="search-result-container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
<TabPanel Name="Identity" ResourceKey="Identity">
|
<TabPanel Name="Identity" ResourceKey="Identity">
|
||||||
@if (profiles != null)
|
@if (profiles != null)
|
||||||
{
|
{
|
||||||
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
|
|
||||||
<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="username" HelpText="A unique username for a user. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
|
<Label Class="col-sm-3" For="username" HelpText="A unique username for a user. Note that this field can not be modified once it is saved." ResourceKey="Username"></Label>
|
||||||
|
@ -22,24 +21,6 @@
|
||||||
<input id="username" class="form-control" @bind="@_username" />
|
<input id="username" class="form-control" @bind="@_username" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-1 align-items-center">
|
|
||||||
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" 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="confirm" HelpText="Please enter the password again to confirm it matches with the value 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">
|
<div class="row mb-1 align-items-center">
|
||||||
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
|
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
|
@ -123,12 +104,7 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private bool _initialized = false;
|
private bool _initialized = false;
|
||||||
private string _passwordrequirements;
|
|
||||||
private string _username = string.Empty;
|
private string _username = string.Empty;
|
||||||
private string _password = string.Empty;
|
|
||||||
private string _passwordtype = "password";
|
|
||||||
private string _togglepassword = string.Empty;
|
|
||||||
private string _confirm = string.Empty;
|
|
||||||
private string _email = string.Empty;
|
private string _email = string.Empty;
|
||||||
private string _displayname = string.Empty;
|
private string _displayname = string.Empty;
|
||||||
private string _notify = "True";
|
private string _notify = "True";
|
||||||
|
@ -142,8 +118,6 @@
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId);
|
|
||||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
|
||||||
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId);
|
||||||
settings = new Dictionary<string, string>();
|
settings = new Dictionary<string, string>();
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
@ -169,16 +143,14 @@
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_username != string.Empty && _password != string.Empty && _confirm != string.Empty && _email != string.Empty)
|
if (_username != string.Empty && _email != string.Empty)
|
||||||
{
|
|
||||||
if (_password == _confirm)
|
|
||||||
{
|
{
|
||||||
if (ValidateProfiles())
|
if (ValidateProfiles())
|
||||||
{
|
{
|
||||||
var user = new User();
|
var user = new User();
|
||||||
user.SiteId = PageState.Site.SiteId;
|
user.SiteId = PageState.Site.SiteId;
|
||||||
user.Username = _username;
|
user.Username = _username;
|
||||||
user.Password = _password;
|
user.Password = ""; // will be auto generated
|
||||||
user.Email = _email;
|
user.Email = _email;
|
||||||
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
|
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
|
||||||
user.PhotoFileId = null;
|
user.PhotoFileId = null;
|
||||||
|
@ -200,11 +172,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
AddModuleMessage(Localizer["Message.Password.NoMatch"], MessageType.Warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
|
AddModuleMessage(Localizer["Message.Required.ProfileInfo"], MessageType.Warning);
|
||||||
}
|
}
|
||||||
|
@ -252,18 +219,4 @@
|
||||||
var value = (string)e.Value;
|
var value = (string)e.Value;
|
||||||
settings = SettingService.SetSetting(settings, SettingName, value);
|
settings = SettingService.SetSetting(settings, SettingName, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TogglePassword()
|
|
||||||
{
|
|
||||||
if (_passwordtype == "password")
|
|
||||||
{
|
|
||||||
_passwordtype = "text";
|
|
||||||
_togglepassword = SharedLocalizer["HidePassword"];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_passwordtype = "password";
|
|
||||||
_togglepassword = SharedLocalizer["ShowPassword"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,11 +333,28 @@ 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">
|
||||||
|
<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">
|
<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">
|
||||||
|
@ -457,6 +474,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;
|
||||||
|
@ -521,6 +540,8 @@ else
|
||||||
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
|
_nameclaimtype = SettingService.GetSetting(settings, "ExternalLogin:NameClaimType", "name");
|
||||||
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
|
_emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "email");
|
||||||
_roleclaimtype = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimType", "");
|
_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", "");
|
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
|
||||||
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
|
_domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", "");
|
||||||
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
|
_createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true");
|
||||||
|
@ -614,6 +635,8 @@ else
|
||||||
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:RoleClaimMappings", _roleclaimmappings, true);
|
||||||
|
settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, true);
|
||||||
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
|
settings = SettingService.SetSetting(settings, "ExternalLogin:ProfileClaimTypes", _profileclaimtypes, true);
|
||||||
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
|
settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true);
|
||||||
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
|
settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true);
|
||||||
|
|
|
@ -173,6 +173,12 @@ else
|
||||||
_editmode = bool.Parse(EditMode);
|
_editmode = bool.Parse(EditMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text = Localize(nameof(Text), Text);
|
||||||
|
Header = Localize(nameof(Header), Header);
|
||||||
|
Message = Localize(nameof(Message), Message);
|
||||||
|
|
||||||
|
_openText = Text;
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(IconName))
|
if (!string.IsNullOrEmpty(IconName))
|
||||||
{
|
{
|
||||||
if (IconOnly)
|
if (IconOnly)
|
||||||
|
@ -191,11 +197,6 @@ else
|
||||||
_iconSpan = $"<span class=\"{IconName}\"></span> ";
|
_iconSpan = $"<span class=\"{IconName}\"></span> ";
|
||||||
}
|
}
|
||||||
|
|
||||||
Text = Localize(nameof(Text), Text);
|
|
||||||
Header = Localize(nameof(Header), Header);
|
|
||||||
Message = Localize(nameof(Message), Message);
|
|
||||||
|
|
||||||
_openText = Text;
|
|
||||||
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
|
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
|
||||||
_authorized = IsAuthorized();
|
_authorized = IsAuthorized();
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public List<Permission> PermissionList { get; set; }
|
public List<Permission> PermissionList { get; set; }
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(Permissions))
|
if (!string.IsNullOrEmpty(Permissions))
|
||||||
{
|
{
|
||||||
|
|
|
@ -134,6 +134,7 @@ namespace Oqtane.Modules
|
||||||
|
|
||||||
// url methods
|
// url methods
|
||||||
|
|
||||||
|
// navigate url
|
||||||
public string NavigateUrl()
|
public string NavigateUrl()
|
||||||
{
|
{
|
||||||
return NavigateUrl(PageState.Page.Path);
|
return NavigateUrl(PageState.Page.Path);
|
||||||
|
@ -149,24 +150,65 @@ namespace Oqtane.Modules
|
||||||
return NavigateUrl(PageState.Page.Path, refresh);
|
return NavigateUrl(PageState.Page.Path, refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string NavigateUrl(string path, string parameters)
|
public string NavigateUrl(string path, string querystring)
|
||||||
{
|
{
|
||||||
return Utilities.NavigateUrl(PageState.Alias.Path, path, parameters);
|
return Utilities.NavigateUrl(PageState.Alias.Path, path, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return NavigateUrl(path, Utilities.CreateQueryString(querystring));
|
||||||
}
|
}
|
||||||
|
|
||||||
public string NavigateUrl(string path, bool refresh)
|
public string NavigateUrl(string path, bool refresh)
|
||||||
{
|
{
|
||||||
return Utilities.NavigateUrl(PageState.Alias.Path, path, refresh ? "refresh" : "");
|
return NavigateUrl(path, refresh ? "refresh" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(int moduleId, string action)
|
||||||
|
{
|
||||||
|
return EditUrl(PageState.Page.Path, moduleId, action, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(int moduleId, string action, string querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(int moduleId, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleId, string action)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleId, action, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleId, string action, string querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleId, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleId, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleId, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
// edit url
|
||||||
public string EditUrl(string action)
|
public string EditUrl(string action)
|
||||||
{
|
{
|
||||||
return EditUrl(ModuleState.ModuleId, action);
|
return EditUrl(ModuleState.ModuleId, action);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(string action, string parameters)
|
public string EditUrl(string action, string querystring)
|
||||||
{
|
{
|
||||||
return EditUrl(ModuleState.ModuleId, action, parameters);
|
return EditUrl(ModuleState.ModuleId, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EditUrl(string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(ModuleState.ModuleId, action, querystring);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(int moduleId, string action)
|
public string EditUrl(int moduleId, string action)
|
||||||
|
@ -174,16 +216,27 @@ namespace Oqtane.Modules
|
||||||
return EditUrl(moduleId, action, "");
|
return EditUrl(moduleId, action, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(int moduleId, string action, string parameters)
|
public string EditUrl(int moduleId, string action, string querystring)
|
||||||
{
|
{
|
||||||
return EditUrl(PageState.Page.Path, moduleId, action, parameters);
|
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(string path, int moduleid, string action, string parameters)
|
public string EditUrl(int moduleId, string action, Dictionary<string, string> querystring)
|
||||||
{
|
{
|
||||||
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters);
|
return EditUrl(PageState.Page.Path, moduleId, action, querystring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string EditUrl(string path, int moduleid, string action, string querystring)
|
||||||
|
{
|
||||||
|
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EditUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleid, action, Utilities.CreateQueryString(querystring));
|
||||||
|
}
|
||||||
|
|
||||||
|
// file url
|
||||||
public string FileUrl(string folderpath, string filename)
|
public string FileUrl(string folderpath, string filename)
|
||||||
{
|
{
|
||||||
return FileUrl(folderpath, filename, false);
|
return FileUrl(folderpath, filename, false);
|
||||||
|
@ -203,6 +256,8 @@ namespace Oqtane.Modules
|
||||||
return Utilities.FileUrl(PageState.Alias, fileid, download);
|
return Utilities.FileUrl(PageState.Alias, fileid, download);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// image url
|
||||||
|
|
||||||
public string ImageUrl(int fileid, int width, int height)
|
public string ImageUrl(int fileid, int width, int height)
|
||||||
{
|
{
|
||||||
return ImageUrl(fileid, width, height, "");
|
return ImageUrl(fileid, width, height, "");
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</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.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -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>
|
|
@ -117,12 +117,6 @@
|
||||||
<resheader name="writer">
|
<resheader name="writer">
|
||||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
</resheader>
|
</resheader>
|
||||||
<data name="Error.User.AddCheckPass" xml:space="preserve">
|
|
||||||
<value>Error Adding User. Please Ensure Password Meets Complexity Requirements And Username And Email Is Not Already In Use.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Message.Password.NoMatch" xml:space="preserve">
|
|
||||||
<value>Passwords Entered Do Not Match</value>
|
|
||||||
</data>
|
|
||||||
<data name="Error.User.Add" xml:space="preserve">
|
<data name="Error.User.Add" xml:space="preserve">
|
||||||
<value>Error Adding User</value>
|
<value>Error Adding User</value>
|
||||||
</data>
|
</data>
|
||||||
|
@ -133,17 +127,11 @@
|
||||||
<value>Identity</value>
|
<value>Identity</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Message.Required.ProfileInfo" xml:space="preserve">
|
<data name="Message.Required.ProfileInfo" xml:space="preserve">
|
||||||
<value>You Must Provide A Username, Password, Email Address And All Required Profile Information</value>
|
<value>You Must Provide A Username, Email Address And All Required Profile Information</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Message.Username.Exists" xml:space="preserve">
|
<data name="Message.Username.Exists" xml:space="preserve">
|
||||||
<value>Username Already Exists</value>
|
<value>Username Already Exists</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Confirm.HelpText" xml:space="preserve">
|
|
||||||
<value>Please enter the password again to confirm it matches with the value above</value>
|
|
||||||
</data>
|
|
||||||
<data name="Confirm.Text" xml:space="preserve">
|
|
||||||
<value>Confirm Password:</value>
|
|
||||||
</data>
|
|
||||||
<data name="DisplayName.HelpText" xml:space="preserve">
|
<data name="DisplayName.HelpText" xml:space="preserve">
|
||||||
<value>The full name of the user</value>
|
<value>The full name of the user</value>
|
||||||
</data>
|
</data>
|
||||||
|
@ -156,21 +144,12 @@
|
||||||
<data name="Email.Text" xml:space="preserve">
|
<data name="Email.Text" xml:space="preserve">
|
||||||
<value>Email:</value>
|
<value>Email:</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Password.HelpText" xml:space="preserve">
|
|
||||||
<value>The user's password. Please choose a password which is sufficiently secure.</value>
|
|
||||||
</data>
|
|
||||||
<data name="Password.Text" xml:space="preserve">
|
|
||||||
<value>Password:</value>
|
|
||||||
</data>
|
|
||||||
<data name="Username.HelpText" xml:space="preserve">
|
<data name="Username.HelpText" xml:space="preserve">
|
||||||
<value>A unique username for a user. Note that this field can not be modified once it is saved.</value>
|
<value>A unique username for a user. Note that this field can not be modified once it is saved.</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Username.Text" xml:space="preserve">
|
<data name="Username.Text" xml:space="preserve">
|
||||||
<value>Username:</value>
|
<value>Username:</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="Password.Placeholder" xml:space="preserve">
|
|
||||||
<value>Password</value>
|
|
||||||
</data>
|
|
||||||
<data name="Notify.HelpText" xml:space="preserve">
|
<data name="Notify.HelpText" xml:space="preserve">
|
||||||
<value>Indicate if new users should receive an email notification</value>
|
<value>Indicate if new users should receive an email notification</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -41,9 +41,7 @@
|
||||||
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==",
|
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==",
|
||||||
CrossOrigin = "anonymous" },
|
CrossOrigin = "anonymous" },
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" },
|
||||||
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
|
new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body },
|
||||||
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
|
|
||||||
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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")));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -331,7 +331,7 @@
|
||||||
if (_pageId != "-")
|
if (_pageId != "-")
|
||||||
{
|
{
|
||||||
_modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
|
_modules = await ModuleService.GetModulesAsync(PageState.Page.SiteId);
|
||||||
_modules = _modules.Where(module => module.PageId == int.Parse(_pageId) &&
|
_modules = _modules.Where(module => module.PageId == int.Parse(_pageId) && module.IsDeleted == false &&
|
||||||
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
|
UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, module.PermissionList) &&
|
||||||
(_moduleType == "add" || module.ModuleDefinition.IsPortable))
|
(_moduleType == "add" || module.ModuleDefinition.IsPortable))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
|
@ -4,11 +4,8 @@
|
||||||
@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>
|
|
||||||
</Authorizing>
|
|
||||||
<Authorized>
|
|
||||||
@if (PageState.Runtime == Runtime.Hybrid)
|
@if (PageState.Runtime == Runtime.Hybrid)
|
||||||
{
|
{
|
||||||
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
|
<button type="button" class="btn btn-primary" @onclick="LogoutUser">@Localizer["Logout"]</button>
|
||||||
|
@ -21,14 +18,14 @@
|
||||||
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
|
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
</Authorized>
|
}
|
||||||
<NotAuthorized>
|
else
|
||||||
|
{
|
||||||
@if (ShowLogin)
|
@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
|
||||||
|
|
|
@ -9,3 +9,18 @@
|
||||||
</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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
@namespace Oqtane.Themes.Controls
|
@namespace Oqtane.Themes.Controls
|
||||||
@using System.Net
|
@using System.Net
|
||||||
@using Microsoft.AspNetCore.Http
|
|
||||||
@inherits ThemeControlBase
|
@inherits ThemeControlBase
|
||||||
|
@inject ISettingService SettingService
|
||||||
@inject IStringLocalizer<Search> Localizer
|
@inject IStringLocalizer<Search> Localizer
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
@if (_searchResultsPage != null)
|
@if (_searchResultsPage != null)
|
||||||
{
|
{
|
||||||
<span class="app-search @CssClass">
|
<span class="@_defaultCssClass @CssClass">
|
||||||
<form method="post" class="app-form-inline" @formname="@($"SearchForm")" @onsubmit="@PerformSearch" data-enhance>
|
<form method="post" class="app-form-inline" @formname="@($"SearchForm")" @onsubmit="@PerformSearch" data-enhance>
|
||||||
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
|
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
|
||||||
|
@if (AllowTextInput)
|
||||||
|
{
|
||||||
<input type="text" name="keywords" maxlength="50"
|
<input type="text" name="keywords" maxlength="50"
|
||||||
class="form-control d-inline-block pe-5 shadow-none"
|
class="form-control d-inline-block pe-5 shadow-none"
|
||||||
@bind="_keywords"
|
@bind="_keywords"
|
||||||
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">
|
||||||
<span class="oi oi-magnifying-glass align-middle"></span>
|
<span class="oi oi-magnifying-glass align-middle"></span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -22,9 +25,8 @@
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private string _defaultCssClass;
|
||||||
private Page _searchResultsPage;
|
private Page _searchResultsPage;
|
||||||
private string _keywords = "";
|
private string _keywords = "";
|
||||||
|
|
||||||
|
@ -32,21 +34,25 @@
|
||||||
public string CssClass { get; set; }
|
public string CssClass { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string SearchResultPagePath { get; set; } = "search";
|
public bool AllowTextInput { get; set; } = true; // setting to false will display only the search icon button - not the textbox
|
||||||
|
|
||||||
[CascadingParameter]
|
[Parameter]
|
||||||
HttpContext HttpContext { get; set; }
|
public string SearchResultPagePath { get; set; } = "search"; // setting to "" will disable search
|
||||||
|
|
||||||
[SupplyParameterFromForm(FormName = "SearchForm")]
|
[SupplyParameterFromForm(FormName = "SearchForm")]
|
||||||
public string KeyWords { get => ""; set => _keywords = value; }
|
public string KeyWords { get => ""; set => _keywords = value; }
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
if (bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "Search_Enabled", "True")))
|
||||||
|
{
|
||||||
|
_defaultCssClass = (AllowTextInput) ? "app-search" : "app-search-noinput";
|
||||||
if (!string.IsNullOrEmpty(SearchResultPagePath))
|
if (!string.IsNullOrEmpty(SearchResultPagePath))
|
||||||
{
|
{
|
||||||
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
|
_searchResultsPage = PageState.Pages.FirstOrDefault(i => i.Path == SearchResultPagePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void PerformSearch()
|
private void PerformSearch()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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>
|
|
||||||
<NotAuthorized>
|
|
||||||
@if (ShowRegister && PageState.Site.AllowRegistration)
|
@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 {
|
||||||
|
|
|
@ -21,9 +21,7 @@ namespace Oqtane.Themes.OqtaneTheme
|
||||||
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==",
|
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==",
|
||||||
CrossOrigin = "anonymous" },
|
CrossOrigin = "anonymous" },
|
||||||
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
|
new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" },
|
||||||
new Resource { ResourceType = ResourceType.Script, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js",
|
new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body }
|
||||||
Integrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==",
|
|
||||||
CrossOrigin = "anonymous", Location = ResourceLocation.Body },
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -93,6 +93,7 @@ namespace Oqtane.Themes
|
||||||
|
|
||||||
// url methods
|
// url methods
|
||||||
|
|
||||||
|
// navigate url
|
||||||
public string NavigateUrl()
|
public string NavigateUrl()
|
||||||
{
|
{
|
||||||
return NavigateUrl(PageState.Page.Path);
|
return NavigateUrl(PageState.Page.Path);
|
||||||
|
@ -108,31 +109,78 @@ namespace Oqtane.Themes
|
||||||
return NavigateUrl(PageState.Page.Path, refresh);
|
return NavigateUrl(PageState.Page.Path, refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, string querystring)
|
||||||
|
{
|
||||||
|
return Utilities.NavigateUrl(PageState.Alias.Path, path, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return NavigateUrl(path, Utilities.CreateQueryString(querystring));
|
||||||
|
}
|
||||||
|
|
||||||
public string NavigateUrl(string path, bool refresh)
|
public string NavigateUrl(string path, bool refresh)
|
||||||
{
|
{
|
||||||
return Utilities.NavigateUrl(PageState.Alias.Path, path, refresh ? "refresh" : "");
|
return NavigateUrl(path, refresh ? "refresh" : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string NavigateUrl(string path, string parameters)
|
public string NavigateUrl(int moduleid, string action)
|
||||||
{
|
{
|
||||||
return Utilities.NavigateUrl(PageState.Alias.Path, path, parameters);
|
return EditUrl(moduleid, action, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(int moduleid, string action, string querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(int moduleid, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(PageState.Page.Path, moduleid, action, Utilities.CreateQueryString(querystring));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleId, string action)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleId, action, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleid, string action, string querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleid, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string NavigateUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleid, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
// edit url
|
||||||
public string EditUrl(int moduleid, string action)
|
public string EditUrl(int moduleid, string action)
|
||||||
{
|
{
|
||||||
return EditUrl(moduleid, action, "");
|
return EditUrl(moduleid, action, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(int moduleid, string action, string parameters)
|
public string EditUrl(int moduleid, string action, string querystring)
|
||||||
{
|
{
|
||||||
return EditUrl(PageState.Page.Path, moduleid, action, parameters);
|
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string EditUrl(string path, int moduleid, string action, string parameters)
|
public string EditUrl(int moduleid, string action, Dictionary<string, string> querystring)
|
||||||
{
|
{
|
||||||
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, parameters);
|
return EditUrl(PageState.Page.Path, moduleid, action, querystring);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string EditUrl(string path, int moduleid, string action, string querystring)
|
||||||
|
{
|
||||||
|
return Utilities.EditUrl(PageState.Alias.Path, path, moduleid, action, querystring);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string EditUrl(string path, int moduleid, string action, Dictionary<string, string> querystring)
|
||||||
|
{
|
||||||
|
return EditUrl(path, moduleid, action, Utilities.CreateQueryString(querystring));
|
||||||
|
}
|
||||||
|
|
||||||
|
// file url
|
||||||
public string FileUrl(string folderpath, string filename)
|
public string FileUrl(string folderpath, string filename)
|
||||||
{
|
{
|
||||||
return FileUrl(folderpath, filename, false);
|
return FileUrl(folderpath, filename, false);
|
||||||
|
@ -152,6 +200,7 @@ namespace Oqtane.Themes
|
||||||
return Utilities.FileUrl(PageState.Alias, fileid, download);
|
return Utilities.FileUrl(PageState.Alias, fileid, download);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// image url
|
||||||
public string ImageUrl(int fileid, int width, int height)
|
public string ImageUrl(int fileid, int width, int height)
|
||||||
{
|
{
|
||||||
return ImageUrl(fileid, width, height, "");
|
return ImageUrl(fileid, width, height, "");
|
||||||
|
|
|
@ -61,7 +61,6 @@
|
||||||
{
|
{
|
||||||
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 = (HttpContext != null) ? true : false;
|
||||||
|
|
||||||
|
@ -80,6 +79,7 @@
|
||||||
{
|
{
|
||||||
_pageState = PageState;
|
_pageState = PageState;
|
||||||
SiteState.Alias = PageState.Alias;
|
SiteState.Alias = PageState.Alias;
|
||||||
|
SiteState.RemoteIPAddress = (PageState != null) ? PageState.RemoteIPAddress : "";
|
||||||
_installed = true;
|
_installed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,7 +157,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 +287,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 +366,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 +403,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 +436,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 +504,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
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Version>5.2.0</Version>
|
<Version>5.2.2</Version>
|
||||||
<Product>Oqtane</Product>
|
<Product>Oqtane</Product>
|
||||||
<Authors>Shaun Walker</Authors>
|
<Authors>Shaun Walker</Authors>
|
||||||
<Company>.NET Foundation</Company>
|
<Company>.NET Foundation</Company>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<Copyright>.NET Foundation</Copyright>
|
<Copyright>.NET Foundation</Copyright>
|
||||||
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
||||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</Version>
|
||||||
<Product>Oqtane</Product>
|
<Product>Oqtane</Product>
|
||||||
<Authors>Shaun Walker</Authors>
|
<Authors>Shaun Walker</Authors>
|
||||||
<Company>.NET Foundation</Company>
|
<Company>.NET Foundation</Company>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<Copyright>.NET Foundation</Copyright>
|
<Copyright>.NET Foundation</Copyright>
|
||||||
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
||||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</Version>
|
||||||
<Product>Oqtane</Product>
|
<Product>Oqtane</Product>
|
||||||
<Authors>Shaun Walker</Authors>
|
<Authors>Shaun Walker</Authors>
|
||||||
<Company>.NET Foundation</Company>
|
<Company>.NET Foundation</Company>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<Copyright>.NET Foundation</Copyright>
|
<Copyright>.NET Foundation</Copyright>
|
||||||
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
||||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</Version>
|
||||||
<Product>Oqtane</Product>
|
<Product>Oqtane</Product>
|
||||||
<Authors>Shaun Walker</Authors>
|
<Authors>Shaun Walker</Authors>
|
||||||
<Company>.NET Foundation</Company>
|
<Company>.NET Foundation</Company>
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
<Copyright>.NET Foundation</Copyright>
|
<Copyright>.NET Foundation</Copyright>
|
||||||
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
|
||||||
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
|
||||||
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
|
<!-- <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks> -->
|
||||||
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</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.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
|
<ApplicationIdGuid>0E29FC31-1B83-48ED-B6E0-9F3C67B775D4</ApplicationIdGuid>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<ApplicationDisplayVersion>5.2.1</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>5.2.2</ApplicationDisplayVersion>
|
||||||
<ApplicationVersion>1</ApplicationVersion>
|
<ApplicationVersion>1</ApplicationVersion>
|
||||||
|
|
||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -198,7 +198,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])) {
|
||||||
|
loadjs.ready(bundles[b], () => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loadjs(urls, bundles[b], {
|
loadjs(urls, bundles[b], {
|
||||||
|
@ -206,21 +208,28 @@ 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) {
|
||||||
|
if (scripts[s].integrity !== '') {
|
||||||
element.integrity = scripts[s].integrity;
|
element.integrity = scripts[s].integrity;
|
||||||
}
|
}
|
||||||
if (path === scripts[s].href && scripts[s].crossorigin !== '') {
|
if (scripts[s].crossorigin !== '') {
|
||||||
element.crossOrigin = scripts[s].crossorigin;
|
element.crossOrigin = scripts[s].crossorigin;
|
||||||
}
|
}
|
||||||
if (path === scripts[s].href && scripts[s].es6module === true) {
|
if (scripts[s].es6module === true) {
|
||||||
element.type = "module";
|
element.type = "module";
|
||||||
}
|
}
|
||||||
if (path === scripts[s].href && scripts[s].location === 'body') {
|
if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) {
|
||||||
|
for (var key in scripts[s].dataAttributes) {
|
||||||
|
element.setAttribute(key, scripts[s].dataAttributes[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (scripts[s].location === 'body') {
|
||||||
document.body.appendChild(element);
|
document.body.appendChild(element);
|
||||||
return false; // return false to bypass default DOM insertion mechanism
|
return false; // return false to bypass default DOM insertion mechanism
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.then(function () { resolve(true) })
|
.then(function () { resolve(true) })
|
||||||
.catch(function (pathsNotFound) { reject(false) });
|
.catch(function (pathsNotFound) { reject(false) });
|
||||||
|
@ -286,41 +295,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++) {
|
||||||
|
totalSize = totalSize + files[i].size;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxChunkSizeMB = 1;
|
||||||
|
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
|
||||||
|
var uploadedSize = 0;
|
||||||
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var FileChunk = [];
|
var fileChunk = [];
|
||||||
var file = files[i];
|
var file = files[i];
|
||||||
var MaxFileSizeMB = 1;
|
var fileStreamPos = 0;
|
||||||
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
|
var endPos = bufferChunkSize;
|
||||||
var FileStreamPos = 0;
|
|
||||||
var EndPos = BufferChunkSize;
|
|
||||||
var Size = file.size;
|
|
||||||
|
|
||||||
while (FileStreamPos < Size) {
|
while (fileStreamPos < file.size) {
|
||||||
FileChunk.push(file.slice(FileStreamPos, EndPos));
|
fileChunk.push(file.slice(fileStreamPos, endPos));
|
||||||
FileStreamPos = EndPos;
|
fileStreamPos = endPos;
|
||||||
EndPos = FileStreamPos + BufferChunkSize;
|
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 +345,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) {
|
||||||
|
if (files.length === 1) {
|
||||||
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
||||||
progressbar.value = 0;
|
}
|
||||||
|
else {
|
||||||
|
progressinfo.innerHTML = ' Error: ' + request.statusText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.send(data);
|
request.send(data);
|
||||||
|
|
|
@ -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.1</version>
|
<version>5.2.2</version>
|
||||||
<authors>Shaun Walker</authors>
|
<authors>Shaun Walker</authors>
|
||||||
<owners>.NET Foundation</owners>
|
<owners>.NET Foundation</owners>
|
||||||
<title>Oqtane Framework</title>
|
<title>Oqtane Framework</title>
|
||||||
|
@ -12,7 +12,8 @@
|
||||||
<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.1</releaseNotes>
|
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes>
|
||||||
|
<readme>readme.md</readme>
|
||||||
<icon>icon.png</icon>
|
<icon>icon.png</icon>
|
||||||
<tags>oqtane</tags>
|
<tags>oqtane</tags>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -20,5 +21,6 @@
|
||||||
<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\net8.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\net8.0" />
|
||||||
<file src="icon.png" target="" />
|
<file src="icon.png" target="" />
|
||||||
|
<file src="readme.md" target="" />
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
|
|
@ -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.1</version>
|
<version>5.2.2</version>
|
||||||
<authors>Shaun Walker</authors>
|
<authors>Shaun Walker</authors>
|
||||||
<owners>.NET Foundation</owners>
|
<owners>.NET Foundation</owners>
|
||||||
<title>Oqtane Framework</title>
|
<title>Oqtane Framework</title>
|
||||||
|
@ -11,12 +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.1/Oqtane.Framework.5.2.1.Upgrade.zip</projectUrl>
|
<projectUrl>https://github.com/oqtane/oqtane.framework/releases/download/v5.2.2/Oqtane.Framework.5.2.2.Upgrade.zip</projectUrl>
|
||||||
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1</releaseNotes>
|
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes>
|
||||||
|
<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>
|
||||||
|
|
|
@ -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.1</version>
|
<version>5.2.2</version>
|
||||||
<authors>Shaun Walker</authors>
|
<authors>Shaun Walker</authors>
|
||||||
<owners>.NET Foundation</owners>
|
<owners>.NET Foundation</owners>
|
||||||
<title>Oqtane Framework</title>
|
<title>Oqtane Framework</title>
|
||||||
|
@ -12,7 +12,8 @@
|
||||||
<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.1</releaseNotes>
|
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes>
|
||||||
|
<readme>readme.md</readme>
|
||||||
<icon>icon.png</icon>
|
<icon>icon.png</icon>
|
||||||
<tags>oqtane</tags>
|
<tags>oqtane</tags>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -20,5 +21,6 @@
|
||||||
<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\net8.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\net8.0" />
|
||||||
<file src="icon.png" target="" />
|
<file src="icon.png" target="" />
|
||||||
|
<file src="readme.md" target="" />
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
|
|
@ -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.1</version>
|
<version>5.2.2</version>
|
||||||
<authors>Shaun Walker</authors>
|
<authors>Shaun Walker</authors>
|
||||||
<owners>.NET Foundation</owners>
|
<owners>.NET Foundation</owners>
|
||||||
<title>Oqtane Framework</title>
|
<title>Oqtane Framework</title>
|
||||||
|
@ -12,7 +12,8 @@
|
||||||
<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.1</releaseNotes>
|
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes>
|
||||||
|
<readme>readme.md</readme>
|
||||||
<icon>icon.png</icon>
|
<icon>icon.png</icon>
|
||||||
<tags>oqtane</tags>
|
<tags>oqtane</tags>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -20,5 +21,6 @@
|
||||||
<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\net8.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\net8.0" />
|
||||||
<file src="icon.png" target="" />
|
<file src="icon.png" target="" />
|
||||||
|
<file src="readme.md" target="" />
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
|
|
@ -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.1</version>
|
<version>5.2.2</version>
|
||||||
<authors>Shaun Walker</authors>
|
<authors>Shaun Walker</authors>
|
||||||
<owners>.NET Foundation</owners>
|
<owners>.NET Foundation</owners>
|
||||||
<title>Oqtane Framework</title>
|
<title>Oqtane Framework</title>
|
||||||
|
@ -12,12 +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.1</releaseNotes>
|
<releaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</releaseNotes>
|
||||||
|
<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\net8.0" />
|
||||||
<file src="icon.png" target="" />
|
<file src="icon.png" target="" />
|
||||||
|
<file src="readme.md" target="" />
|
||||||
</files>
|
</files>
|
||||||
</package>
|
</package>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.1.Install.zip" -Force
|
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.2.Install.zip" -Force
|
||||||
|
|
Binary file not shown.
9
Oqtane.Package/readme.md
Normal file
9
Oqtane.Package/readme.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# Oqtane Framework
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Static Blazor, Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI).
|
||||||
|
|
||||||
|
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and was inspired by his earlier efforts with DotNetNuke... however Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
|
||||||
|
|
||||||
|
More information about Oqtane can be found at: [https://www.oqtane.org](https://www.oqtane.org)
|
|
@ -1 +1 @@
|
||||||
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.1.Upgrade.zip" -Force
|
Compress-Archive -Path "..\Oqtane.Server\bin\Release\net8.0\publish\*" -DestinationPath "Oqtane.Framework.5.2.2.Upgrade.zip" -Force
|
||||||
|
|
|
@ -534,9 +534,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)
|
||||||
|
@ -644,6 +644,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 +696,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -425,11 +425,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 +437,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 +492,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)
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -28,9 +27,10 @@ namespace Oqtane.Controllers
|
||||||
private readonly IUserPermissions _userPermissions;
|
private readonly IUserPermissions _userPermissions;
|
||||||
private readonly IJwtManager _jwtManager;
|
private readonly IJwtManager _jwtManager;
|
||||||
private readonly IFileRepository _files;
|
private readonly IFileRepository _files;
|
||||||
|
private readonly ISettingRepository _settings;
|
||||||
private readonly ILogManager _logger;
|
private readonly ILogManager _logger;
|
||||||
|
|
||||||
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ILogManager logger)
|
public UserController(IUserRepository users, ITenantManager tenantManager, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, IFileRepository files, ISettingRepository settings, ILogManager logger)
|
||||||
{
|
{
|
||||||
_users = users;
|
_users = users;
|
||||||
_tenantManager = tenantManager;
|
_tenantManager = tenantManager;
|
||||||
|
@ -39,6 +39,7 @@ namespace Oqtane.Controllers
|
||||||
_userPermissions = userPermissions;
|
_userPermissions = userPermissions;
|
||||||
_jwtManager = jwtManager;
|
_jwtManager = jwtManager;
|
||||||
_files = files;
|
_files = files;
|
||||||
|
_settings = settings;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,31 +111,58 @@ namespace Oqtane.Controllers
|
||||||
|
|
||||||
private User Filter(User user)
|
private User Filter(User user)
|
||||||
{
|
{
|
||||||
|
// clone object to avoid mutating cache
|
||||||
|
User filtered = null;
|
||||||
|
|
||||||
if (user != null)
|
if (user != null)
|
||||||
{
|
{
|
||||||
user.Password = "";
|
filtered = new User();
|
||||||
user.IsAuthenticated = false;
|
|
||||||
user.TwoFactorCode = "";
|
|
||||||
user.TwoFactorExpiry = null;
|
|
||||||
|
|
||||||
if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) && User.Identity.Name?.ToLower() != user.Username.ToLower())
|
// public properties
|
||||||
|
filtered.SiteId = user.SiteId;
|
||||||
|
filtered.UserId = user.UserId;
|
||||||
|
filtered.Username = user.Username;
|
||||||
|
filtered.DisplayName = user.DisplayName;
|
||||||
|
|
||||||
|
// restricted properties
|
||||||
|
filtered.Password = "";
|
||||||
|
filtered.TwoFactorCode = "";
|
||||||
|
filtered.SecurityStamp = "";
|
||||||
|
|
||||||
|
// 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)
|
||||||
{
|
{
|
||||||
user.Email = "";
|
filtered.Email = user.Email;
|
||||||
user.PhotoFileId = null;
|
filtered.PhotoFileId = user.PhotoFileId;
|
||||||
user.LastLoginOn = DateTime.MinValue;
|
filtered.LastLoginOn = user.LastLoginOn;
|
||||||
user.LastIPAddress = "";
|
filtered.LastIPAddress = user.LastIPAddress;
|
||||||
user.Roles = "";
|
filtered.TwoFactorRequired = false;
|
||||||
user.CreatedBy = "";
|
filtered.Roles = user.Roles;
|
||||||
user.CreatedOn = DateTime.MinValue;
|
filtered.CreatedBy = user.CreatedBy;
|
||||||
user.ModifiedBy = "";
|
filtered.CreatedOn = user.CreatedOn;
|
||||||
user.ModifiedOn = DateTime.MinValue;
|
filtered.ModifiedBy = user.ModifiedBy;
|
||||||
user.DeletedBy = "";
|
filtered.ModifiedOn = user.ModifiedOn;
|
||||||
user.DeletedOn = DateTime.MinValue;
|
filtered.DeletedBy = user.DeletedBy;
|
||||||
user.IsDeleted = false;
|
filtered.DeletedOn = user.DeletedOn;
|
||||||
user.TwoFactorRequired = false;
|
filtered.IsDeleted = user.IsDeleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if authenticated user is accessing their own user account
|
||||||
|
if (_userPermissions.GetUser(User).UserId == user.UserId)
|
||||||
|
{
|
||||||
|
// include all settings
|
||||||
|
filtered.Settings = user.Settings;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// include only public settings
|
||||||
|
filtered.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
|
||||||
|
.Where(item => !item.IsPrivate)
|
||||||
|
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return user;
|
|
||||||
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST api/<controller>
|
// POST api/<controller>
|
||||||
|
@ -147,11 +175,13 @@ namespace Oqtane.Controllers
|
||||||
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
|
if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
|
||||||
{
|
{
|
||||||
user.EmailConfirmed = true;
|
user.EmailConfirmed = true;
|
||||||
|
user.IsAuthenticated = true;
|
||||||
allowregistration = true;
|
allowregistration = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
user.EmailConfirmed = false;
|
user.EmailConfirmed = false;
|
||||||
|
user.IsAuthenticated = false;
|
||||||
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
|
allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,10 +262,26 @@ namespace Oqtane.Controllers
|
||||||
[HttpPost("logout")]
|
[HttpPost("logout")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public async Task Logout([FromBody] User user)
|
public async Task Logout([FromBody] User user)
|
||||||
|
{
|
||||||
|
if (_userPermissions.GetUser(User).UserId == user.UserId)
|
||||||
{
|
{
|
||||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||||
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Logout {Username}", (user != null) ? user.Username : "");
|
_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
|
||||||
[HttpPost("verify")]
|
[HttpPost("verify")]
|
||||||
|
@ -355,6 +401,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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -527,35 +527,76 @@ 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
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
|
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Role Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create claims identity
|
||||||
|
identityuser = await _identityUserManager.FindByEmailAsync(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,7 +645,7 @@ 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, providerName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else // claims invalid
|
else // claims invalid
|
||||||
|
|
|
@ -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,27 +114,34 @@ 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;
|
||||||
log.MessageTemplate = log.Message;
|
log.MessageTemplate = log.Message;
|
||||||
log = ProcessStructuredLog(log);
|
log = ProcessStructuredLog(log);
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (log.SiteId != -1)
|
||||||
{
|
{
|
||||||
_logs.AddLog(log);
|
_logs.AddLog(log);
|
||||||
SendNotification(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 = (index < message.Length - 1) ? message.IndexOf("{", index + 1) : -1;
|
||||||
index = message.IndexOf("{", index + 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
};
|
|
||||||
|
|
||||||
// set claims identity (note jwt already contains the roles - we are reloading to ensure most accurate permissions)
|
|
||||||
var _userRoles = context.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
|
|
||||||
var claimsidentity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList());
|
|
||||||
context.User = new ClaimsPrincipal(claimsidentity);
|
context.User = new ClaimsPrincipal(claimsidentity);
|
||||||
|
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For User {Username}", user.Username);
|
||||||
logger.Log(alias.SiteId, LogLevel.Information, "TokenValidation", Enums.LogFunction.Security, "Token Validated For UserId {UserId} And Username {Username}", user.UserId, user.Username);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.Log(alias.SiteId, LogLevel.Error, "TokenValidation", Enums.LogFunction.Security, "Token Validated But User {Username} Does Not Exist Or Is Deleted", user.Username);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -13,6 +13,7 @@ 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);
|
||||||
|
|
|
@ -64,8 +64,8 @@ namespace Oqtane.Managers
|
||||||
{
|
{
|
||||||
user.SiteId = siteid;
|
user.SiteId = siteid;
|
||||||
user.Roles = GetUserRoles(user.UserId, user.SiteId);
|
user.Roles = GetUserRoles(user.UserId, user.SiteId);
|
||||||
List<Setting> settings = _settings.GetSettings(EntityNames.User, user.UserId).ToList();
|
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
|
||||||
user.Settings = settings.Where(item => !item.IsPrivate || user.UserId == user.UserId)
|
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
|
||||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
|
@ -144,6 +144,9 @@ namespace Oqtane.Managers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
succeeded = true;
|
||||||
|
if (!user.IsAuthenticated)
|
||||||
{
|
{
|
||||||
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
|
var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
|
||||||
succeeded = result.Succeeded;
|
succeeded = result.Succeeded;
|
||||||
|
@ -153,6 +156,7 @@ namespace Oqtane.Managers
|
||||||
}
|
}
|
||||||
user.EmailConfirmed = succeeded;
|
user.EmailConfirmed = succeeded;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (succeeded)
|
if (succeeded)
|
||||||
{
|
{
|
||||||
|
@ -227,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
|
||||||
{
|
{
|
||||||
|
@ -237,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)
|
||||||
|
@ -259,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);
|
||||||
}
|
}
|
||||||
|
@ -367,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)
|
||||||
{
|
{
|
||||||
|
@ -414,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)
|
||||||
{
|
{
|
||||||
|
@ -469,6 +484,7 @@ namespace Oqtane.Managers
|
||||||
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
|
||||||
if (identityuser != null && !string.IsNullOrEmpty(token))
|
if (identityuser != null && !string.IsNullOrEmpty(token))
|
||||||
{
|
{
|
||||||
|
// note that ResetPasswordAsync checks password complexity rules
|
||||||
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
|
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
|
||||||
if (result.Succeeded)
|
if (result.Succeeded)
|
||||||
{
|
{
|
||||||
|
|
|
@ -127,6 +127,46 @@ namespace Oqtane.Migrations.EntityBuilders
|
||||||
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
return table.Column<DateTimeOffset>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddDateOnlyColumn(string name, bool nullable = false)
|
||||||
|
{
|
||||||
|
_migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddDateOnlyColumn(string name, bool nullable, DateOnly defaultValue)
|
||||||
|
{
|
||||||
|
_migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OperationBuilder<AddColumnOperation> AddDateOnlyColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||||
|
{
|
||||||
|
return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OperationBuilder<AddColumnOperation> AddDateOnlyColumn(ColumnsBuilder table, string name, bool nullable, DateOnly defaultValue)
|
||||||
|
{
|
||||||
|
return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTimeOnlyColumn(string name, bool nullable = false)
|
||||||
|
{
|
||||||
|
_migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddTimeOnlyColumn(string name, bool nullable, TimeOnly defaultValue)
|
||||||
|
{
|
||||||
|
_migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OperationBuilder<AddColumnOperation> AddTimeOnlyColumn(ColumnsBuilder table, string name, bool nullable = false)
|
||||||
|
{
|
||||||
|
return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected OperationBuilder<AddColumnOperation> AddTimeOnlyColumn(ColumnsBuilder table, string name, bool nullable, TimeOnly defaultValue)
|
||||||
|
{
|
||||||
|
return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
public void AddByteColumn(string name, bool nullable = false)
|
public void AddByteColumn(string name, bool nullable = false)
|
||||||
{
|
{
|
||||||
_migrationBuilder.AddColumn<byte>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
|
_migrationBuilder.AddColumn<byte>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</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.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace Oqtane.Providers
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return true;
|
return authState.User.SecurityStamp() == user.SecurityStamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,12 @@ namespace Oqtane.Repository
|
||||||
|
|
||||||
public void AddLog(Log log)
|
public void AddLog(Log log)
|
||||||
{
|
{
|
||||||
|
if (log.Url.Length > 2048) log.Url = log.Url.Substring(0, 2048);
|
||||||
|
if (log.Server.Length > 200) log.Server = log.Server.Substring(0, 200);
|
||||||
|
if (log.Category.Length > 200) log.Category = log.Category.Substring(0, 200);
|
||||||
|
if (log.Feature.Length > 200) log.Feature = log.Feature.Substring(0, 200);
|
||||||
|
if (log.Function.Length > 20) log.Function = log.Function.Substring(0, 20);
|
||||||
|
if (log.Level.Length > 20) log.Level = log.Level.Substring(0, 20);
|
||||||
using var db = _dbContextFactory.CreateDbContext();
|
using var db = _dbContextFactory.CreateDbContext();
|
||||||
db.Log.Add(log);
|
db.Log.Add(log);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Internal;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Oqtane.Infrastructure;
|
using Oqtane.Infrastructure;
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
|
|
|
@ -441,7 +441,7 @@ namespace Oqtane.Repository
|
||||||
pageModule.Module.PermissionList = new List<Permission>();
|
pageModule.Module.PermissionList = new List<Permission>();
|
||||||
foreach (var permission in pageTemplateModule.PermissionList)
|
foreach (var permission in pageTemplateModule.PermissionList)
|
||||||
{
|
{
|
||||||
pageModule.Module.PermissionList.Add(permission.Clone(permission));
|
pageModule.Module.PermissionList.Add(permission.Clone());
|
||||||
}
|
}
|
||||||
pageModule.Module.AllPages = false;
|
pageModule.Module.AllPages = false;
|
||||||
pageModule.Module.IsDeleted = false;
|
pageModule.Module.IsDeleted = false;
|
||||||
|
|
|
@ -5,15 +5,11 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Security;
|
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Oqtane.Infrastructure;
|
using Oqtane.Infrastructure;
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
using Oqtane.Shared;
|
using Oqtane.Shared;
|
||||||
using Oqtane.Themes;
|
using Oqtane.Themes;
|
||||||
using System.Reflection.Metadata;
|
|
||||||
using Oqtane.Migrations.Master;
|
|
||||||
using Oqtane.Modules;
|
|
||||||
|
|
||||||
namespace Oqtane.Repository
|
namespace Oqtane.Repository
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Oqtane.Infrastructure;
|
using Oqtane.Infrastructure;
|
||||||
|
@ -14,13 +15,15 @@ namespace Oqtane.Repository
|
||||||
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
|
private readonly IDbContextFactory<TenantDBContext> _dbContextFactory;
|
||||||
private readonly IRoleRepository _roles;
|
private readonly IRoleRepository _roles;
|
||||||
private readonly ITenantManager _tenantManager;
|
private readonly ITenantManager _tenantManager;
|
||||||
|
private readonly UserManager<IdentityUser> _identityUserManager;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, IMemoryCache cache)
|
public UserRoleRepository(IDbContextFactory<TenantDBContext> dbContextFactory, IRoleRepository roles, ITenantManager tenantManager, UserManager<IdentityUser> identityUserManager, IMemoryCache cache)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_dbContextFactory = dbContextFactory;
|
||||||
_roles = roles;
|
_roles = roles;
|
||||||
_tenantManager = tenantManager;
|
_tenantManager = tenantManager;
|
||||||
|
_identityUserManager = identityUserManager;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,9 +72,7 @@ namespace Oqtane.Repository
|
||||||
DeleteUserRoles(userRole.UserId);
|
DeleteUserRoles(userRole.UserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
var alias = _tenantManager.GetAlias();
|
UpdateSecurityStamp(userRole.UserId);
|
||||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
|
|
||||||
return userRole;
|
return userRole;
|
||||||
}
|
}
|
||||||
|
@ -82,9 +83,7 @@ namespace Oqtane.Repository
|
||||||
db.Entry(userRole).State = EntityState.Modified;
|
db.Entry(userRole).State = EntityState.Modified;
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
||||||
var alias = _tenantManager.GetAlias();
|
UpdateSecurityStamp(userRole.UserId);
|
||||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
|
|
||||||
return userRole;
|
return userRole;
|
||||||
}
|
}
|
||||||
|
@ -144,9 +143,7 @@ namespace Oqtane.Repository
|
||||||
db.UserRole.Remove(userRole);
|
db.UserRole.Remove(userRole);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
||||||
var alias = _tenantManager.GetAlias();
|
UpdateSecurityStamp(userRole.UserId);
|
||||||
_cache.Remove($"user:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
_cache.Remove($"userroles:{userRole.UserId}:{alias.SiteKey}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteUserRoles(int userId)
|
public void DeleteUserRoles(int userId)
|
||||||
|
@ -158,9 +155,30 @@ namespace Oqtane.Repository
|
||||||
}
|
}
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
||||||
|
UpdateSecurityStamp(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSecurityStamp(int userId)
|
||||||
|
{
|
||||||
|
// update user security stamp
|
||||||
|
using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var user = db.User.Find(userId);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
|
||||||
|
if (identityuser != null)
|
||||||
|
{
|
||||||
|
_identityUserManager.UpdateSecurityStampAsync(identityuser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refresh cache
|
||||||
var alias = _tenantManager.GetAlias();
|
var alias = _tenantManager.GetAlias();
|
||||||
|
if (alias != null)
|
||||||
|
{
|
||||||
_cache.Remove($"user:{userId}:{alias.SiteKey}");
|
_cache.Remove($"user:{userId}:{alias.SiteKey}");
|
||||||
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
|
_cache.Remove($"userroles:{userId}:{alias.SiteKey}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -13,14 +13,17 @@ namespace Oqtane.Security
|
||||||
public class ClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : IdentityUser
|
public class ClaimsPrincipalFactory<TUser> : UserClaimsPrincipalFactory<TUser> where TUser : IdentityUser
|
||||||
{
|
{
|
||||||
private readonly ITenantManager _tenants;
|
private readonly ITenantManager _tenants;
|
||||||
|
// cannot utilize IUserManager due to circular references - which is fine as this method is only called on login
|
||||||
private readonly IUserRepository _users;
|
private readonly IUserRepository _users;
|
||||||
private readonly IUserRoleRepository _userRoles;
|
private readonly IUserRoleRepository _userRoles;
|
||||||
|
private readonly UserManager<TUser> _userManager;
|
||||||
|
|
||||||
public ClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor)
|
public ClaimsPrincipalFactory(UserManager<TUser> userManager, IOptions<IdentityOptions> optionsAccessor, ITenantManager tenants, IUserRepository users, IUserRoleRepository userroles) : base(userManager, optionsAccessor)
|
||||||
{
|
{
|
||||||
_tenants = tenants;
|
_tenants = tenants;
|
||||||
_users = users;
|
_users = users;
|
||||||
_userRoles = userroles;
|
_userRoles = userroles;
|
||||||
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser identityuser)
|
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(TUser identityuser)
|
||||||
|
@ -33,6 +36,7 @@ namespace Oqtane.Security
|
||||||
Alias alias = _tenants.GetAlias();
|
Alias alias = _tenants.GetAlias();
|
||||||
if (alias != null)
|
if (alias != null)
|
||||||
{
|
{
|
||||||
|
user.SecurityStamp = await _userManager.GetSecurityStampAsync(identityuser);
|
||||||
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList();
|
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, alias.SiteId).ToList();
|
||||||
identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,11 @@ using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Oqtane.Infrastructure;
|
using Oqtane.Infrastructure;
|
||||||
using Oqtane.Repository;
|
|
||||||
using Oqtane.Models;
|
using Oqtane.Models;
|
||||||
using System.Collections.Generic;
|
|
||||||
using Oqtane.Extensions;
|
using Oqtane.Extensions;
|
||||||
using Oqtane.Shared;
|
using Oqtane.Shared;
|
||||||
using System.IO;
|
using Oqtane.Managers;
|
||||||
|
|
||||||
|
|
||||||
namespace Oqtane.Security
|
namespace Oqtane.Security
|
||||||
{
|
{
|
||||||
|
@ -24,49 +23,38 @@ namespace Oqtane.Security
|
||||||
// check if framework is installed
|
// check if framework is installed
|
||||||
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
|
if (config.IsInstalled() && !path.StartsWith("/_")) // ignore Blazor framework requests
|
||||||
{
|
{
|
||||||
// get current site
|
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
|
||||||
|
|
||||||
var alias = context.HttpContext.GetAlias();
|
var alias = context.HttpContext.GetAlias();
|
||||||
if (alias != null)
|
if (alias != null)
|
||||||
{
|
{
|
||||||
var claims = context.Principal.Claims;
|
var userManager = context.HttpContext.RequestServices.GetService(typeof(IUserManager)) as IUserManager;
|
||||||
|
var user = userManager.GetUser(context.Principal.UserId(), alias.SiteId); // cached
|
||||||
|
|
||||||
// check if principal has roles and matches current site
|
// check if user is valid, not deleted, has roles, and security stamp has not changed
|
||||||
if (!claims.Any(item => item.Type == ClaimTypes.Role) || !claims.Any(item => item.Type == "sitekey" && item.Value == alias.SiteKey))
|
if (user != null && !user.IsDeleted && !string.IsNullOrEmpty(user.Roles) && context.Principal.SecurityStamp() == user.SecurityStamp)
|
||||||
{
|
{
|
||||||
var userRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRepository)) as IUserRepository;
|
// validate sitekey in case user has changed sites in installation
|
||||||
var userRoleRepository = context.HttpContext.RequestServices.GetService(typeof(IUserRoleRepository)) as IUserRoleRepository;
|
if (context.Principal.SiteKey() != alias.SiteKey || !context.Principal.Roles().Any())
|
||||||
var _logger = context.HttpContext.RequestServices.GetService(typeof(ILogManager)) as ILogManager;
|
|
||||||
|
|
||||||
User user = userRepository.GetUser(context.Principal.Identity.Name);
|
|
||||||
if (user != null)
|
|
||||||
{
|
{
|
||||||
// replace principal with roles for current site
|
// refresh principal
|
||||||
List<UserRole> userroles = userRoleRepository.GetUserRoles(user.UserId, alias.SiteId).ToList();
|
var identity = UserSecurity.CreateClaimsIdentity(alias, user);
|
||||||
if (userroles.Any())
|
|
||||||
{
|
|
||||||
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
|
||||||
context.ReplacePrincipal(new ClaimsPrincipal(identity));
|
context.ReplacePrincipal(new ClaimsPrincipal(identity));
|
||||||
context.ShouldRenew = true;
|
context.ShouldRenew = true;
|
||||||
Log(_logger, alias, "Permissions Updated For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
Log(_logger, alias, "Permissions Refreshed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// user has no roles - remove principal
|
// remove principal (ie. log user out)
|
||||||
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||||
context.RejectPrincipal();
|
context.RejectPrincipal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// user does not exist - remove principal
|
// user is signed in but site cannot be determined
|
||||||
Log(_logger, alias, "Permissions Removed For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
Log(_logger, alias, "Alias Could Not Be Resolved For User {Username} Accessing {Url}", context.Principal.Identity.Name, path);
|
||||||
context.RejectPrincipal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// user is signed in but tenant cannot be determined
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +65,8 @@ namespace Oqtane.Security
|
||||||
{
|
{
|
||||||
if (!path.StartsWith("/api/")) // reduce log verbosity
|
if (!path.StartsWith("/api/")) // reduce log verbosity
|
||||||
{
|
{
|
||||||
logger.Log(alias.SiteId, LogLevel.Information, "LoginValidation", Enums.LogFunction.Security, message, username, path);
|
var siteId = (alias != null) ? alias.SiteId : -1;
|
||||||
|
logger.Log(siteId, LogLevel.Information, "UserValidation", Enums.LogFunction.Security, message, username, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ namespace Oqtane.Services
|
||||||
private readonly ILogManager _logger;
|
private readonly ILogManager _logger;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly IHttpContextAccessor _accessor;
|
private readonly IHttpContextAccessor _accessor;
|
||||||
|
private readonly string _private = "[PRIVATE]";
|
||||||
|
|
||||||
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
|
public ServerSiteService(ISiteRepository sites, IPageRepository pages, IThemeRepository themes, IPageModuleRepository pageModules, IModuleDefinitionRepository moduleDefinitions, ILanguageRepository languages, IUserPermissions userPermissions, ISettingRepository settings, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
|
||||||
{
|
{
|
||||||
|
@ -69,18 +70,26 @@ namespace Oqtane.Services
|
||||||
return GetSite(siteId);
|
return GetSite(siteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// clone object so that cache is not mutated
|
||||||
|
site = site.Clone();
|
||||||
|
|
||||||
|
// trim site settings based on user permissions
|
||||||
|
site.Settings = site.Settings
|
||||||
|
.Where(item => !item.Value.StartsWith(_private) || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
|
||||||
|
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||||
|
|
||||||
// trim pages based on user permissions
|
// trim pages based on user permissions
|
||||||
var pages = new List<Page>();
|
var pages = new List<Page>();
|
||||||
foreach (Page page in site.Pages)
|
foreach (Page page in site.Pages)
|
||||||
{
|
{
|
||||||
if (!page.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList)))
|
if (!page.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, page.PermissionList) && (Utilities.IsEffectiveAndNotExpired(page.EffectiveDate, page.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList)))
|
||||||
{
|
{
|
||||||
|
page.Settings = page.Settings
|
||||||
|
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
|
||||||
|
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||||
pages.Add(page);
|
pages.Add(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clone object so that cache is not mutated
|
|
||||||
site = site.Clone(site);
|
|
||||||
site.Pages = pages;
|
site.Pages = pages;
|
||||||
|
|
||||||
return Task.FromResult(site);
|
return Task.FromResult(site);
|
||||||
|
@ -94,10 +103,9 @@ namespace Oqtane.Services
|
||||||
{
|
{
|
||||||
// site settings
|
// site settings
|
||||||
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
|
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
|
||||||
.Where(item => !item.IsPrivate || _accessor.HttpContext.User.IsInRole(RoleNames.Admin))
|
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
|
||||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
|
||||||
|
|
||||||
// populate File Extensions
|
// populate file extensions
|
||||||
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
|
site.ImageFiles = site.Settings.ContainsKey("ImageFiles") && !string.IsNullOrEmpty(site.Settings["ImageFiles"])
|
||||||
? site.Settings["ImageFiles"] : Constants.ImageFiles;
|
? site.Settings["ImageFiles"] : Constants.ImageFiles;
|
||||||
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
|
site.UploadableFiles = site.Settings.ContainsKey("UploadableFiles") && !string.IsNullOrEmpty(site.Settings["UploadableFiles"])
|
||||||
|
@ -109,14 +117,13 @@ namespace Oqtane.Services
|
||||||
foreach (Page page in _pages.GetPages(site.SiteId))
|
foreach (Page page in _pages.GetPages(site.SiteId))
|
||||||
{
|
{
|
||||||
page.Settings = settings.Where(item => item.EntityId == page.PageId)
|
page.Settings = settings.Where(item => item.EntityId == page.PageId)
|
||||||
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, page.PermissionList))
|
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue);
|
||||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
|
||||||
site.Pages.Add(page);
|
site.Pages.Add(page);
|
||||||
}
|
}
|
||||||
site.Pages = GetPagesHierarchy(site.Pages);
|
site.Pages = GetPagesHierarchy(site.Pages);
|
||||||
|
|
||||||
// framework modules
|
// framework modules
|
||||||
var modules = GetModules(site.SiteId);
|
var modules = GetPageModules(site.SiteId);
|
||||||
site.Settings.Add(Constants.AdminDashboardModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule).ModuleId.ToString());
|
site.Settings.Add(Constants.AdminDashboardModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.AdminDashboardModule).ModuleId.ToString());
|
||||||
site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString());
|
site.Settings.Add(Constants.PageManagementModule, modules.FirstOrDefault(item => item.ModuleDefinitionName == Constants.PageManagementModule).ModuleId.ToString());
|
||||||
|
|
||||||
|
@ -249,31 +256,28 @@ namespace Oqtane.Services
|
||||||
public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
|
public Task<List<Module>> GetModulesAsync(int siteId, int pageId)
|
||||||
{
|
{
|
||||||
var alias = _tenantManager.GetAlias();
|
var alias = _tenantManager.GetAlias();
|
||||||
var sitemodules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
var modules = _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
||||||
{
|
|
||||||
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
|
||||||
return GetModules(siteId);
|
|
||||||
});
|
|
||||||
|
|
||||||
var modules = new List<Module>();
|
|
||||||
foreach (Module module in sitemodules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
|
|
||||||
{
|
|
||||||
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
|
||||||
{
|
|
||||||
modules.Add(module);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Task.FromResult(modules);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Module> GetModules(int siteId)
|
|
||||||
{
|
|
||||||
var alias = _tenantManager.GetAlias();
|
|
||||||
return _cache.GetOrCreate($"modules:{alias.SiteKey}", entry =>
|
|
||||||
{
|
{
|
||||||
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
entry.SlidingExpiration = TimeSpan.FromMinutes(30);
|
||||||
return GetPageModules(siteId);
|
return GetPageModules(siteId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// clone object so that cache is not mutated
|
||||||
|
modules = modules.ConvertAll(module => module.Clone());
|
||||||
|
|
||||||
|
// trim modules for current page based on user permissions
|
||||||
|
var pagemodules = new List<Module>();
|
||||||
|
foreach (Module module in modules.Where(item => (item.PageId == pageId || pageId == -1) && !item.IsDeleted && _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.View, item.PermissionList)))
|
||||||
|
{
|
||||||
|
if (Utilities.IsEffectiveAndNotExpired(module.EffectiveDate, module.ExpiryDate) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
||||||
|
{
|
||||||
|
module.Settings = module.Settings
|
||||||
|
.Where(item => !item.Value.StartsWith(_private) || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, module.PermissionList))
|
||||||
|
.ToDictionary(setting => setting.Key, setting => setting.Value.Replace(_private, ""));
|
||||||
|
pagemodules.Add(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task.FromResult(pagemodules);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Module> GetPageModules(int siteId)
|
private List<Module> GetPageModules(int siteId)
|
||||||
|
@ -311,8 +315,7 @@ namespace Oqtane.Services
|
||||||
ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)),
|
ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)),
|
||||||
|
|
||||||
Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId)
|
Settings = settings.Where(item => item.EntityId == pagemodule.ModuleId)
|
||||||
.Where(item => !item.IsPrivate || _userPermissions.IsAuthorized(_accessor.HttpContext.User, PermissionNames.Edit, pagemodule.Module.PermissionList))
|
.ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue)
|
||||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
modules.Add(module);
|
modules.Add(module);
|
||||||
|
|
14
Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs
vendored
Normal file
14
Oqtane.Server/wwwroot/Modules/Templates/External/Client/Startup/ClientStartup.cs
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Oqtane.Services;
|
||||||
|
using [Owner].Module.[Module].Services;
|
||||||
|
|
||||||
|
namespace [Owner].Module.[Module].Startup
|
||||||
|
{
|
||||||
|
public class ClientStartup : IClientStartup
|
||||||
|
{
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddScoped<I[Module]Service, [Module]Service>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Oqtane.Server/wwwroot/Modules/Templates/External/Server/Startup/ServerStartup.cs
vendored
Normal file
28
Oqtane.Server/wwwroot/Modules/Templates/External/Server/Startup/ServerStartup.cs
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Oqtane.Infrastructure;
|
||||||
|
using [Owner].Module.[Module].Repository;
|
||||||
|
using [Owner].Module.[Module].Services;
|
||||||
|
|
||||||
|
namespace [Owner].Module.[Module].Startup
|
||||||
|
{
|
||||||
|
public class ServerStartup : IServerStartup
|
||||||
|
{
|
||||||
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
|
{
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureMvc(IMvcBuilder mvcBuilder)
|
||||||
|
{
|
||||||
|
// not implemented
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddTransient<I[Module]Service, Server[Module]Service>();
|
||||||
|
services.AddDbContextFactory<[Module]Context>(opt => { }, ServiceLifetime.Transient);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -117,6 +117,10 @@
|
||||||
margin: .5rem;
|
margin: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-logo .navbar-brand {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
.main .top-row {
|
.main .top-row {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
|
@ -5,24 +5,30 @@
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<title>Upgrade Framework</title>
|
<title>Upgrade Framework</title>
|
||||||
<base href="/" />
|
<base href="/" />
|
||||||
<link rel="stylesheet" href="https://www.oqtane.net/css/app.css">
|
<link rel="stylesheet" type="text/css" href="[BOOTSTRAPCSSURL]" integrity="[BOOTSTRAPCSSINTEGRITY]" crossorigin="anonymous" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="https://www.oqtane.net/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body onload="forceWait()">
|
<body onload="refresh()">
|
||||||
<div>
|
<div>
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<h1 align="center">Please Wait... Upgrade In Progress...</h1>
|
<h1 align="center">Please Wait... Upgrade In Progress...</h1>
|
||||||
<p align="center">(this process can take a few minutes... please be patient)</p>
|
<p align="center">(this process can take a few minutes... please be patient)</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="w-50 mx-auto mt-5">
|
||||||
<div class="app-progress-indicator"></div>
|
<div class="progress" role="progressbar" aria-label="Basic example" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated [PROGRESSCLASS]" style="width: [PROGRESS]%"></div>
|
||||||
|
</div>
|
||||||
|
<div class="fs-6 fst-italic mt-1">
|
||||||
|
[STATUS]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function forceWait() {
|
function refresh() {
|
||||||
setInterval(function () {
|
setTimeout(function () {
|
||||||
window.location.href = "/";
|
window.location.href = "/?reload";
|
||||||
}, 120 * 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -235,6 +235,7 @@ app {
|
||||||
.app-form-inline {
|
.app-form-inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-search {
|
.app-search {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -249,9 +250,25 @@ app {
|
||||||
.app-search input + button .oi {
|
.app-search input + button .oi {
|
||||||
top: 0;
|
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 Editor */
|
||||||
.text-area-editor > textarea {
|
.text-area-editor > textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 250px;
|
min-height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-logo .navbar-brand {
|
||||||
|
padding: 5px 20px 5px 20px;
|
||||||
|
}
|
|
@ -198,7 +198,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])) {
|
||||||
|
loadjs.ready(bundles[b], () => {
|
||||||
resolve(true);
|
resolve(true);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
loadjs(urls, bundles[b], {
|
loadjs(urls, bundles[b], {
|
||||||
|
@ -293,41 +295,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++) {
|
||||||
|
totalSize = totalSize + files[i].size;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maxChunkSizeMB = 1;
|
||||||
|
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024);
|
||||||
|
var uploadedSize = 0;
|
||||||
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
for (var i = 0; i < files.length; i++) {
|
||||||
var FileChunk = [];
|
var fileChunk = [];
|
||||||
var file = files[i];
|
var file = files[i];
|
||||||
var MaxFileSizeMB = 1;
|
var fileStreamPos = 0;
|
||||||
var BufferChunkSize = MaxFileSizeMB * (1024 * 1024);
|
var endPos = bufferChunkSize;
|
||||||
var FileStreamPos = 0;
|
|
||||||
var EndPos = BufferChunkSize;
|
|
||||||
var Size = file.size;
|
|
||||||
|
|
||||||
while (FileStreamPos < Size) {
|
while (fileStreamPos < file.size) {
|
||||||
FileChunk.push(file.slice(FileStreamPos, EndPos));
|
fileChunk.push(file.slice(fileStreamPos, endPos));
|
||||||
FileStreamPos = EndPos;
|
fileStreamPos = endPos;
|
||||||
EndPos = FileStreamPos + BufferChunkSize;
|
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 !== "") {
|
||||||
|
@ -335,28 +345,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) {
|
||||||
|
if (files.length === 1) {
|
||||||
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
|
||||||
progressbar.value = 0;
|
}
|
||||||
|
else {
|
||||||
|
progressinfo.innerHTML = ' Error: ' + request.statusText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
request.send(data);
|
request.send(data);
|
||||||
|
|
|
@ -16,7 +16,7 @@ namespace Oqtane.Themes
|
||||||
string Thumbnail { get; }
|
string Thumbnail { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Identifies all panes in a theme ( delimited by "," or ";") - assumed to be a layout if no panes specified
|
/// Comma delimited list of all panes in a theme
|
||||||
/// </summary>
|
/// </summary>
|
||||||
string Panes { get; }
|
string Panes { get; }
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace Oqtane.Models
|
namespace Oqtane.Models
|
||||||
|
@ -40,5 +39,18 @@ namespace Oqtane.Models
|
||||||
/// Version of the satellite assembly
|
/// Version of the satellite assembly
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Version { get; set; }
|
public string Version { get; set; }
|
||||||
|
|
||||||
|
public Language Clone()
|
||||||
|
{
|
||||||
|
return new Language
|
||||||
|
{
|
||||||
|
LanguageId = LanguageId,
|
||||||
|
SiteId = SiteId,
|
||||||
|
Name = Name,
|
||||||
|
Code = Code,
|
||||||
|
IsDefault = IsDefault,
|
||||||
|
Version = Version
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ using Oqtane.Shared;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
@ -28,11 +29,18 @@ namespace Oqtane.Models
|
||||||
public string ModuleDefinitionName { get; set; }
|
public string ModuleDefinitionName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if this Module Instance should be shown on all pages of the current <see cref="Site"/>
|
/// Determines if this module should be shown on all pages of the current <see cref="Site"/>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool AllPages { get; set; }
|
public bool AllPages { get; set; }
|
||||||
|
|
||||||
#region IDeletable Properties (note that these are NotMapped and are only used for storing PageModule properties)
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to the <see cref="ModuleDefinition"/> used for this module.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public ModuleDefinition ModuleDefinition { get; set; }
|
||||||
|
|
||||||
|
#region IDeletable Properties
|
||||||
|
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string DeletedBy { get; set; }
|
public string DeletedBy { get; set; }
|
||||||
|
@ -43,14 +51,23 @@ namespace Oqtane.Models
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// list of permissions for this module
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public List<Permission> PermissionList { get; set; }
|
public List<Permission> PermissionList { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of settings for this module
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public Dictionary<string, string> Settings { get; set; }
|
public Dictionary<string, string> Settings { get; set; }
|
||||||
|
|
||||||
#region PageModule properties
|
#region PageModule properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the PageModule instance
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int PageModuleId { get; set; }
|
public int PageModuleId { get; set; }
|
||||||
|
|
||||||
|
@ -60,24 +77,39 @@ namespace Oqtane.Models
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int PageId { get; set; }
|
public int PageId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Title of the pagemodule instance
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The Pane this module is shown in.
|
/// The pane where this pagemodule instance will be injected on the page
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Pane { get; set; }
|
public string Pane { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The order of the pagemodule instance within the Pane
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int Order { get; set; }
|
public int Order { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The container for the pagemodule instance
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string ContainerType { get; set; }
|
public string ContainerType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Start of when this module is visible. See also <see cref="ExpiryDate"/>
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public DateTime? EffectiveDate { get; set; }
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End of when this module is visible. See also <see cref="EffectiveDate"/>
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public DateTime? ExpiryDate { get; set; }
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
@ -85,38 +117,67 @@ namespace Oqtane.Models
|
||||||
|
|
||||||
#region SiteRouter properties
|
#region SiteRouter properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the type name for the module component being rendered
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string ModuleType { get; set; }
|
public string ModuleType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the module instance in a pane
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int PaneModuleIndex { get; set; }
|
public int PaneModuleIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of modules in a pane
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public int PaneModuleCount { get; set; }
|
public int PaneModuleCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A unique id to help determine if a component should be rendered
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public Guid RenderId { get; set; }
|
public Guid RenderId { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ModuleDefinition
|
#region IModuleControl properties
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reference to the <see cref="ModuleDefinition"/> used for this module.
|
/// The minimum access level to view the component being rendered
|
||||||
/// TODO: todoc - unclear if this is always populated
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public ModuleDefinition ModuleDefinition { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IModuleControl properties
|
|
||||||
[NotMapped]
|
|
||||||
public SecurityAccessLevel SecurityAccessLevel { get; set; }
|
public SecurityAccessLevel SecurityAccessLevel { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional title for the component
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string ControlTitle { get; set; }
|
public string ControlTitle { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optional mapping of Url actions to a component
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Actions { get; set; }
|
public string Actions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionally indicate if a compoent should not be rendered with the default modal admin container
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool UseAdminContainer { get; set; }
|
public bool UseAdminContainer { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionally specify the render mode for the component (overrides the Site setting)
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string RenderMode { get; set; }
|
public string RenderMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Optionally specify id the component should be prerendered (overrides the Site setting)
|
||||||
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public bool? Prerender { get; set; }
|
public bool? Prerender { get; set; }
|
||||||
|
|
||||||
|
@ -140,5 +201,34 @@ namespace Oqtane.Models
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public Module Clone()
|
||||||
|
{
|
||||||
|
return new Module
|
||||||
|
{
|
||||||
|
ModuleId = ModuleId,
|
||||||
|
SiteId = SiteId,
|
||||||
|
ModuleDefinitionName = ModuleDefinitionName,
|
||||||
|
AllPages = AllPages,
|
||||||
|
PageModuleId = PageModuleId,
|
||||||
|
PageId = PageId,
|
||||||
|
Title = Title,
|
||||||
|
Pane = Pane,
|
||||||
|
Order = Order,
|
||||||
|
ContainerType = ContainerType,
|
||||||
|
EffectiveDate = EffectiveDate,
|
||||||
|
ExpiryDate = ExpiryDate,
|
||||||
|
CreatedBy = CreatedBy,
|
||||||
|
CreatedOn = CreatedOn,
|
||||||
|
ModifiedBy = ModifiedBy,
|
||||||
|
ModifiedOn = ModifiedOn,
|
||||||
|
DeletedBy = DeletedBy,
|
||||||
|
DeletedOn = DeletedOn,
|
||||||
|
IsDeleted = IsDeleted,
|
||||||
|
ModuleDefinition = ModuleDefinition,
|
||||||
|
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
|
||||||
|
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
@ -75,33 +76,68 @@ namespace Oqtane.Models
|
||||||
public string BodyContent { get; set; }
|
public string BodyContent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Icon file for this page.
|
/// Icon class name for this page
|
||||||
/// TODO: unclear what this is for, and what icon library is used. Probably FontAwesome?
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Icon { get; set; }
|
public string Icon { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if this page should be included in navigation menu
|
||||||
|
/// </summary>
|
||||||
public bool IsNavigation { get; set; }
|
public bool IsNavigation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates if this page should be clickable in navigation menu
|
||||||
|
/// </summary>
|
||||||
public bool IsClickable { get; set; }
|
public bool IsClickable { get; set; }
|
||||||
public int? UserId { get; set; }
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Start of when this assignment is valid. See also <see cref="ExpiryDate"/>
|
/// Indicates if page is personalizable ie. allows users to create custom versions of the page
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? EffectiveDate { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// End of when this assignment is valid. See also <see cref="EffectiveDate"/>
|
|
||||||
/// </summary>
|
|
||||||
public DateTime? ExpiryDate { get; set; }
|
|
||||||
public bool IsPersonalizable { get; set; }
|
public bool IsPersonalizable { get; set; }
|
||||||
|
|
||||||
#region IDeletable Properties
|
/// <summary>
|
||||||
|
/// Reference to the user <see cref="User"/> who owns the personalized page
|
||||||
public string DeletedBy { get; set; }
|
/// </summary>
|
||||||
public DateTime? DeletedOn { get; set; }
|
public int? UserId { get; set; }
|
||||||
public bool IsDeleted { get; set; }
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of Pane-names which this Page has.
|
/// Start of when this page is visible. See also <see cref="ExpiryDate"/>
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? EffectiveDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// End of when this page is visible. See also <see cref="EffectiveDate"/>
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ExpiryDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The hierarchical level of the page
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public int Level { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if there are sub-pages. True if this page has sub-pages.
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public bool HasChildren { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of permissions for this page
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public List<Permission> PermissionList { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of settings for this page
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public Dictionary<string, string> Settings { get; set; }
|
||||||
|
|
||||||
|
#region SiteRouter properties
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of Pane names for the Theme assigned to this page
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public List<string> Panes { get; set; }
|
public List<string> Panes { get; set; }
|
||||||
|
@ -112,20 +148,15 @@ namespace Oqtane.Models
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public List<Resource> Resources { get; set; }
|
public List<Resource> Resources { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
#endregion
|
||||||
public List<Permission> PermissionList { get; set; }
|
|
||||||
|
|
||||||
[NotMapped]
|
#region IDeletable Properties
|
||||||
public Dictionary<string, string> Settings { get; set; }
|
|
||||||
|
|
||||||
[NotMapped]
|
public string DeletedBy { get; set; }
|
||||||
public int Level { get; set; }
|
public DateTime? DeletedOn { get; set; }
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
#endregion
|
||||||
/// Determines if there are sub-pages. True if this page has sub-pages.
|
|
||||||
/// </summary>
|
|
||||||
[NotMapped]
|
|
||||||
public bool HasChildren { get; set; }
|
|
||||||
|
|
||||||
#region Deprecated Properties
|
#region Deprecated Properties
|
||||||
|
|
||||||
|
@ -152,5 +183,42 @@ namespace Oqtane.Models
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
public Page Clone()
|
||||||
|
{
|
||||||
|
return new Page
|
||||||
|
{
|
||||||
|
PageId = PageId,
|
||||||
|
SiteId = SiteId,
|
||||||
|
Path = Path,
|
||||||
|
ParentId = ParentId,
|
||||||
|
Name = Name,
|
||||||
|
Title = Title,
|
||||||
|
Order = Order,
|
||||||
|
Url = Url,
|
||||||
|
ThemeType = ThemeType,
|
||||||
|
DefaultContainerType = DefaultContainerType,
|
||||||
|
HeadContent = HeadContent,
|
||||||
|
BodyContent = BodyContent,
|
||||||
|
Icon = Icon,
|
||||||
|
IsNavigation = IsNavigation,
|
||||||
|
IsClickable = IsClickable,
|
||||||
|
UserId = UserId,
|
||||||
|
IsPersonalizable = IsPersonalizable,
|
||||||
|
EffectiveDate = EffectiveDate,
|
||||||
|
ExpiryDate = ExpiryDate,
|
||||||
|
Level = Level,
|
||||||
|
HasChildren = HasChildren,
|
||||||
|
CreatedBy = CreatedBy,
|
||||||
|
CreatedOn = CreatedOn,
|
||||||
|
ModifiedBy = ModifiedBy,
|
||||||
|
ModifiedOn = ModifiedOn,
|
||||||
|
DeletedBy = DeletedBy,
|
||||||
|
DeletedOn = DeletedOn,
|
||||||
|
IsDeleted = IsDeleted,
|
||||||
|
PermissionList = PermissionList.ConvertAll(permission => permission.Clone()),
|
||||||
|
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,17 +101,21 @@ namespace Oqtane.Models
|
||||||
IsAuthorized = isAuthorized;
|
IsAuthorized = isAuthorized;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Permission Clone(Permission permission)
|
public Permission Clone()
|
||||||
{
|
{
|
||||||
return new Permission
|
return new Permission
|
||||||
{
|
{
|
||||||
SiteId = permission.SiteId,
|
SiteId = SiteId,
|
||||||
EntityName = permission.EntityName,
|
EntityName = EntityName,
|
||||||
EntityId = permission.EntityId,
|
EntityId = EntityId,
|
||||||
PermissionName = permission.PermissionName,
|
PermissionName = PermissionName,
|
||||||
RoleName = permission.RoleName,
|
RoleName = RoleName,
|
||||||
UserId = permission.UserId,
|
UserId = UserId,
|
||||||
IsAuthorized = permission.IsAuthorized
|
IsAuthorized = IsAuthorized,
|
||||||
|
CreatedBy = CreatedBy,
|
||||||
|
CreatedOn = CreatedOn,
|
||||||
|
ModifiedBy = ModifiedBy,
|
||||||
|
ModifiedOn = ModifiedOn
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -187,47 +187,47 @@ namespace Oqtane.Models
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public List<Theme> Themes { get; set; }
|
public List<Theme> Themes { get; set; }
|
||||||
|
|
||||||
public Site Clone(Site site)
|
public Site Clone()
|
||||||
{
|
{
|
||||||
return new Site
|
return new Site
|
||||||
{
|
{
|
||||||
SiteId = site.SiteId,
|
SiteId = SiteId,
|
||||||
TenantId = site.TenantId,
|
TenantId = TenantId,
|
||||||
Name = site.Name,
|
Name = Name,
|
||||||
LogoFileId = site.LogoFileId,
|
LogoFileId = LogoFileId,
|
||||||
FaviconFileId = site.FaviconFileId,
|
FaviconFileId = FaviconFileId,
|
||||||
DefaultThemeType = site.DefaultThemeType,
|
DefaultThemeType = DefaultThemeType,
|
||||||
DefaultContainerType = site.DefaultContainerType,
|
DefaultContainerType = DefaultContainerType,
|
||||||
AdminContainerType = site.AdminContainerType,
|
AdminContainerType = AdminContainerType,
|
||||||
PwaIsEnabled = site.PwaIsEnabled,
|
PwaIsEnabled = PwaIsEnabled,
|
||||||
PwaAppIconFileId = site.PwaAppIconFileId,
|
PwaAppIconFileId = PwaAppIconFileId,
|
||||||
PwaSplashIconFileId = site.PwaSplashIconFileId,
|
PwaSplashIconFileId = PwaSplashIconFileId,
|
||||||
AllowRegistration = site.AllowRegistration,
|
AllowRegistration = AllowRegistration,
|
||||||
VisitorTracking = site.VisitorTracking,
|
VisitorTracking = VisitorTracking,
|
||||||
CaptureBrokenUrls = site.CaptureBrokenUrls,
|
CaptureBrokenUrls = CaptureBrokenUrls,
|
||||||
SiteGuid = site.SiteGuid,
|
SiteGuid = SiteGuid,
|
||||||
RenderMode = site.RenderMode,
|
RenderMode = RenderMode,
|
||||||
Runtime = site.Runtime,
|
Runtime = Runtime,
|
||||||
Prerender = site.Prerender,
|
Prerender = Prerender,
|
||||||
Hybrid = site.Hybrid,
|
Hybrid = Hybrid,
|
||||||
Version = site.Version,
|
Version = Version,
|
||||||
HomePageId = site.HomePageId,
|
HomePageId = HomePageId,
|
||||||
HeadContent = site.HeadContent,
|
HeadContent = HeadContent,
|
||||||
BodyContent = site.BodyContent,
|
BodyContent = BodyContent,
|
||||||
IsDeleted = site.IsDeleted,
|
IsDeleted = IsDeleted,
|
||||||
DeletedBy = site.DeletedBy,
|
DeletedBy = DeletedBy,
|
||||||
DeletedOn = site.DeletedOn,
|
DeletedOn = DeletedOn,
|
||||||
ImageFiles = site.ImageFiles,
|
ImageFiles = ImageFiles,
|
||||||
UploadableFiles = site.UploadableFiles,
|
UploadableFiles = UploadableFiles,
|
||||||
SiteTemplateType = site.SiteTemplateType,
|
SiteTemplateType = SiteTemplateType,
|
||||||
CreatedBy = site.CreatedBy,
|
CreatedBy = CreatedBy,
|
||||||
CreatedOn = site.CreatedOn,
|
CreatedOn = CreatedOn,
|
||||||
ModifiedBy = site.ModifiedBy,
|
ModifiedBy = ModifiedBy,
|
||||||
ModifiedOn = site.ModifiedOn,
|
ModifiedOn = ModifiedOn,
|
||||||
Settings = site.Settings.ToDictionary(),
|
Settings = Settings.ToDictionary(setting => setting.Key, setting => setting.Value),
|
||||||
Pages = site.Pages.ToList(),
|
Pages = Pages.ConvertAll(page => page.Clone()),
|
||||||
Languages = site.Languages.ToList(),
|
Languages = Languages.ConvertAll(language => language.Clone()),
|
||||||
Themes = site.Themes.ToList()
|
Themes = Themes
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,12 @@ namespace Oqtane.Models
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? TwoFactorExpiry { get; set; }
|
public DateTime? TwoFactorExpiry { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A token indicating if a user's security properties have been modified
|
||||||
|
/// </summary>
|
||||||
|
[NotMapped]
|
||||||
|
public string SecurityStamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reference to the <see cref="Site"/> this user belongs to.
|
/// Reference to the <see cref="Site"/> this user belongs to.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -66,8 +72,7 @@ namespace Oqtane.Models
|
||||||
public int SiteId { get; set; }
|
public int SiteId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Role names this user has.
|
/// Semi-colon delimited list of role names for the user
|
||||||
/// TODO: todoc - is this comma separated?
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[NotMapped]
|
[NotMapped]
|
||||||
public string Roles { get; set; }
|
public string Roles { get; set; }
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Configurations>Debug;Release</Configurations>
|
<Configurations>Debug;Release</Configurations>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</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.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
|
|
|
@ -100,7 +100,7 @@ namespace Oqtane.Security
|
||||||
{
|
{
|
||||||
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
|
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
|
||||||
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
|
identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.UserId.ToString()));
|
||||||
identity.AddClaim(new Claim("sitekey", alias.SiteKey));
|
identity.AddClaim(new Claim(Constants.SiteKeyClaimType, alias.SiteKey));
|
||||||
if (user.Roles.Contains(RoleNames.Host))
|
if (user.Roles.Contains(RoleNames.Host))
|
||||||
{
|
{
|
||||||
// host users are site admins by default
|
// host users are site admins by default
|
||||||
|
@ -115,6 +115,7 @@ namespace Oqtane.Security
|
||||||
identity.AddClaim(new Claim(ClaimTypes.Role, role));
|
identity.AddClaim(new Claim(ClaimTypes.Role, role));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
identity.AddClaim(new Claim(Constants.SecurityStampClaimType, user.SecurityStamp));
|
||||||
}
|
}
|
||||||
return identity;
|
return identity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ namespace Oqtane.Shared
|
||||||
{
|
{
|
||||||
public class Constants
|
public class Constants
|
||||||
{
|
{
|
||||||
public static readonly string Version = "5.2.1";
|
public static readonly string Version = "5.2.2";
|
||||||
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1";
|
public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2";
|
||||||
public const string PackageId = "Oqtane.Framework";
|
public const string PackageId = "Oqtane.Framework";
|
||||||
public const string ClientId = "Oqtane.Client";
|
public const string ClientId = "Oqtane.Client";
|
||||||
public const string UpdaterPackageId = "Oqtane.Updater";
|
public const string UpdaterPackageId = "Oqtane.Updater";
|
||||||
|
@ -67,6 +67,9 @@ namespace Oqtane.Shared
|
||||||
public static readonly string AntiForgeryTokenHeaderName = "X-XSRF-TOKEN-HEADER";
|
public static readonly string AntiForgeryTokenHeaderName = "X-XSRF-TOKEN-HEADER";
|
||||||
public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE";
|
public static readonly string AntiForgeryTokenCookieName = "X-XSRF-TOKEN-COOKIE";
|
||||||
|
|
||||||
|
public static readonly string SecurityStampClaimType = "AspNet.Identity.SecurityStamp";
|
||||||
|
public static readonly string SiteKeyClaimType = "Oqtane.Identity.SiteKey";
|
||||||
|
|
||||||
public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??";
|
public static readonly string DefaultVisitorFilter = "bot,crawler,slurp,spider,(none),??";
|
||||||
|
|
||||||
public static readonly string HttpContextAliasKey = "Alias";
|
public static readonly string HttpContextAliasKey = "Alias";
|
||||||
|
@ -83,6 +86,11 @@ namespace Oqtane.Shared
|
||||||
public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" };
|
public static readonly string[] InternalPagePaths = { "login", "register", "reset", "404" };
|
||||||
public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
|
public const string DefaultTextEditor = "Oqtane.Modules.Controls.QuillJSTextEditor, Oqtane.Client";
|
||||||
|
|
||||||
|
public const string BootstrapScriptUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js";
|
||||||
|
public const string BootstrapScriptIntegrity = "sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==";
|
||||||
|
public const string BootstrapStylesheetUrl = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css";
|
||||||
|
public const string BootstrapStylesheetIntegrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==";
|
||||||
|
|
||||||
// Obsolete constants
|
// Obsolete constants
|
||||||
|
|
||||||
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";
|
const string RoleObsoleteMessage = "Use the corresponding member from Oqtane.Shared.RoleNames";
|
||||||
|
|
|
@ -23,12 +23,13 @@ namespace Oqtane.Shared
|
||||||
|
|
||||||
public static (string UrlParameters, string Querystring, string Fragment) ParseParameters(string parameters)
|
public static (string UrlParameters, string Querystring, string Fragment) ParseParameters(string parameters)
|
||||||
{
|
{
|
||||||
// /urlparameters /urlparameters?Id=1 /urlparameters#5 /urlparameters?Id=1#5 /urlparameters?reload#5
|
// /urlparameters /urlparameters?id=1 /urlparameters#5 /urlparameters?id=1#5 /urlparameters?reload#5
|
||||||
// Id=1 Id=1#5 reload#5 reload
|
// ?id=1 ?id=1#5 ?reload#5 ?reload
|
||||||
|
// id=1 id=1#5 reload#5 reload
|
||||||
// #5
|
// #5
|
||||||
|
|
||||||
// create absolute url to convert to Uri
|
// create absolute url to convert to Uri
|
||||||
parameters = (!parameters.StartsWith("/") && !parameters.StartsWith("#") ? "?" : "") + parameters;
|
parameters = (!parameters.StartsWith("/") && !parameters.StartsWith("#") && !parameters.StartsWith("?") ? "?" : "") + parameters;
|
||||||
parameters = Constants.PackageRegistryUrl + parameters;
|
parameters = Constants.PackageRegistryUrl + parameters;
|
||||||
var uri = new Uri(parameters);
|
var uri = new Uri(parameters);
|
||||||
var querystring = uri.Query.Replace("?", "");
|
var querystring = uri.Query.Replace("?", "");
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.28822.285
|
VisualStudioVersion = 16.0.28822.285
|
||||||
|
@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Updater", "Oqtane.Updater\Oqtane.Updater.csproj", "{2E8C6889-37CF-4C8D-88B1-505547F25098}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Updater", "Oqtane.Updater\Oqtane.Updater.csproj", "{2E8C6889-37CF-4C8D-88B1-505547F25098}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Shared", "Oqtane.Shared\Oqtane.Shared.csproj", "{E2512C17-291F-460A-A6D1-741C301DA184}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
@ -22,6 +24,10 @@ Global
|
||||||
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2E8C6889-37CF-4C8D-88B1-505547F25098}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{E2512C17-291F-460A-A6D1-741C301DA184}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{E2512C17-291F-460A-A6D1-741C301DA184}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<Version>5.2.1</Version>
|
<Version>5.2.2</Version>
|
||||||
<Product>Oqtane</Product>
|
<Product>Oqtane</Product>
|
||||||
<Authors>Shaun Walker</Authors>
|
<Authors>Shaun Walker</Authors>
|
||||||
<Company>.NET Foundation</Company>
|
<Company>.NET Foundation</Company>
|
||||||
|
@ -11,11 +11,15 @@
|
||||||
<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.1</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.2</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>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Oqtane.Shared\Oqtane.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Oqtane.Shared;
|
||||||
|
|
||||||
namespace Oqtane.Updater
|
namespace Oqtane.Updater
|
||||||
{
|
{
|
||||||
|
@ -31,10 +32,6 @@ namespace Oqtane.Updater
|
||||||
|
|
||||||
if (Directory.Exists(deployfolder))
|
if (Directory.Exists(deployfolder))
|
||||||
{
|
{
|
||||||
string log = "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine;
|
|
||||||
log += "ContentRootPath: " + contentrootfolder + Environment.NewLine;
|
|
||||||
log += "WebRootPath: " + webrootfolder + Environment.NewLine;
|
|
||||||
|
|
||||||
string packagename = "";
|
string packagename = "";
|
||||||
string[] packages = Directory.GetFiles(deployfolder, "Oqtane.Framework.*.Upgrade.zip");
|
string[] packages = Directory.GetFiles(deployfolder, "Oqtane.Framework.*.Upgrade.zip");
|
||||||
if (packages.Length > 0)
|
if (packages.Length > 0)
|
||||||
|
@ -42,15 +39,27 @@ namespace Oqtane.Updater
|
||||||
packagename = packages[packages.Length - 1]; // use highest version
|
packagename = packages[packages.Length - 1]; // use highest version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create upgrade log file
|
||||||
|
var logFilePath = Path.Combine(deployfolder, $"{Path.GetFileNameWithoutExtension(packagename)}.log");
|
||||||
|
if (File.Exists(logFilePath))
|
||||||
|
{
|
||||||
|
File.Delete(logFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteLog(logFilePath, "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine);
|
||||||
|
WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine);
|
||||||
|
WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine);
|
||||||
if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak")))
|
if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak")))
|
||||||
{
|
{
|
||||||
log += "Located Upgrade Package: " + packagename + Environment.NewLine;
|
WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine);
|
||||||
|
|
||||||
log += "Stopping Application Using: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine;
|
WriteLog(logFilePath, "Stopping Application Using: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine);
|
||||||
File.Copy(Path.Combine(webrootfolder, "app_offline.bak"), Path.Combine(contentrootfolder, "app_offline.htm"), true);
|
var offlineTemplate = File.ReadAllText(Path.Combine(webrootfolder, "app_offline.bak"));
|
||||||
|
var offlineFilePath = Path.Combine(contentrootfolder, "app_offline.htm");
|
||||||
|
|
||||||
// get list of files in package with local paths
|
// get list of files in package with local paths
|
||||||
log += "Retrieving List Of Files From Upgrade Package..." + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 5, "Retrieving List Of Files From Upgrade Package");
|
||||||
|
WriteLog(logFilePath, "Retrieving List Of Files From Upgrade Package..." + Environment.NewLine);
|
||||||
List<string> files = new List<string>();
|
List<string> files = new List<string>();
|
||||||
using (ZipArchive archive = ZipFile.OpenRead(packagename))
|
using (ZipArchive archive = ZipFile.OpenRead(packagename))
|
||||||
{
|
{
|
||||||
|
@ -59,15 +68,18 @@ namespace Oqtane.Updater
|
||||||
if (!string.IsNullOrEmpty(entry.Name))
|
if (!string.IsNullOrEmpty(entry.Name))
|
||||||
{
|
{
|
||||||
files.Add(Path.Combine(contentrootfolder, entry.FullName));
|
files.Add(Path.Combine(contentrootfolder, entry.FullName));
|
||||||
|
WriteLog(logFilePath, "Check File: " + entry.FullName + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
// ensure files are not locked
|
// ensure files are not locked
|
||||||
if (CanAccessFiles(files))
|
if (CanAccessFiles(files))
|
||||||
{
|
{
|
||||||
log += "Preparing Backup Folder: " + backupfolder + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder");
|
||||||
bool success = true;
|
WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// clear out backup folder
|
// clear out backup folder
|
||||||
|
@ -79,14 +91,16 @@ namespace Oqtane.Updater
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log += "Error Creating Backup Folder: " + ex.Message + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Creating Backup Folder", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Error Creating Backup Folder: " + ex.Message + Environment.NewLine);
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// backup files
|
// backup files
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
log += "Backing Up Files..." + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 15, "Backing Up Files");
|
||||||
|
WriteLog(logFilePath, "Backing Up Files..." + Environment.NewLine);
|
||||||
foreach (string file in files)
|
foreach (string file in files)
|
||||||
{
|
{
|
||||||
string filename = Path.Combine(backupfolder, file.Replace(contentrootfolder + Path.DirectorySeparatorChar, ""));
|
string filename = Path.Combine(backupfolder, file.Replace(contentrootfolder + Path.DirectorySeparatorChar, ""));
|
||||||
|
@ -99,12 +113,15 @@ namespace Oqtane.Updater
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
||||||
}
|
}
|
||||||
File.Copy(file, filename);
|
File.Copy(file, filename);
|
||||||
|
WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log += "Error Backing Up Files: " + ex.Message + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Backing Up Files", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Error Backing Up Files: " + ex.Message + Environment.NewLine);
|
||||||
success = false;
|
success = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,7 +129,8 @@ namespace Oqtane.Updater
|
||||||
// extract files
|
// extract files
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
log += "Extracting Files From Upgrade Package..." + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package");
|
||||||
|
WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (ZipArchive archive = ZipFile.OpenRead(packagename))
|
using (ZipArchive archive = ZipFile.OpenRead(packagename))
|
||||||
|
@ -127,6 +145,7 @@ namespace Oqtane.Updater
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
Directory.CreateDirectory(Path.GetDirectoryName(filename));
|
||||||
}
|
}
|
||||||
entry.ExtractToFile(filename, true);
|
entry.ExtractToFile(filename, true);
|
||||||
|
WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -134,12 +153,14 @@ namespace Oqtane.Updater
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
success = false;
|
success = false;
|
||||||
log += "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
log += "Removing Backup Folder..." + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 90, "Removing Backup Folder");
|
||||||
|
WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// clean up backup
|
// clean up backup
|
||||||
|
@ -149,12 +170,14 @@ namespace Oqtane.Updater
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log += "Error Removing Backup Folder: " + ex.Message + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-warning");
|
||||||
|
WriteLog(logFilePath, "Error Removing Backup Folder: " + ex.Message + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
log += "Restoring Files From Backup Folder..." + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Upgrade Failed, Restoring Files From Backup Folder", "bg-warning");
|
||||||
|
WriteLog(logFilePath, "Restoring Files From Backup Folder..." + Environment.NewLine);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// restore on failure
|
// restore on failure
|
||||||
|
@ -165,6 +188,7 @@ namespace Oqtane.Updater
|
||||||
if (File.Exists(filename))
|
if (File.Exists(filename))
|
||||||
{
|
{
|
||||||
File.Copy(filename, file);
|
File.Copy(filename, file);
|
||||||
|
WriteLog(logFilePath, "Restore File: " + filename + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// clean up backup
|
// clean up backup
|
||||||
|
@ -172,41 +196,38 @@ namespace Oqtane.Updater
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
log += "Error Restoring Files From Backup Folder: " + ex.Message + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Restoring Files From Backup Folder", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Error Restoring Files From Backup Folder: " + ex.Message + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
log += "Upgrade Failed: Could Not Backup Files" + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
log += "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine;
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Some Files Are Locked By The Hosting Environment", "bg-danger");
|
||||||
|
WriteLog(logFilePath, "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger");
|
||||||
|
Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process.
|
||||||
// bring the app back online
|
// bring the app back online
|
||||||
if (File.Exists(Path.Combine(contentrootfolder, "app_offline.htm")))
|
if (File.Exists(Path.Combine(contentrootfolder, "app_offline.htm")))
|
||||||
{
|
{
|
||||||
log += "Restarting Application By Removing: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine;
|
WriteLog(logFilePath, "Restarting Application By Removing: " + Path.Combine(contentrootfolder, "app_offline.htm") + Environment.NewLine);
|
||||||
File.Delete(Path.Combine(contentrootfolder, "app_offline.htm"));
|
File.Delete(Path.Combine(contentrootfolder, "app_offline.htm"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
log += "Framework Upgrade Package Not Found Or " + Path.Combine(webrootfolder, "app_offline.bak") + " Does Not Exist" + Environment.NewLine;
|
WriteLog(logFilePath, "Framework Upgrade Package Not Found Or " + Path.Combine(webrootfolder, "app_offline.bak") + " Does Not Exist" + Environment.NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
log += "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine;
|
WriteLog(logFilePath, "Upgrade Process Ended: " + DateTime.UtcNow.ToString() + Environment.NewLine);
|
||||||
|
|
||||||
// create upgrade log file
|
|
||||||
string logfile = Path.Combine(deployfolder, Path.GetFileNameWithoutExtension(packagename) + ".log");
|
|
||||||
if (File.Exists(logfile))
|
|
||||||
{
|
|
||||||
File.Delete(logfile);
|
|
||||||
}
|
|
||||||
File.WriteAllText(logfile, log);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -269,5 +290,21 @@ namespace Oqtane.Updater
|
||||||
}
|
}
|
||||||
return canAccess;
|
return canAccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void UpdateOfflineContent(string filePath, string contentTemplate, int progress, string status, string progressClass = "")
|
||||||
|
{
|
||||||
|
var content = contentTemplate
|
||||||
|
.Replace("[BOOTSTRAPCSSURL]", Constants.BootstrapStylesheetUrl)
|
||||||
|
.Replace("[BOOTSTRAPCSSINTEGRITY]", Constants.BootstrapStylesheetIntegrity)
|
||||||
|
.Replace("[PROGRESS]", progress.ToString())
|
||||||
|
.Replace("[PROGRESSCLASS]", progressClass)
|
||||||
|
.Replace("[STATUS]", status);
|
||||||
|
File.WriteAllText(filePath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteLog(string logFilePath, string logContent)
|
||||||
|
{
|
||||||
|
File.AppendAllText(logFilePath, $"[{DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff")}] {logContent}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
44
README.md
44
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Latest Release
|
# Latest Release
|
||||||
|
|
||||||
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) was released on July 25, 2024 and is a major release including 109 pull requests by 8 different contributors, pushing the total number of project commits all-time to over 5600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
|
[5.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1) was released on August 22, 2024 and is a maintenance release including 41 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 5700. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
|
||||||
|
|
||||||
[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
|
[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
|
||||||
|
|
||||||
|
@ -14,26 +14,46 @@ Oqtane is being developed based on some fundamental principles which are outline
|
||||||
|
|
||||||
Please note that this project is owned by the .NET Foundation and is governed by the **[.NET Foundation Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct)**
|
Please note that this project is owned by the .NET Foundation and is governed by the **[.NET Foundation Contributor Covenant Code of Conduct](https://dotnetfoundation.org/code-of-conduct)**
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started (Version 5.x)
|
||||||
|
|
||||||
**Using Version 5:**
|
**Installing using source code from the Dev/Master branch:**
|
||||||
|
|
||||||
- Install **[.NET 8.0.7 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
|
- Install **[.NET 8.0.8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)**.
|
||||||
|
|
||||||
- Install the latest edition (v17.9 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
|
- Install the latest edition (v17.9 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**.
|
||||||
|
|
||||||
- Clone the Oqtane dev branch source code to your local system.
|
- Clone (or download) the Oqtane Master or Dev branch source code to your local system.
|
||||||
|
|
||||||
- Open the **Oqtane.sln** solution file.
|
- Open the **Oqtane.sln** solution file.
|
||||||
|
|
||||||
- **Important:** Rebuild the entire solution before running it.
|
- **Important:** Rebuild the entire solution before running it (ie. Build / Rebuild Solution).
|
||||||
|
|
||||||
- Make sure you specify Oqtane.Server as the Startup Project
|
- Make sure you specify Oqtane.Server as the Startup Project.
|
||||||
|
|
||||||
- Run the application.
|
- Run the application... an Installation Wizard screen will be displayed which will allow you to configure your preferred database and create a host user account.
|
||||||
|
|
||||||
|
**Developing a custom module:**
|
||||||
|
|
||||||
|
- follow the instructions for installing using source code outlined above
|
||||||
|
|
||||||
|
- login as the host user
|
||||||
|
|
||||||
|
- navigate to Control Panel (gear icon at top-right of page), Admin Dashboard, Module Management
|
||||||
|
|
||||||
|
- select Create Module
|
||||||
|
|
||||||
|
- enter information corresponding to the module you wish to create and then select the Create button
|
||||||
|
|
||||||
|
- make note of the Location where the code was generated and open the solution file in Visual Studio
|
||||||
|
|
||||||
|
- Build / Rebuild Solution, ensure the Oqtane.Server is set as the Startup Project, and hit F5 to run the solution
|
||||||
|
|
||||||
**Installing an official release:**
|
**Installing an official release:**
|
||||||
|
|
||||||
|
- all official releases of Oqtane are distributed on [GitHub](https://github.com/oqtane/oqtane.framework/releases). Releases include an Install.zip package for new installations and an Upgrade.zip for existing installations.
|
||||||
|
|
||||||
|
- A detailed set of instructions for installing Oqtane on Azure is located here: [Installing Oqtane on Azure](https://blazorhelpwebsite.com/ViewBlogPost/1)
|
||||||
|
|
||||||
- A detailed set of instructions for installing Oqtane on IIS is located here: [Installing Oqtane on IIS](https://www.oqtane.org/Resources/Blog/PostId/542/installing-oqtane-on-iis)
|
- A detailed set of instructions for installing Oqtane on IIS is located here: [Installing Oqtane on IIS](https://www.oqtane.org/Resources/Blog/PostId/542/installing-oqtane-on-iis)
|
||||||
- Instructions for upgrading Oqtane are located here: [Upgrading Oqtane](https://www.oqtane.org/Resources/Blog/PostId/543/upgrading-oqtane)
|
- Instructions for upgrading Oqtane are located here: [Upgrading Oqtane](https://www.oqtane.org/Resources/Blog/PostId/543/upgrading-oqtane)
|
||||||
|
|
||||||
|
@ -63,6 +83,10 @@ Backlog (TBD)
|
||||||
- [ ] Folder Providers
|
- [ ] Folder Providers
|
||||||
- [ ] Generative AI Integration
|
- [ ] Generative AI Integration
|
||||||
|
|
||||||
|
[5.2.1](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.1) (Aug 22, 2024)
|
||||||
|
- [x] Stabilization improvements
|
||||||
|
- [x] Unzip support in File Management
|
||||||
|
|
||||||
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) (Jul 25, 2024)
|
[5.2.0](https://github.com/oqtane/oqtane.framework/releases/tag/v5.2.0) (Jul 25, 2024)
|
||||||
- [x] Site Content Search
|
- [x] Site Content Search
|
||||||
- [x] RichTextEditor extensibility
|
- [x] RichTextEditor extensibility
|
||||||
|
@ -90,7 +114,7 @@ Backlog (TBD)
|
||||||
➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html)
|
➡️ Full list and older versions can be found in the [docs roadmap](https://docs.oqtane.org/guides/roadmap/index.html)
|
||||||
|
|
||||||
# Background
|
# Background
|
||||||
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Initially created as a proof of concept, Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
|
Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalker/) and is inspired by the DotNetNuke web application framework. Oqtane is a native Blazor application written from the ground up using modern .NET Core technology and a Single Page Application (SPA) architecture. It is a modular application framework offering a fully dynamic page compositing model, multi-site support, designer friendly themes, and extensibility via third party modules.
|
||||||
|
|
||||||
# Reference Implementations
|
# Reference Implementations
|
||||||
|
|
||||||
|
@ -106,7 +130,7 @@ The following diagram visualizes the client and server components in the Oqtane
|
||||||
|
|
||||||
# Databases
|
# Databases
|
||||||
|
|
||||||
As of version 2.1, Oqtane supports multiple relational database providers.
|
As of version 2.1 (June 2021) Oqtane supports multiple relational database providers - SQL Server, SQLite, MySQL, PostgreSQL
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user