Merge pull request #5092 from oqtane/dev

6.1.0 Release
This commit is contained in:
Shaun Walker 2025-02-11 11:49:06 -05:00 committed by GitHub
commit b2f65903ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 1989 additions and 1105 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2018-2024 .NET Foundation Copyright (c) 2018-2025 .NET Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -71,14 +71,14 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="Provide a username for the primary user account" ResourceKey="Username">Username:</Label> <Label Class="col-sm-3" For="username" HelpText="Provide a username for the primary user account" ResourceKey="Username">Username:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="username" type="text" class="form-control" @bind="@_hostUsername" /> <input id="username" type="text" class="form-control" maxlength="256" @bind="@_hostUsername" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label> <Label Class="col-sm-3" For="password" HelpText="Provide a password for the primary user account" ResourceKey="Password">Password:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="password" type="@_passwordType" class="form-control" @bind="@_hostPassword" autocomplete="new-password" /> <input id="password" type="@_passwordType" class="form-control" maxlength="256" @bind="@_hostPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button> <button type="button" class="btn btn-secondary" @onclick="@TogglePassword" tabindex="-1">@_togglePassword</button>
</div> </div>
</div> </div>
@ -87,7 +87,7 @@
<Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label> <Label Class="col-sm-3" For="confirm" HelpText="Please confirm the password entered above by entering it again" ResourceKey="Confirm">Confirm:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<div class="input-group"> <div class="input-group">
<input id="confirm" type="@_confirmPasswordType" class="form-control" @bind="@_confirmPassword" autocomplete="new-password" /> <input id="confirm" type="@_confirmPasswordType" class="form-control" maxlength="256" @bind="@_confirmPassword" autocomplete="new-password" />
<button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword" tabindex="-1">@_toggleConfirmPassword</button> <button type="button" class="btn btn-secondary" @onclick="@ToggleConfirmPassword" tabindex="-1">@_toggleConfirmPassword</button>
</div> </div>
</div> </div>
@ -95,7 +95,13 @@
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="Provide the email address for the host user account" ResourceKey="Email">Email:</Label> <Label Class="col-sm-3" For="email" HelpText="Provide the email address for the host user account" ResourceKey="Email">Email:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input type="text" class="form-control" @bind="@_hostEmail" /> <input type="text" class="form-control" maxlength="256" @bind="@_hostEmail" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="name" HelpText="Provide the full name of the host user" ResourceKey="Name">Full Name:</Label>
<div class="col-sm-9">
<input type="text" class="form-control" maxlength="50" @bind="@_hostName" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
@ -153,6 +159,7 @@
private string _toggleConfirmPassword = string.Empty; private string _toggleConfirmPassword = string.Empty;
private string _confirmPassword = string.Empty; private string _confirmPassword = string.Empty;
private string _hostEmail = string.Empty; private string _hostEmail = string.Empty;
private string _hostName = string.Empty;
private List<SiteTemplate> _templates; private List<SiteTemplate> _templates;
private string _template = Constants.DefaultSiteTemplate; private string _template = Constants.DefaultSiteTemplate;
private bool _register = true; private bool _register = true;
@ -236,7 +243,7 @@
} }
} }
if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@") && !string.IsNullOrEmpty(_hostName))
{ {
var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword); var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword);
if (result.Succeeded) if (result.Succeeded)
@ -256,7 +263,7 @@
HostUsername = _hostUsername, HostUsername = _hostUsername,
HostPassword = _hostPassword, HostPassword = _hostPassword,
HostEmail = _hostEmail, HostEmail = _hostEmail,
HostName = _hostUsername, HostName = _hostName,
TenantName = TenantNames.Master, TenantName = TenantNames.Master,
IsNewTenant = true, IsNewTenant = true,
SiteName = Constants.DefaultSite, SiteName = Constants.DefaultSite,

View File

@ -49,18 +49,24 @@
} }
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label> <Label Class="col-sm-3" For="capacity" HelpText="Enter the maximum folder capacity (in megabytes). Specify zero if the capacity is unlimited." ResourceKey="Capacity">Capacity: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="capacity" class="form-control" @bind="@_capacity" required /> <input id="capacity" class="form-control" @bind="@_capacity" required />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="cachecontrol" HelpText="Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI." ResourceKey="CacheControl">Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" maxlength="50" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imagesizes" HelpText="Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended)." ResourceKey="ImageSizes">Image Sizes: </Label>
<div class="col-sm-9">
<input id="imagesizes" class="form-control" @bind="@_imagesizes" maxlength="512" />
</div>
</div>
</div> </div>
@if (PageState.QueryString.ContainsKey("id")) @if (PageState.QueryString.ContainsKey("id"))
{ {
@ -100,8 +106,9 @@
private int _parentId = -1; private int _parentId = -1;
private string _name; private string _name;
private string _type = FolderTypes.Private; private string _type = FolderTypes.Private;
private string _imagesizes = string.Empty;
private string _capacity = "0"; private string _capacity = "0";
private string _cachecontrol = string.Empty;
private string _imagesizes = string.Empty;
private bool _isSystem; private bool _isSystem;
private List<Permission> _permissions = null; private List<Permission> _permissions = null;
private string _createdBy; private string _createdBy;
@ -132,8 +139,9 @@
_parentId = folder.ParentId ?? -1; _parentId = folder.ParentId ?? -1;
_name = folder.Name; _name = folder.Name;
_type = folder.Type; _type = folder.Type;
_imagesizes = folder.ImageSizes;
_capacity = folder.Capacity.ToString(); _capacity = folder.Capacity.ToString();
_cachecontrol = folder.CacheControl;
_imagesizes = folder.ImageSizes;
_isSystem = folder.IsSystem; _isSystem = folder.IsSystem;
_permissions = folder.PermissionList; _permissions = folder.PermissionList;
_createdBy = folder.CreatedBy; _createdBy = folder.CreatedBy;
@ -193,7 +201,7 @@
{ {
folder.ParentId = _parentId; folder.ParentId = _parentId;
} }
// check for duplicate folder names // check for duplicate folder names
if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId)) if (_folders.Any(item => item.ParentId == folder.ParentId && item.Name == _name && item.FolderId != _folderId))
{ {
@ -204,8 +212,9 @@
folder.SiteId = PageState.Site.SiteId; folder.SiteId = PageState.Site.SiteId;
folder.Name = _name; folder.Name = _name;
folder.Type = _type; folder.Type = _type;
folder.ImageSizes = _imagesizes;
folder.Capacity = int.Parse(_capacity); folder.Capacity = int.Parse(_capacity);
folder.CacheControl = _cachecontrol;
folder.ImageSizes = _imagesizes;
folder.IsSystem = _isSystem; folder.IsSystem = _isSystem;
folder.PermissionList = _permissionGrid.GetPermissionList(); folder.PermissionList = _permissionGrid.GetPermissionList();

View File

@ -3,54 +3,92 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IFolderService FolderService @inject IFolderService FolderService
@inject IFileService FileService @inject IFileService FileService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@if (_files != null) @if (_files == null)
{ {
<div class="row"> <p>
<div class="col-md mb-1"> <em>@SharedLocalizer["Loading"]</em>
<ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" /> </p>
</div> }
<div class="col-md-8 mb-1"> else
<div class="input-group"> {
<span class="input-group-text">@Localizer["Folder"]:</span> <TabStrip>
<select class="form-select" @onchange="(e => FolderChanged(e))"> <TabPanel Name="Files" Heading="Files" ResourceKey="Files">
@foreach (Folder folder in _folders) <div class="row">
{ <div class="col-md mb-1">
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option> <ActionLink Action="Edit" Text="Add Folder" Class="btn btn-secondary" ResourceKey="AddFolder" />
} </div>
</select> <div class="col-md-8 mb-1">
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp; <div class="input-group">
<span class="input-group-text">@Localizer["Folder"]:</span>
<select class="form-select" @onchange="(e => FolderChanged(e))">
@foreach (Folder folder in _folders)
{
<option value="@(folder.FolderId)">@(new string('-', folder.Level * 2))@(folder.Name)</option>
}
</select>
<ActionLink Action="Edit" Text="Edit Folder" Class="btn btn-secondary" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="EditFolder" />&nbsp;
</div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div> </div>
</div>
<div class="col-md mb-1 text-end">
<ActionLink Action="Add" Text="Upload Files" Parameters="@($"id=" + _folderId.ToString())" ResourceKey="UploadFiles" />
</div>
</div>
<Pager Items="@_files" SearchProperties="Name"> @if (_files.Count != 0)
<Header> {
<th style="width: 1px;">&nbsp;</th> <Pager Items="@_files" SearchProperties="Name">
<th style="width: 1px;">&nbsp;</th> <Header>
<th>@SharedLocalizer["Name"]</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Modified"]</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Type"]</th> <th>@SharedLocalizer["Name"]</th>
<th>@Localizer["Size"]</th> <th>@Localizer["Modified"]</th>
</Header> <th>@Localizer["Type"]</th>
<Row> <th>@Localizer["Size"]</th>
<td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td> </Header>
<td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td> <Row>
<td><a href="@context.Url" target="_new">@context.Name</a></td> <td><ActionLink Action="Details" Text="Edit" Parameters="@($"id=" + context.FileId.ToString())" ResourceKey="Details" /></td>
<td>@context.ModifiedOn</td> <td><ActionDialog Header="Delete File" Message="@string.Format(Localizer["Confirm.File.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteFile(context))" ResourceKey="DeleteFile" /></td>
<td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td> <td><a href="@context.Url" target="_new">@context.Name</a></td>
<td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td> <td>@context.ModifiedOn</td>
</Row> <td>@context.Extension.ToUpper() @SharedLocalizer["File"]</td>
</Pager> <td>@string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB</td>
@if (_files.Count == 0) </Row>
{ </Pager>
<div class="text-center">@Localizer["NoFiles"]</div> }
} else
{
<div class="text-center">@Localizer["NoFiles"]</div>
}
</TabPanel>
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings" Security="SecurityAccessLevel.Admin">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="maxChunkSize" HelpText="Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks)." ResourceKey="MaxChunkSize">Max Upload Chunk Size (MB): </Label>
<div class="col-sm-9">
<input id="maxChunkSize" type="number" min="1" max="10" step="1" class="form-control" @bind="@_maxChunkSize" />
</div>
</div>
</div>
<br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel>
</TabStrip>
} }
@code { @code {
@ -58,6 +96,10 @@
private int _folderId = -1; private int _folderId = -1;
private List<File> _files; private List<File> _files;
private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private int _maxChunkSize = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
@ -71,6 +113,13 @@
_folderId = _folders[0].FolderId; _folderId = _folders[0].FolderId;
await GetFiles(); await GetFiles();
} }
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
_maxChunkSize = int.Parse(SettingService.GetSetting(settings, "MaxChunkSize", "1"));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -115,4 +164,23 @@
AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error); AddModuleMessage(string.Format(Localizer["Error.File.Delete"], file.Name), MessageType.Error);
} }
} }
private async Task SaveSiteSettings()
{
try
{
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "MaxChunkSize", _maxChunkSize.ToString(), false);
await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Saving Site Settings {Error}", ex.Message);
AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error);
}
}
} }

View File

@ -111,6 +111,8 @@
private async Task CreateModule() private async Task CreateModule()
{ {
validated = true; validated = true;
_owner = _owner.Trim();
_module = _module.Trim();
var interop = new Interop(JSRuntime); var interop = new Interop(JSRuntime);
if (await interop.FormValid(form)) if (await interop.FormValid(form))
{ {

View File

@ -116,7 +116,7 @@
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label> <Label Class="col-sm-3" For="path" HelpText="Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'." ResourceKey="UrlPath">Url Path: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" /> <input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div> </div>
@ -263,6 +263,12 @@
<input id="title" class="form-control" @bind="@_title" maxlength="200" /> <input id="title" class="form-control" @bind="@_title" maxlength="200" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="path" HelpText="Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash." ResourceKey="PersonalizedUrlPath">Url Path: </Label>
<div class="col-sm-9">
<input id="path" class="form-control" @bind="@_path" maxlength="256" />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label> <Label Class="col-sm-3" For="theme" HelpText="Select the theme for this page" ResourceKey="Theme">Theme: </Label>
<div class="col-sm-9"> <div class="col-sm-9">

View File

@ -13,71 +13,71 @@
} }
else else
{ {
<TabStrip> <TabStrip>
<TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages"> <TabPanel Name="Pages" ResourceKey="Pages" Heading="Pages">
@if (!_pages.Where(item => item.IsDeleted).Any()) @if (!_pages.Where(item => item.IsDeleted).Any())
{ {
<br /> <br />
<p>@Localizer["NoPage.Deleted"]</p> <p>@Localizer["NoPage.Deleted"]</p>
} }
else else
{ {
<Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage"> <Pager Items="@_pages.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pagePage.ToString()" OnPageChange="OnPageChangePage">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Path"]</th>
<th>@Localizer["DeletedBy"]</th> <th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th> <th>@Localizer["DeletedOn"]</th>
</Header> </Header>
<Row> <Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td> <td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td> <td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td> <td>@context.Path</td>
<td>@context.DeletedBy</td> <td>@context.DeletedBy</td>
<td>@context.DeletedOn</td> <td>@context.DeletedOn</td>
</Row> </Row>
</Pager> </Pager>
<br /> <br />
<ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" /> <ActionDialog Header="Remove All Deleted Pages" Message="Are You Sure You Wish To Permanently Remove All Deleted Pages?" Action="Remove All Deleted Pages" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllPages())" ResourceKey="DeleteAllPages" />
} }
</TabPanel> </TabPanel>
<TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules"> <TabPanel Name="Modules" ResourceKey="Modules" Heading="Modules">
@if (!_modules.Where(item => item.IsDeleted).Any()) @if (!_modules.Where(item => item.IsDeleted).Any())
{ {
<br /> <br />
<p>@Localizer["NoModule.Deleted"]</p> <p>@Localizer["NoModule.Deleted"]</p>
} }
else else
{ {
<Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule"> <Pager Items="@_modules.Where(item => item.IsDeleted).OrderByDescending(item => item.DeletedOn)" CurrentPage="@_pageModule.ToString()" OnPageChange="OnPageChangeModule">
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@Localizer["Page"]</th> <th>@Localizer["Page"]</th>
<th>@Localizer["Module"]</th> <th>@Localizer["Module"]</th>
<th>@Localizer["DeletedBy"]</th> <th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th> <th>@Localizer["DeletedOn"]</th>
</Header> </Header>
<Row> <Row>
<td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td> <td><button type="button" @onclick="@(() => RestoreModule(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td> <td><ActionDialog Header="Delete Module" Message="@string.Format(Localizer["Confirm.Module.Delete"], context.Title)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteModule(context))" ResourceKey="DeleteModule" /></td>
<td>@_pages.Find(item => item.PageId == context.PageId).Name</td> <td>@_pages.Find(item => item.PageId == context.PageId).Name</td>
<td>@context.Title</td> <td>@context.Title</td>
<td>@context.DeletedBy</td> <td>@context.DeletedBy</td>
<td>@context.DeletedOn</td> <td>@context.DeletedOn</td>
</Row> </Row>
</Pager> </Pager>
<br /> <br />
<ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" /> <ActionDialog Header="Remove All Deleted Modules" Message="Are You Sure You Wish To Permanently Remove All Deleted Modules?" Action="Remove All Deleted Modules" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeleteAllModules())" ResourceKey="DeleteAllModules" />
} }
</TabPanel> </TabPanel>
</TabStrip> </TabStrip>
} }
@code { @code {
private List<Page> _pages; private List<Page> _pages;
private List<Module> _modules; private List<Module> _modules;
private int _pagePage = 1; private int _pagePage = 1;
private int _pageModule = 1; private int _pageModule = 1;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -105,12 +105,25 @@ else
{ {
try try
{ {
page.IsDeleted = false; var validated = true;
await PageService.UpdatePageAsync(page); if (page.ParentId != null)
await logger.LogInformation("Page Restored {Page}", page); {
await Load(); var parent = _pages.Find(item => item.PageId == page.ParentId);
StateHasChanged(); validated = !parent.IsDeleted;
NavigationManager.NavigateTo(NavigateUrl()); }
if (validated)
{
page.IsDeleted = false;
await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page);
AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success);
await Load();
StateHasChanged();
}
else
{
AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -125,9 +138,9 @@ else
{ {
await PageService.DeletePageAsync(page.PageId); await PageService.DeletePageAsync(page.PageId);
await logger.LogInformation("Page Permanently Deleted {Page}", page); await logger.LogInformation("Page Permanently Deleted {Page}", page);
AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -148,10 +161,10 @@ else
} }
await logger.LogInformation("Pages Permanently Deleted"); await logger.LogInformation("Pages Permanently Deleted");
AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success);
await Load(); await Load();
HideProgressIndicator(); HideProgressIndicator();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -169,6 +182,7 @@ else
pagemodule.IsDeleted = false; pagemodule.IsDeleted = false;
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation("Module Restored {Module}", module); await logger.LogInformation("Module Restored {Module}", module);
AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
} }
@ -185,6 +199,7 @@ else
{ {
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
await logger.LogInformation("Module Permanently Deleted {Module}", module); await logger.LogInformation("Module Permanently Deleted {Module}", module);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
} }
@ -205,6 +220,7 @@ else
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
} }
await logger.LogInformation("Modules Permanently Deleted"); await logger.LogInformation("Modules Permanently Deleted");
AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success);
await Load(); await Load();
HideProgressIndicator(); HideProgressIndicator();
StateHasChanged(); StateHasChanged();

View File

@ -58,7 +58,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -180,27 +180,28 @@ else
private async Task DeleteUserRole(int UserRoleId) private async Task DeleteUserRole(int UserRoleId)
{ {
validated = true; try
var interop = new Interop(JSRuntime);
if (await interop.FormValid(form))
{ {
try var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{ {
await UserRoleService.DeleteUserRoleAsync(UserRoleId); await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
} }
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles();
StateHasChanged();
} }
else catch (Exception ex)
{ {
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message);
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
} }
} }
} }

View File

@ -144,18 +144,6 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="imageExt" HelpText="Enter a comma separated list of image file extensions" ResourceKey="ImageExtensions">Image Extensions: </Label>
<div class="col-sm-9">
<input id="imageExt" spellcheck="false" class="form-control" @bind="@_imageFiles" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="uploadableFileExt" HelpText="Enter a comma separated list of uploadable file extensions" ResourceKey="UploadableFileExtensions">Uploadable File Extensions: </Label>
<div class="col-sm-9">
<input id="uploadableFileExt" spellcheck="false" class="form-control" @bind="@_uploadableFiles" />
</div>
</div>
</div> </div>
</Section> </Section>
<Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent"> <Section Name="PageContent" Heading="Page Content" ResourceKey="PageContent">
@ -431,7 +419,6 @@
private Dictionary<string, string> _textEditors = new Dictionary<string, string>(); private Dictionary<string, string> _textEditors = new Dictionary<string, string>();
private string _textEditor = ""; private string _textEditor = "";
private string _imageFiles = string.Empty; private string _imageFiles = string.Empty;
private string _uploadableFiles = string.Empty;
private string _headcontent = string.Empty; private string _headcontent = string.Empty;
private string _bodycontent = string.Empty; private string _bodycontent = string.Empty;
@ -528,8 +515,6 @@
_textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor);
_imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); _imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles);
_imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; _imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles;
_uploadableFiles = SettingService.GetSetting(settings, "UploadableFiles", Constants.UploadableFiles);
_uploadableFiles = (string.IsNullOrEmpty(_uploadableFiles)) ? Constants.UploadableFiles : _uploadableFiles;
// page content // page content
_headcontent = site.HeadContent; _headcontent = site.HeadContent;
@ -734,8 +719,6 @@
// functionality // functionality
settings = SettingService.SetSetting(settings, "TextEditor", _textEditor); settings = SettingService.SetSetting(settings, "TextEditor", _textEditor);
settings = SettingService.SetSetting(settings, "ImageFiles", (_imageFiles != Constants.ImageFiles) ? _imageFiles.Replace(" ", "") : "", false);
settings = SettingService.SetSetting(settings, "UploadableFiles", (_uploadableFiles != Constants.UploadableFiles) ? _uploadableFiles.Replace(" ", "") : "", false);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);

View File

@ -133,15 +133,27 @@
</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="packageregistryurl" HelpText="Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager: </Label> <Label Class="col-sm-3" For="cachecontrol" HelpText="Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled." ResourceKey="CacheControl">Static Asset Caching: </Label>
<div class="col-sm-9">
<input id="cachecontrol" class="form-control" @bind="@_cachecontrol" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryurl" HelpText="Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation." ResourceKey="PackageManager">Package Manager Url: </Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" /> <input id="packageregistryurl" class="form-control" @bind="@_packageregistryurl" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="packageregistryemail" HelpText="Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations." ResourceKey="PackageManagerEmail">Package Manager Email: </Label>
<div class="col-sm-9">
<input id="packageregistryemail" class="form-control" @bind="@_packageregistryemail" />
</div>
</div>
</div> </div>
<br /><br /> <br /><br />
<button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp; <button type="button" class="btn btn-success" @onclick="SaveConfig">@SharedLocalizer["Save"]</button>&nbsp;
<a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Access.ApiFramework"]</a>&nbsp; <a class="btn btn-primary" href="swagger/index.html" target="_new">@Localizer["Swagger"]</a>&nbsp;
<ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" /> <ActionDialog Header="Restart Application" Message="Are You Sure You Wish To Restart The Application?" Action="Restart Application" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await RestartApplication())" ResourceKey="RestartApplication" />
</TabPanel> </TabPanel>
<TabPanel Name="Log" Heading="Log" ResourceKey="Log"> <TabPanel Name="Log" Heading="Log" ResourceKey="Log">
@ -179,9 +191,11 @@
private string _logginglevel = string.Empty; private string _logginglevel = string.Empty;
private string _notificationlevel = string.Empty; private string _notificationlevel = string.Empty;
private string _swagger = string.Empty; private string _swagger = string.Empty;
private string _cachecontrol = string.Empty;
private string _packageregistryurl = string.Empty; private string _packageregistryurl = string.Empty;
private string _packageregistryemail = string.Empty;
private string _log = string.Empty; private string _log = string.Empty;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -209,9 +223,11 @@
_detailederrors = systeminfo["DetailedErrors"].ToString(); _detailederrors = systeminfo["DetailedErrors"].ToString();
_logginglevel = systeminfo["Logging:LogLevel:Default"].ToString(); _logginglevel = systeminfo["Logging:LogLevel:Default"].ToString();
_notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString(); _notificationlevel = systeminfo["Logging:LogLevel:Notify"].ToString();
_swagger = systeminfo["UseSwagger"].ToString(); _swagger = systeminfo["UseSwagger"].ToString();
_cachecontrol = systeminfo["CacheControl"].ToString();
_packageregistryurl = systeminfo["PackageRegistryUrl"].ToString(); _packageregistryurl = systeminfo["PackageRegistryUrl"].ToString();
} _packageregistryemail = systeminfo["PackageRegistryEmail"].ToString();
}
systeminfo = await SystemService.GetSystemInfoAsync("log"); systeminfo = await SystemService.GetSystemInfoAsync("log");
if (systeminfo != null) if (systeminfo != null)
@ -229,8 +245,10 @@
settings.Add("Logging:LogLevel:Default", _logginglevel); settings.Add("Logging:LogLevel:Default", _logginglevel);
settings.Add("Logging:LogLevel:Notify", _notificationlevel); settings.Add("Logging:LogLevel:Notify", _notificationlevel);
settings.Add("UseSwagger", _swagger); settings.Add("UseSwagger", _swagger);
settings.Add("PackageRegistryUrl", _packageregistryurl); settings.Add("CacheControl", _cachecontrol);
await SystemService.UpdateSystemInfoAsync(settings); settings.Add("PackageRegistryUrl", _packageregistryurl);
settings.Add("PackageRegistryEmail", _packageregistryemail);
await SystemService.UpdateSystemInfoAsync(settings);
AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success); AddModuleMessage(Localizer["Success.UpdateConfig.Restart"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)

View File

@ -3,6 +3,7 @@
@inject NavigationManager NavigationManager @inject NavigationManager NavigationManager
@inject IUrlMappingService UrlMappingService @inject IUrlMappingService UrlMappingService
@inject ISiteService SiteService @inject ISiteService SiteService
@inject ISettingService SettingService
@inject IStringLocalizer<Index> Localizer @inject IStringLocalizer<Index> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -62,7 +63,13 @@ else
</select> </select>
</div> </div>
</div> </div>
</div> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="retention" HelpText="Number of days of broken urls to retain" ResourceKey="Retention">Retention (Days): </Label>
<div class="col-sm-9">
<input id="retention" class="form-control" type="number" min="0" step="1" @bind="@_retention" />
</div>
</div>
</div>
<br /> <br />
<button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveSiteSettings">@SharedLocalizer["Save"]</button>
</TabPanel> </TabPanel>
@ -73,6 +80,7 @@ else
private bool _mapped = true; private bool _mapped = true;
private List<UrlMapping> _urlMappings; private List<UrlMapping> _urlMappings;
private string _capturebrokenurls; private string _capturebrokenurls;
private int _retention = 30;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin;
@ -80,7 +88,10 @@ else
{ {
await GetUrlMappings(); await GetUrlMappings();
_capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString(); _capturebrokenurls = PageState.Site.CaptureBrokenUrls.ToString();
}
var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
_retention = int.Parse(SettingService.GetSetting(settings, "UrlMappingRetention", "30"));
}
private async void MappedChanged(ChangeEventArgs e) private async void MappedChanged(ChangeEventArgs e)
{ {
@ -124,7 +135,12 @@ else
var site = PageState.Site; var site = PageState.Site;
site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls); site.CaptureBrokenUrls = bool.Parse(_capturebrokenurls);
await SiteService.UpdateSiteAsync(site); await SiteService.UpdateSiteAsync(site);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
var settings = await SettingService.GetSiteSettingsAsync(site.SiteId);
settings = SettingService.SetSetting(settings, "UrlMappingRetention", _retention.ToString(), true);
await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId);
AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -6,6 +6,7 @@
@inject IProfileService ProfileService @inject IProfileService ProfileService
@inject ISettingService SettingService @inject ISettingService SettingService
@inject IFileService FileService @inject IFileService FileService
@inject IServiceProvider ServiceProvider
@inject IStringLocalizer<Edit> Localizer @inject IStringLocalizer<Edit> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -51,15 +52,18 @@
<input id="displayname" class="form-control" @bind="@displayname" /> <input id="displayname" class="form-control" @bind="@displayname" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center"> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label> {
<div class="col-sm-9"> <div class="row mb-1 align-items-center">
<select id="isdeleted" class="form-select" @bind="@isdeleted"> <Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<option value="True">@SharedLocalizer["Yes"]</option> <div class="col-sm-9">
<option value="False">@SharedLocalizer["No"]</option> <select id="isdeleted" class="form-select" @bind="@isdeleted">
</select> <option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div> </div>
</div> }
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label> <Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -127,8 +131,15 @@
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br /> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost)
<br /> {
<button type="button" class="btn btn-primary ms-1" @onclick="ImpersonateUser">@Localizer["Impersonate"]</button>
}
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True")
{
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo> <AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@ -146,6 +157,7 @@
private string isdeleted; private string isdeleted;
private string lastlogin; private string lastlogin;
private string lastipaddress; private string lastipaddress;
private bool ishost = false;
private List<Profile> profiles; private List<Profile> profiles;
private Dictionary<string, string> userSettings; private Dictionary<string, string> userSettings;
@ -180,6 +192,7 @@
isdeleted = user.IsDeleted.ToString(); isdeleted = user.IsDeleted.ToString();
lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn);
lastipaddress = user.LastIPAddress; lastipaddress = user.LastIPAddress;
ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host);
userSettings = user.Settings; userSettings = user.Settings;
createdby = user.CreatedBy; createdby = user.CreatedBy;
@ -226,8 +239,10 @@
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); {
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
}
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
@ -259,6 +274,44 @@
} }
} }
private async Task ImpersonateUser()
{
try
{
await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username);
// post back to the server so that the cookies are set correctly
var interop = new Interop(JSRuntime);
var fields = new { __RequestVerificationToken = SiteState.AntiForgeryToken, username = username, returnurl = PageState.Alias.Path };
string url = Utilities.TenantUrl(PageState.Alias, "/pages/impersonate/");
await interop.SubmitForm(url, fields);
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Impersonating User {Username} {Error}", username, ex.Message);
AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error);
}
}
private async Task DeleteUser()
{
try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId)
{
var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId);
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Permanently Deleted {User}", user);
NavigationManager.NavigateTo(NavigateUrl());
}
}
catch (Exception ex)
{
await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message);
AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
}
}
private bool ValidateProfiles() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in profiles)

View File

@ -35,7 +35,7 @@ else
<ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" /> <ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId || context.User.IsDeleted)" ResourceKey="DeleteUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" /> <ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
@ -379,7 +379,16 @@ else
<input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" /> <input id="profileclaimtypes" class="form-control" @bind="@_profileclaimtypes" />
</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="savetokens" HelpText="Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie." ResourceKey="SaveTokens">Save Tokens?</Label>
<div class="col-sm-9">
<select id="savetokens" class="form-select" @bind="@_savetokens" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label> <Label Class="col-sm-3" For="domainfilter" HelpText="Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9"> <div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" /> <input id="domainfilter" class="form-control" @bind="@_domainfilter" />
@ -497,6 +506,7 @@ else
private string _roleclaimmappings; private string _roleclaimmappings;
private string _synchronizeroles; private string _synchronizeroles;
private string _profileclaimtypes; private string _profileclaimtypes;
private string _savetokens;
private string _domainfilter; private string _domainfilter;
private string _createusers; private string _createusers;
private string _verifyusers; private string _verifyusers;
@ -577,6 +587,7 @@ else
_roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", ""); _roleclaimmappings = SettingService.GetSetting(settings, "ExternalLogin:RoleClaimMappings", "");
_synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false"); _synchronizeroles = SettingService.GetSetting(settings, "ExternalLogin:SynchronizeRoles", "false");
_profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", ""); _profileclaimtypes = SettingService.GetSetting(settings, "ExternalLogin:ProfileClaimTypes", "");
_savetokens = SettingService.GetSetting(settings, "ExternalLogin:SaveTokens", "false");
_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");
_verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true");
@ -600,19 +611,31 @@ else
{ {
try try
{ {
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
if (user != null)
{ {
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
await logger.LogInformation("User Deleted {User}", UserRole.User); if (user != null)
await LoadUsersAsync(true); {
StateHasChanged(); user.IsDeleted = true;
await UserService.UpdateUserAsync(user);
await logger.LogInformation("User Soft Deleted {User}", user);
}
} }
else
{
var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId);
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success);
await LoadUsersAsync(true);
StateHasChanged();
} }
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
} }
} }
@ -666,6 +689,7 @@ else
settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true); settings = SettingService.SetSetting(settings, "ExternalLogin:RoleClaimMappings", _roleclaimmappings, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:SynchronizeRoles", _synchronizeroles, 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:SaveTokens", _savetokens, 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);
settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true);

View File

@ -53,17 +53,17 @@ else
<p align="center"> <p align="center">
<Pager Items="@userroles"> <Pager Items="@userroles">
<Header> <Header>
<th>@Localizer["Roles"]</th> <th>@Localizer["Roles"]</th>
<th>@Localizer["Effective"]</th> <th>@Localizer["Effective"]</th>
<th>@Localizer["Expiry"]</th> <th>@Localizer["Expiry"]</th>
<th>&nbsp;</th> <th>&nbsp;</th>
</Header> </Header>
<Row> <Row>
<td>@context.Role.Name</td> <td>@context.Role.Name</td>
<td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td> <td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td>
<td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td> <td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td>
<td> <td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.Name == RoleNames.Host && userid == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -171,8 +171,18 @@ else
{ {
try try
{ {
await UserRoleService.DeleteUserRoleAsync(UserRoleId); var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); if (userrole.Role.Name == RoleNames.Registered)
{
userrole.ExpiryDate = DateTime.UtcNow;
await UserRoleService.UpdateUserRoleAsync(userrole);
await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
else
{
await UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success); AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success);
await GetUserRoles(); await GetUserRoles();
StateHasChanged(); StateHasChanged();

View File

@ -22,9 +22,9 @@
<div class="modal-footer"> <div class="modal-footer">
@if (!string.IsNullOrEmpty(Action)) @if (!string.IsNullOrEmpty(Action))
{ {
<button type="button" class="@Class" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button> <button type="button" class="@ConfirmClass" @onclick="Confirm">@((MarkupString)_iconSpan) @Text</button>
} }
<button type="button" class="btn btn-secondary" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button> <button type="button" class="@CancelClass" @onclick="DisplayModal">@SharedLocalizer["Cancel"]</button>
</div> </div>
</div> </div>
</div> </div>
@ -66,12 +66,12 @@ else
{ {
<form method="post" @formname="@($"ActionDialogConfirmForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="Confirm" data-enhance> <form method="post" @formname="@($"ActionDialogConfirmForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="Confirm" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="@Class">@((MarkupString)_iconSpan) @Text</button> <button type="submit" class="@ConfirmClass">@((MarkupString)_iconSpan) @Text</button>
</form> </form>
} }
<form method="post" @formname="@($"ActionDialogCancelForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance> <form method="post" @formname="@($"ActionDialogCancelForm:{ModuleState.PageModuleId}:{Id}")" @onsubmit="DisplayModal" data-enhance>
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<button type="submit" class="btn btn-secondary">@SharedLocalizer["Cancel"]</button> <button type="submit" class="@CancelClass">@SharedLocalizer["Cancel"]</button>
</form> </form>
</div> </div>
</div> </div>
@ -128,6 +128,12 @@ else
[Parameter] [Parameter]
public string Class { get; set; } // optional public string Class { get; set; } // optional
[Parameter]
public string ConfirmClass { get; set; } // optional - for Confirm modal button
[Parameter]
public string CancelClass { get; set; } // optional - for Cancel modal button
[Parameter] [Parameter]
public bool Disabled { get; set; } // optional public bool Disabled { get; set; } // optional
@ -168,6 +174,16 @@ else
Class = "btn btn-success"; Class = "btn btn-success";
} }
if (string.IsNullOrEmpty(ConfirmClass))
{
ConfirmClass = Class;
}
if (string.IsNullOrEmpty(CancelClass))
{
CancelClass = "btn btn-secondary";
}
if (!string.IsNullOrEmpty(EditMode)) if (!string.IsNullOrEmpty(EditMode))
{ {
_editmode = bool.Parse(EditMode); _editmode = bool.Parse(EditMode);
@ -196,7 +212,7 @@ else
_openIconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}"; _openIconSpan = $"<span class=\"{IconName}\"></span>{(IconOnly ? "" : "&nbsp")}";
_iconSpan = $"<span class=\"{IconName}\"></span>&nbsp"; _iconSpan = $"<span class=\"{IconName}\"></span>&nbsp";
} }
_permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList; _permissions = (PermissionList == null) ? ModuleState.PermissionList : PermissionList;
_authorized = IsAuthorized(); _authorized = IsAuthorized();

View File

@ -3,8 +3,8 @@
@inherits ModuleControlBase @inherits ModuleControlBase
@inject IFolderService FolderService @inject IFolderService FolderService
@inject IFileService FileService @inject IFileService FileService
@inject ISettingService SettingService
@inject IUserService UserService @inject IUserService UserService
@inject ISettingService SettingService
@inject IStringLocalizer<FileManager> Localizer @inject IStringLocalizer<FileManager> Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer @inject IStringLocalizer<SharedResources> SharedLocalizer
@ -157,6 +157,9 @@
[Parameter] [Parameter]
public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false
[Parameter]
public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB
[Parameter] [Parameter]
public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded public EventCallback<int> OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded
@ -359,6 +362,8 @@
} }
if (restricted == "") if (restricted == "")
{ {
CancellationTokenSource tokenSource = new CancellationTokenSource();
try try
{ {
// upload the files // upload the files
@ -377,57 +382,21 @@
} }
} }
var chunksize = ChunkSize;
if (chunksize == 1)
{
// if ChunkSize parameter is not overridden use the site setting
chunksize = int.Parse(SettingService.GetSetting(PageState.Site.Settings, "MaxChunkSize", "1"));
}
if (!ShowProgress) if (!ShowProgress)
{ {
_uploading = true; _uploading = true;
StateHasChanged(); StateHasChanged();
} }
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt); // upload files
var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token);
// uploading is asynchronous so we need to poll to determine if uploads are completed
var success = true;
int upload = 0;
while (upload < uploads.Length && success)
{
success = false;
var filename = uploads[upload].Split(':')[0];
var size = Int64.Parse(uploads[upload].Split(':')[1]); // bytes
var megabits = (size / 1048576.0) * 8; // binary conversion
var uploadspeed = (PageState.Alias.Name.Contains("localhost")) ? 100 : 3; // 3 Mbps is FCC minimum for broadband upload
var uploadtime = (megabits / uploadspeed); // seconds
var maxattempts = 5; // polling (minimum timeout duration will be 5 seconds)
var sleep = (int)Math.Ceiling(uploadtime / maxattempts) * 1000; // milliseconds
int attempts = 0;
while (attempts < maxattempts && !success)
{
attempts += 1;
Thread.Sleep(sleep);
if (Folder == Constants.PackagesFolder)
{
var files = await FileService.GetFilesAsync(folder);
if (files != null && files.Any(item => item.Name == filename))
{
success = true;
}
}
else
{
var file = await FileService.GetFileAsync(int.Parse(folder), filename);
if (file != null)
{
success = true;
}
}
}
if (success)
{
upload++;
}
}
// reset progress indicators // reset progress indicators
if (ShowProgress) if (ShowProgress)
@ -452,7 +421,7 @@
} }
else else
{ {
await logger.LogInformation("File Upload Failed Or Is Still In Progress {Files}", uploads); await logger.LogError("File Upload Failed {Files}", uploads);
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
} }
@ -482,6 +451,10 @@
_message = Localizer["Error.File.Upload"]; _message = Localizer["Error.File.Upload"];
_messagetype = MessageType.Error; _messagetype = MessageType.Error;
_uploading = false; _uploading = false;
await tokenSource.CancelAsync();
}
finally {
tokenSource.Dispose();
} }
} }

View File

@ -18,6 +18,7 @@ namespace Oqtane.Modules
private Logger _logger; private Logger _logger;
private string _urlparametersstate; private string _urlparametersstate;
private Dictionary<string, string> _urlparameters; private Dictionary<string, string> _urlparameters;
private bool _scriptsloaded = false;
protected Logger logger => _logger ?? (_logger = new Logger(this)); protected Logger logger => _logger ?? (_logger = new Logger(this));
@ -98,7 +99,7 @@ namespace Oqtane.Modules
var inline = 0; var inline = 0;
foreach (Resource resource in resources) foreach (Resource resource in resources)
{ {
if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive)
{ {
if (!string.IsNullOrEmpty(resource.Url)) if (!string.IsNullOrEmpty(resource.Url))
{ {
@ -117,6 +118,7 @@ namespace Oqtane.Modules
await interop.IncludeScripts(scripts.ToArray()); await interop.IncludeScripts(scripts.ToArray());
} }
} }
_scriptsloaded = true;
} }
} }
@ -125,6 +127,14 @@ namespace Oqtane.Modules
return PageState?.RenderId == ModuleState?.RenderId; return PageState?.RenderId == ModuleState?.RenderId;
} }
public bool ScriptsLoaded
{
get
{
return _scriptsloaded;
}
}
// path method // path method
public string ModulePath() public string ModulePath()
@ -132,6 +142,15 @@ namespace Oqtane.Modules
return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/"; return PageState?.Alias.BaseUrl + "/Modules/" + GetType().Namespace + "/";
} }
// fingerprint hash code for static assets
public string Fingerprint
{
get
{
return ModuleState.ModuleDefinition.Fingerprint;
}
}
// url methods // url methods
// navigate url // navigate url

View File

@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -12,7 +12,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -22,10 +22,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -8,20 +8,20 @@
} }
}, },
"profiles": { "profiles": {
"IIS Express": { "Oqtane": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Oqtane.Client": {
"commandName": "Project", "commandName": "Project",
"launchBrowser": true, "launchBrowser": true,
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"applicationUrl": "http://localhost:44358/" "applicationUrl": "http://localhost:44358/"
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
} }
} }
} }

View File

@ -186,4 +186,10 @@
<data name="Message.Username.Invalid" xml:space="preserve"> <data name="Message.Username.Invalid" xml:space="preserve">
<value>The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits.</value> <value>The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits.</value>
</data> </data>
<data name="Name.Text" xml:space="preserve">
<value>Full Name:</value>
</data>
<data name="Name.HelpText" xml:space="preserve">
<value>Provide the full name of the host user</value>
</data>
</root> </root>

View File

@ -175,7 +175,7 @@
<value>Capacity:</value> <value>Capacity:</value>
</data> </data>
<data name="ImageSizes.HelpText" xml:space="preserve"> <data name="ImageSizes.HelpText" xml:space="preserve">
<value>Enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes.</value> <value>Optionally enter a list of image sizes which can be generated dynamically from uploaded images (ie. 200x200,400x400). Use * to indicate the folder supports all image sizes (not recommended).</value>
</data> </data>
<data name="ImageSizes.Text" xml:space="preserve"> <data name="ImageSizes.Text" xml:space="preserve">
<value>Image Sizes:</value> <value>Image Sizes:</value>
@ -198,4 +198,10 @@
<data name="Settings.Heading" xml:space="preserve"> <data name="Settings.Heading" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Optionally provide a Cache-Control directive for this folder. For example 'public, max-age=60' indicates that files in this folder should be cached for 60 seconds. Please note that when caching is enabled, changes to files will not be immediately reflected in the UI.</value>
</data>
</root> </root>

View File

@ -165,4 +165,31 @@
<data name="UploadFiles.Text" xml:space="preserve"> <data name="UploadFiles.Text" xml:space="preserve">
<value>Upload Files</value> <value>Upload Files</value>
</data> </data>
<data name="Files.Heading" xml:space="preserve">
<value>Files</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="MaxChunkSize.Text" xml:space="preserve">
<value>Max Upload Chunk Size (MB):</value>
</data>
<data name="MaxChunkSize.HelpText" xml:space="preserve">
<value>Files are split into chunks to streamline the upload process. Specify the maximum chunk size in MB (note that higher chunk sizes should only be used on faster networks).</value>
</data>
<data name="Success.SaveSiteSettings" xml:space="preserve">
<value>Settings Saved Successfully</value>
</data>
<data name="Error.SaveSiteSettings" xml:space="preserve">
<value>Error Saving Settings</value>
</data>
</root> </root>

View File

@ -169,7 +169,7 @@
<value>Select whether the page is part of the site navigation or hidden</value> <value>Select whether the page is part of the site navigation or hidden</value>
</data> </data>
<data name="UrlPath.HelpText" xml:space="preserve"> <data name="UrlPath.HelpText" xml:space="preserve">
<value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. If the page is intended to be the root path specify '/'.</value> <value>Optionally enter a url path for this page (ie. home ). If you do not provide a url path, the page name will be used. Please note that spaces and punctuation will be replaced by a dash. If the page is intended to be the root path specify '/'.</value>
</data> </data>
<data name="Redirect.HelpText" xml:space="preserve"> <data name="Redirect.HelpText" xml:space="preserve">
<value>Optionally enter a url which this page should redirect to when a user navigates to it</value> <value>Optionally enter a url which this page should redirect to when a user navigates to it</value>
@ -297,4 +297,10 @@
<data name="ExpiryDate.Text" xml:space="preserve"> <data name="ExpiryDate.Text" xml:space="preserve">
<value>Expiry Date: </value> <value>Expiry Date: </value>
</data> </data>
</root> <data name="PersonalizedUrlPath.Text" xml:space="preserve">
<value>Url Path:</value>
</data>
<data name="PersonalizedUrlPath.HelpText" xml:space="preserve">
<value>Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash.</value>
</data>
</root>

View File

@ -195,4 +195,25 @@
<data name="Modules.Heading" xml:space="preserve"> <data name="Modules.Heading" xml:space="preserve">
<value>Modules</value> <value>Modules</value>
</data> </data>
<data name="Message.Page.Restore" xml:space="preserve">
<value>You Cannot Restore A Page If Its Parent Is Deleted</value>
</data>
<data name="Success.Page.Restore" xml:space="preserve">
<value>Page Restored Successfully</value>
</data>
<data name="Success.Page.Delete" xml:space="preserve">
<value>Page Deleted Successfully</value>
</data>
<data name="Success.Pages.Deleted" xml:space="preserve">
<value>All Pages Deleted Successfully</value>
</data>
<data name="Success.Module.Restore" xml:space="preserve">
<value>Module Restored Successfully</value>
</data>
<data name="Success.Module.Delete" xml:space="preserve">
<value>Module Deleted Successfully</value>
</data>
<data name="Success.Modules.Delete" xml:space="preserve">
<value>All Modules Deleted Successfully</value>
</data>
</root> </root>

View File

@ -402,18 +402,6 @@
<data name="Retention.Text" xml:space="preserve"> <data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value> <value>Retention (Days):</value>
</data> </data>
<data name="ImageExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of image file extensions</value>
</data>
<data name="ImageExtensions.Text" xml:space="preserve">
<value>Image Extensions:</value>
</data>
<data name="UploadableFileExtensions.HelpText" xml:space="preserve">
<value>Enter a comma separated list of uploadable file extensions</value>
</data>
<data name="UploadableFileExtensions.Text" xml:space="preserve">
<value>Uploadable File Extensions:</value>
</data>
<data name="HybridEnabled.HelpText" xml:space="preserve"> <data name="HybridEnabled.HelpText" xml:space="preserve">
<value>Specifies if the site can be integrated with an external .NET MAUI hybrid application</value> <value>Specifies if the site can be integrated with an external .NET MAUI hybrid application</value>
</data> </data>

View File

@ -117,8 +117,8 @@
<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="Access.ApiFramework" xml:space="preserve"> <data name="Swagger" xml:space="preserve">
<value>Access Swagger API</value> <value>Access Swagger UI</value>
</data> </data>
<data name="FrameworkVersion.HelpText" xml:space="preserve"> <data name="FrameworkVersion.HelpText" xml:space="preserve">
<value>Framework Version</value> <value>Framework Version</value>
@ -220,10 +220,10 @@
<value>You Have Been Successfully Registered For Updates</value> <value>You Have Been Successfully Registered For Updates</value>
</data> </data>
<data name="PackageManager.HelpText" xml:space="preserve"> <data name="PackageManager.HelpText" xml:space="preserve">
<value>Specify The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value> <value>Specify The Url Of The Package Manager Service For Installing Modules, Themes, And Translations. If This Field Is Blank It Means The Package Manager Service Is Disabled For This Installation.</value>
</data> </data>
<data name="PackageManager.Text" xml:space="preserve"> <data name="PackageManager.Text" xml:space="preserve">
<value>Package Manager:</value> <value>Package Manager Url:</value>
</data> </data>
<data name="Swagger.HelpText" xml:space="preserve"> <data name="Swagger.HelpText" xml:space="preserve">
<value>Specify If Swagger Is Enabled For Your Server API</value> <value>Specify If Swagger Is Enabled For Your Server API</value>
@ -294,4 +294,16 @@
<data name="Process.Text" xml:space="preserve"> <data name="Process.Text" xml:space="preserve">
<value>Process: </value> <value>Process: </value>
</data> </data>
<data name="PackageManagerEmail.Text" xml:space="preserve">
<value>Package Manager Email:</value>
</data>
<data name="PackageManagerEmail.HelpText" xml:space="preserve">
<value>Specify The Email Address Of The User Account Used For Interacting With The Package Manager Service. This Account Is Used For Managing Packages Across Multiple Installations.</value>
</data>
<data name="CacheControl.Text" xml:space="preserve">
<value>Static Asset Caching:</value>
</data>
<data name="CacheControl.HelpText" xml:space="preserve">
<value>Provide a Cache-Control directive for static assets. For example 'public, max-age=60' indicates that static assets should be cached for 60 seconds. A blank value indicates caching is not enabled.</value>
</data>
</root> </root>

View File

@ -162,4 +162,10 @@
<data name="Edit.Text" xml:space="preserve"> <data name="Edit.Text" xml:space="preserve">
<value>Edit</value> <value>Edit</value>
</data> </data>
<data name="Retention.Text" xml:space="preserve">
<value>Retention (Days):</value>
</data>
<data name="Retention.HelpText" xml:space="preserve">
<value>Number of days of broken urls to retain</value>
</data>
</root> </root>

View File

@ -195,4 +195,19 @@
<data name="LastLogin.Text" xml:space="preserve"> <data name="LastLogin.Text" xml:space="preserve">
<value>Last Login:</value> <value>Last Login:</value>
</data> </data>
<data name="DeleteUser.Header" xml:space="preserve">
<value>Delete User</value>
</data>
<data name="DeleteUser.Text" xml:space="preserve">
<value>Delete</value>
</data>
<data name="DeleteUser.Message" xml:space="preserve">
<value>Are You Sure You Wish To Permanently Delete This User?</value>
</data>
<data name="Impersonate" xml:space="preserve">
<value>Impersonate</value>
</data>
<data name="Error.User.Impersonate" xml:space="preserve">
<value>Unable To Impersonate User</value>
</data>
</root> </root>

View File

@ -495,4 +495,16 @@
<data name="OIDC" xml:space="preserve"> <data name="OIDC" xml:space="preserve">
<value>OpenID Connect (OIDC)</value> <value>OpenID Connect (OIDC)</value>
</data> </data>
<data name="SaveTokens.Text" xml:space="preserve">
<value>Save Tokens?</value>
</data>
<data name="SaveTokens.HelpText" xml:space="preserve">
<value>Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie.</value>
</data>
<data name="Success.DeleteUser" xml:space="preserve">
<value>User Deleted Successfully</value>
</data>
<data name="Error.DeleteUser" xml:space="preserve">
<value>Error Deleting User</value>
</data>
</root> </root>

View File

@ -127,7 +127,7 @@
<value>Error Loading Files</value> <value>Error Loading Files</value>
</data> </data>
<data name="Error.File.Upload" xml:space="preserve"> <data name="Error.File.Upload" xml:space="preserve">
<value>File Upload Failed Or Is Still In Progress</value> <value>File Upload Failed</value>
</data> </data>
<data name="Message.File.NotSelected" xml:space="preserve"> <data name="Message.File.NotSelected" xml:space="preserve">
<value>You Have Not Selected A File To Upload</value> <value>You Have Not Selected A File To Upload</value>

View File

@ -427,7 +427,7 @@
<value>At Least One Uppercase Letter</value> <value>At Least One Uppercase Letter</value>
</data> </data>
<data name="Password.ValidationCriteria" xml:space="preserve"> <data name="Password.ValidationCriteria" xml:space="preserve">
<value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site.</value> <value>Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site.</value>
</data> </data>
<data name="ProfileInvalid" xml:space="preserve"> <data name="ProfileInvalid" xml:space="preserve">
<value>{0} Is Not Valid</value> <value>{0} Is Not Valid</value>
@ -474,4 +474,7 @@
<data name="User" xml:space="preserve"> <data name="User" xml:space="preserve">
<value>User</value> <value>User</value>
</data> </data>
<data name="Path" xml:space="preserve">
<value>Path</value>
</data>
</root> </root>

View File

@ -56,10 +56,5 @@ namespace Oqtane.Services
{ {
await PostAsync($"{ApiUrl}/restart"); await PostAsync($"{ApiUrl}/restart");
} }
public async Task RegisterAsync(string email)
{
await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true);
}
} }
} }

View File

@ -34,13 +34,5 @@ namespace Oqtane.Services
/// </summary> /// </summary>
/// <returns>internal status/message object</returns> /// <returns>internal status/message object</returns>
Task RestartAsync(); Task RestartAsync();
/// <summary>
/// Registers a new <see cref="User"/>
/// </summary>
/// <param name="email">Email of the user to be registered</param>
/// <returns></returns>
Task RegisterAsync(string email);
} }
} }

View File

@ -131,6 +131,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))
{ {
page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId); page = await PageService.AddPageAsync(PageState.Page.PageId, PageState.User.UserId);
PageState.EditMode = true;
} }
if (_showEditMode) if (_showEditMode)
@ -153,7 +154,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))
{ {
NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString())); NavigationManager.NavigateTo(NavigateUrl(page.Path, "edit=" + PageState.EditMode.ToString().ToLower()));
} }
} }
} }

View File

@ -8,14 +8,14 @@
{ {
@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="@CssClass" @onclick="LogoutUser">@Localizer["Logout"]</button>
} }
else else
{ {
<form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm"> <form method="post" class="app-form-inline" action="@logouturl" @formname="LogoutForm">
<input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" /> <input type="hidden" name="@Constants.RequestVerificationToken" value="@SiteState.AntiForgeryToken" />
<input type="hidden" name="returnurl" value="@returnurl" /> <input type="hidden" name="returnurl" value="@returnurl" />
<button type="submit" class="btn btn-primary">@Localizer["Logout"]</button> <button type="submit" class="@CssClass">@Localizer["Logout"]</button>
</form> </form>
} }
} }
@ -23,7 +23,7 @@
{ {
@if (ShowLogin) @if (ShowLogin)
{ {
<a href="@loginurl" class="btn btn-primary">@SharedLocalizer["Login"]</a> <a href="@loginurl" class="@CssClass">@SharedLocalizer["Login"]</a>
} }
} }
</span> </span>
@ -32,4 +32,6 @@
{ {
[Parameter] [Parameter]
public bool ShowLogin { get; set; } = true; public bool ShowLogin { get; set; } = true;
[Parameter]
public string CssClass { get; set; } = "btn btn-primary";
} }

View File

@ -8,13 +8,13 @@
<span class="app-profile"> <span class="app-profile">
@if (PageState.User != null) @if (PageState.User != null)
{ {
<a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="btn btn-primary">@PageState.User.Username</a> <a href="@NavigateUrl("profile", "returnurl=" + _returnurl)" class="@CssClass">@PageState.User.Username</a>
} }
else else
{ {
@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="@CssClass">@Localizer["Register"]</a>
} }
} }
</span> </span>
@ -23,6 +23,8 @@
[Parameter] [Parameter]
public bool ShowRegister { get; set; } public bool ShowRegister { get; set; }
[Parameter]
public string CssClass { get; set; } = "btn btn-primary";
private string _returnurl = ""; private string _returnurl = "";

View File

@ -15,6 +15,8 @@ namespace Oqtane.Themes
{ {
public abstract class ThemeBase : ComponentBase, IThemeControl public abstract class ThemeBase : ComponentBase, IThemeControl
{ {
private bool _scriptsloaded = false;
[Inject] [Inject]
protected ILogService LoggingService { get; set; } protected ILogService LoggingService { get; set; }
@ -62,7 +64,7 @@ namespace Oqtane.Themes
var inline = 0; var inline = 0;
foreach (Resource resource in resources) foreach (Resource resource in resources)
{ {
if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive)
{ {
if (!string.IsNullOrEmpty(resource.Url)) if (!string.IsNullOrEmpty(resource.Url))
{ {
@ -82,6 +84,25 @@ namespace Oqtane.Themes
} }
} }
} }
_scriptsloaded = true;
}
public bool ScriptsLoaded
{
get
{
return _scriptsloaded;
}
}
// property for obtaining theme information about this theme component
public Theme ThemeState
{
get
{
var type = GetType().Namespace + ", " + GetType().Assembly.GetName().Name;
return PageState?.Site.Themes.FirstOrDefault(item => item.ThemeName == type);
}
} }
// path method // path method
@ -91,6 +112,15 @@ namespace Oqtane.Themes
return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/"; return PageState?.Alias.BaseUrl + "/Themes/" + GetType().Namespace + "/";
} }
// fingerprint hash code for static assets
public string Fingerprint
{
get
{
return ThemeState.Fingerprint;
}
}
// url methods // url methods
// navigate url // navigate url

View File

@ -70,7 +70,7 @@
if (!script.Contains("><") && !script.Contains("data-reload")) if (!script.Contains("><") && !script.Contains("data-reload"))
{ {
// add data-reload attribute to inline script // add data-reload attribute to inline script
headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"true\"")); headcontent = headcontent.Replace(script, script.Replace("<script", "<script data-reload=\"always\""));
} }
index = headcontent.IndexOf("<script", index + 1); index = headcontent.IndexOf("<script", index + 1);
} }

View File

@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Text.Json; using System.Text.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
namespace Oqtane.UI namespace Oqtane.UI
{ {
@ -209,17 +210,22 @@ namespace Oqtane.UI
} }
public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt)
{
UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1);
return Task.CompletedTask;
}
public ValueTask<bool> UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( return _jsRuntime.InvokeAsync<bool>(
"Oqtane.Interop.uploadFiles", "Oqtane.Interop.uploadFiles", cancellationToken,
posturl, folder, id, antiforgerytoken, jwt); posturl, folder, id, antiforgerytoken, jwt, chunksize);
return Task.CompletedTask;
} }
catch catch
{ {
return Task.CompletedTask; return new ValueTask<bool>(Task.FromResult(false));
} }
} }

View File

@ -20,7 +20,7 @@
@if (!string.IsNullOrEmpty(_error)) @if (!string.IsNullOrEmpty(_error))
{ {
<ModuleMessage Message="@_error" Type="@MessageType.Warning" /> <ModuleMessage Message="@_error" Type="@MessageType.Warning" />
} }
@DynamicComponent @DynamicComponent
@ -244,7 +244,9 @@
// look for personalized page // look for personalized page
if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList)) if (user != null && page.IsPersonalizable && !UserSecurity.IsAuthorized(user, PermissionNames.Edit, page.PermissionList))
{ {
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + user.Username, site.SiteId); var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.PageId}";
var path = (user.Settings.ContainsKey(settingName)) ? user.Settings[settingName] : Utilities.GetFriendlyUrl(user.Username);
var personalized = await PageService.GetPageAsync(route.PagePath + "/" + path, site.SiteId);
if (personalized != null) if (personalized != null)
{ {
// redirect to the personalized page // redirect to the personalized page
@ -389,7 +391,7 @@
if (themetype != null) if (themetype != null)
{ {
// get resources for theme (ITheme) // get resources for theme (ITheme)
page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName)); page.Resources = ManagePageResources(page.Resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint);
var themeobject = Activator.CreateInstance(themetype) as IThemeControl; var themeobject = Activator.CreateInstance(themetype) as IThemeControl;
if (themeobject != null) if (themeobject != null)
@ -399,7 +401,7 @@
panes = themeobject.Panes; panes = themeobject.Panes;
} }
// get resources for theme control // get resources for theme control
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.Fingerprint);
} }
} }
// theme settings components are dynamically loaded within the framework Page Management module // theme settings components are dynamically loaded within the framework Page Management module
@ -409,7 +411,7 @@
if (settingsType != null) if (settingsType != null)
{ {
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace); page.Resources = ManagePageResources(page.Resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint);
} }
} }
@ -453,7 +455,7 @@
if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime))) if (module.ModuleDefinition != null && (module.ModuleDefinition.Runtimes == "" || module.ModuleDefinition.Runtimes.Contains(Runtime)))
{ {
page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName)); page.Resources = ManagePageResources(page.Resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint);
// handle default action // handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -502,7 +504,7 @@
module.RenderMode = moduleobject.RenderMode; module.RenderMode = moduleobject.RenderMode;
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, module.ModuleDefinition?.Fingerprint);
// settings components are dynamically loaded within the framework Settings module // settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -523,7 +525,7 @@
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, module.ModuleDefinition?.Fingerprint);
} }
// container settings component // container settings component
@ -534,7 +536,7 @@
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, theme.Fingerprint);
} }
} }
} }
@ -593,7 +595,7 @@
return (page, modules); return (page, modules);
} }
private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name) private List<Resource> ManagePageResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint)
{ {
if (resources != null) if (resources != null)
{ {
@ -613,7 +615,7 @@
// ensure resource does not exist already // ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{ {
pageresources.Add(resource.Clone(level, name)); pageresources.Add(resource.Clone(level, name, fingerprint));
} }
} }
} }

View File

@ -11,8 +11,6 @@
RenderFragment DynamicComponent { get; set; } RenderFragment DynamicComponent { get; set; }
private string lastPagePath = "";
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
// handle page redirection // handle page redirection
@ -92,8 +90,9 @@
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (!firstRender && PageState.Page.Path != lastPagePath) if (!firstRender)
{ {
// site content
if (!string.IsNullOrEmpty(PageState.Site.HeadContent) && PageState.Site.HeadContent.Contains("<script")) if (!string.IsNullOrEmpty(PageState.Site.HeadContent) && PageState.Site.HeadContent.Contains("<script"))
{ {
await InjectScripts(PageState.Site.HeadContent, ResourceLocation.Head); await InjectScripts(PageState.Site.HeadContent, ResourceLocation.Head);
@ -102,6 +101,7 @@
{ {
await InjectScripts(PageState.Site.BodyContent, ResourceLocation.Body); await InjectScripts(PageState.Site.BodyContent, ResourceLocation.Body);
} }
// page content
if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script")) if (!string.IsNullOrEmpty(PageState.Page.HeadContent) && PageState.Page.HeadContent.Contains("<script"))
{ {
await InjectScripts(PageState.Page.HeadContent, ResourceLocation.Head); await InjectScripts(PageState.Page.HeadContent, ResourceLocation.Head);
@ -110,7 +110,6 @@
{ {
await InjectScripts(PageState.Page.BodyContent, ResourceLocation.Body); await InjectScripts(PageState.Page.BodyContent, ResourceLocation.Body);
} }
lastPagePath = PageState.Page.Path;
} }
// style sheets // style sheets
@ -191,16 +190,13 @@
} }
else else
{ {
if (dataAttributes == null || !dataAttributes.ContainsKey("data-reload") || dataAttributes["data-reload"] != "false") if (id == "")
{ {
if (id == "") count += 1;
{ id = $"page{PageState.Page.PageId}-script{count}";
count += 1;
id = $"page{PageState.Page.PageId}-script{count}";
}
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
} }
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
} }
index = content.IndexOf("<script", index + 1); index = content.IndexOf("<script", index + 1);
} }

View File

@ -1,9 +1,9 @@
using System.Data; using System.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations.Operations; using Microsoft.EntityFrameworkCore.Migrations.Operations;
using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders; using Microsoft.EntityFrameworkCore.Migrations.Operations.Builders;
using MySql.Data.MySqlClient; using MySql.Data.MySqlClient;
using MySql.EntityFrameworkCore.Metadata;
using Oqtane.Databases; using Oqtane.Databases;
namespace Oqtane.Database.MySQL namespace Oqtane.Database.MySQL
@ -21,11 +21,11 @@ namespace Oqtane.Database.MySQL
public MySQLDatabase() :base(_name, _friendlyName) { } public MySQLDatabase() :base(_name, _friendlyName) { }
public override string Provider => "MySql.EntityFrameworkCore"; public override string Provider => "Pomelo.EntityFrameworkCore.MySql";
public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name) public override OperationBuilder<AddColumnOperation> AddAutoIncrementColumn(ColumnsBuilder table, string name)
{ {
return table.Column<int>(name: name, nullable: false).Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn); return table.Column<int>(name: name, nullable: false).Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
} }
public override string ConcatenateSql(params string[] values) public override string ConcatenateSql(params string[] values)
@ -86,7 +86,7 @@ namespace Oqtane.Database.MySQL
public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString) public override DbContextOptionsBuilder UseDatabase(DbContextOptionsBuilder optionsBuilder, string connectionString)
{ {
return optionsBuilder.UseMySQL(connectionString); return optionsBuilder.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString));
} }
private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query) private void PrepareCommand(MySqlConnection conn, MySqlCommand cmd, string query)

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,8 +33,8 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.0-preview" /> <PackageReference Include="MySql.Data" Version="9.2.0" />
<PackageReference Include="MySql.Data" Version="9.1.0" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.2.efcore.9.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -42,7 +42,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" /> <MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)Pomelo.EntityFrameworkCore.MySql.dll;$(OutputPath)MySql.Data.dll" DestinationPath="..\Oqtane.Server\bin\$(Configuration)\net9.0\%(Filename)%(Extension)" />
</ItemGroup> </ItemGroup>
<Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')"> <Target Name="PublishProvider" AfterTargets="PostBuildEvent" Inputs="@(MySQLFiles)" Outputs="@(MySQLFiles->'%(DestinationPath)')">

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -34,8 +34,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" /> <PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.1" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -10,7 +10,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
@ -33,7 +33,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -6,7 +6,7 @@
<!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> --> <!-- <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks> -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> --> <!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -14,7 +14,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane.Maui</RootNamespace> <RootNamespace>Oqtane.Maui</RootNamespace>
@ -30,7 +30,7 @@
<ApplicationId>com.oqtane.maui</ApplicationId> <ApplicationId>com.oqtane.maui</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>6.0.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>6.1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion> <ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged --> <!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
@ -67,14 +67,14 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="9.0.1" />
<PackageReference Include="Microsoft.Maui.Controls" Version="9.0.0" /> <PackageReference Include="Microsoft.Maui.Controls" Version="9.0.30" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.0" /> <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.30" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.30" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -120,13 +120,22 @@ Oqtane.Interop = {
this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore);
} }
}, },
includeScript: function (id, src, integrity, crossorigin, type, content, location) { includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) {
var script; var script;
if (src !== "") { if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]");
} }
else { else {
script = document.getElementById(id); if (id !== "") {
script = document.getElementById(id);
} else {
const scripts = document.querySelectorAll("script:not([src])");
for (let i = 0; i < scripts.length; i++) {
if (scripts[i].textContent.includes(content)) {
script = scripts[i];
}
}
}
} }
if (script !== null) { if (script !== null) {
script.remove(); script.remove();
@ -152,37 +161,36 @@ Oqtane.Interop = {
else { else {
script.innerHTML = content; script.innerHTML = content;
} }
script.async = false; if (dataAttributes !== null) {
this.addScript(script, location) for (var key in dataAttributes) {
.then(() => { script.setAttribute(key, dataAttributes[key]);
if (src !== "") { }
console.log(src + ' loaded'); }
}
else { try {
console.log(id + ' loaded'); this.addScript(script, location);
} } catch (error) {
}) if (src !== "") {
.catch(() => { console.error("Failed to load external script: ${src}", error);
if (src !== "") { } else {
console.error(src + ' failed'); console.error("Failed to load inline script: ${content}", error);
} }
else { }
console.error(id + ' failed');
}
});
} }
}, },
addScript: function (script, location) { addScript: function (script, location) {
if (location === 'head') { return new Promise((resolve, reject) => {
document.head.appendChild(script); script.async = false;
} script.defer = false;
if (location === 'body') {
document.body.appendChild(script);
}
return new Promise((res, rej) => { script.onload = () => resolve();
script.onload = res(); script.onerror = (error) => reject(error);
script.onerror = rej();
if (location === 'head') {
document.head.appendChild(script);
} else {
document.body.appendChild(script);
}
}); });
}, },
includeScripts: async function (scripts) { includeScripts: async function (scripts) {
@ -222,10 +230,10 @@ Oqtane.Interop = {
if (scripts[s].crossorigin !== '') { if (scripts[s].crossorigin !== '') {
element.crossOrigin = scripts[s].crossorigin; element.crossOrigin = scripts[s].crossorigin;
} }
if (scripts[s].es6module === true) { if (scripts[s].type !== '') {
element.type = "module"; element.type = scripts[s].type;
} }
if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) { if (scripts[s].dataAttributes !== null) {
for (var key in scripts[s].dataAttributes) { for (var key in scripts[s].dataAttributes) {
element.setAttribute(key, scripts[s].dataAttributes[key]); element.setAttribute(key, scripts[s].dataAttributes[key]);
} }
@ -300,97 +308,107 @@ Oqtane.Interop = {
} }
return files; return files;
}, },
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
var success = true;
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id); var progressbar = document.getElementById('ProgressBar_' + id);
var totalSize = 0;
for (var i = 0; i < fileinput.files.length; i++) {
totalSize += fileinput.files[i].size;
}
let uploadSize = 0;
if (!chunksize || chunksize < 1) {
chunksize = 1; // 1 MB default
}
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;"); progressinfo.setAttribute('style', 'display: inline;');
progressinfo.innerHTML = ''; if (fileinput.files.length > 1) {
progressbar.setAttribute("style", "width: 100%; display: inline;"); progressinfo.innerHTML = fileinput.files[0].name + ', ...';
}
else {
progressinfo.innerHTML = fileinput.files[0].name;
}
progressbar.setAttribute('style', 'width: 100%; display: inline;');
progressbar.value = 0; progressbar.value = 0;
} }
var files = fileinput.files; const uploadFile = (file) => {
var totalSize = 0; const chunkSize = chunksize * (1024 * 1024);
for (var i = 0; i < files.length; i++) { const totalParts = Math.ceil(file.size / chunkSize);
totalSize = totalSize + files[i].size; let partCount = 0;
const uploadPart = () => {
const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name);
var credentials = 'same-origin';
var headers = new Headers();
headers.append('PartCount', partCount + 1);
headers.append('TotalParts', totalParts);
if (jwt !== "") {
headers.append('Authorization', 'Bearer ' + jwt);
credentials = 'include';
}
return fetch(posturl, {
method: 'POST',
headers: headers,
credentials: credentials,
body: formdata
})
.then(response => {
if (!response.ok) {
if (progressinfo !== null) {
progressinfo.innerHTML = 'Error: ' + response.statusText;
}
throw new Error('Failed');
}
return;
})
.then(data => {
partCount++;
if (progressbar !== null) {
uploadSize += chunk.size;
var percent = Math.ceil((uploadSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
if (partCount < totalParts) {
uploadPart().then(resolve).catch(reject);
}
else {
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
};
return uploadPart();
};
try {
for (const file of fileinput.files) {
await uploadFile(file);
}
} catch (error) {
success = false;
} }
var maxChunkSizeMB = 1; fileinput.value = '';
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024); return success;
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
}
var totalParts = fileChunk.length;
var partCount = 0;
while (chunk = fileChunk.shift()) {
partCount++;
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder);
data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
if (jwt !== "") {
request.setRequestHeader('Authorization', 'Bearer ' + jwt);
request.withCredentials = true;
}
request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
if (files.length === 1) {
progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
}
};
request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) {
uploadedSize = uploadedSize + e.total;
var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) {
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
}
};
request.send(data);
}
if (i === files.length - 1) {
fileinput.value = '';
}
}
}, },
refreshBrowser: function (verify, wait) { refreshBrowser: function (verify, wait) {
async function attemptReload (verify) { async function attemptReload (verify) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ if "%%~nxi" == "%%j" set /A found=1
) )
if not !found! == 1 rmdir /Q/S "%%i" if not !found! == 1 rmdir /Q/S "%%i"
) )
del "..\Oqtane.Server\bin\Release\net9.0\publish\Oqtane.Server.staticwebassets.endpoints.json"
del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json" del "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.json"
ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json" ren "..\Oqtane.Server\bin\Release\net9.0\publish\appsettings.release.json" "appsettings.json"
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1" C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe ".\install.ps1"

View File

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

View File

@ -7,6 +7,7 @@
@using Microsoft.AspNetCore.Localization @using Microsoft.AspNetCore.Localization
@using Microsoft.Net.Http.Headers @using Microsoft.Net.Http.Headers
@using Microsoft.Extensions.Primitives @using Microsoft.Extensions.Primitives
@using Microsoft.AspNetCore.Authentication
@using Oqtane.Client @using Oqtane.Client
@using Oqtane.UI @using Oqtane.UI
@using Oqtane.Repository @using Oqtane.Repository
@ -39,7 +40,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/" /> <base href="/" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="css/app.css?v=@_fingerprint" />
@if (_scripts.Contains("PWA Manifest")) @if (_scripts.Contains("PWA Manifest"))
{ {
<link id="app-manifest" rel="manifest" /> <link id="app-manifest" rel="manifest" />
@ -70,15 +71,15 @@
} }
<script src="_framework/blazor.web.js"></script> <script src="_framework/blazor.web.js"></script>
<script src="js/app.js"></script> <script src="js/app.js?v=@_fingerprint"></script>
<script src="js/loadjs.min.js"></script> <script src="js/loadjs.min.js?v=@_fingerprint"></script>
<script src="js/interop.js"></script> <script src="js/interop.js?v=@_fingerprint"></script>
@((MarkupString)_scripts) @((MarkupString)_scripts)
@((MarkupString)_bodyResources) @((MarkupString)_bodyResources)
@if (_renderMode == RenderModes.Static) @if (_renderMode == RenderModes.Static)
{ {
<page-script src="./js/reload.js"></page-script> <page-script src="./js/reload.js?v=@_fingerprint"></page-script>
} }
} }
else else
@ -94,6 +95,7 @@
private string _renderMode = RenderModes.Interactive; private string _renderMode = RenderModes.Interactive;
private string _runtime = Runtimes.Server; private string _runtime = Runtimes.Server;
private bool _prerender = true; private bool _prerender = true;
private string _fingerprint = "";
private int _visitorId = -1; private int _visitorId = -1;
private string _antiForgeryToken = ""; private string _antiForgeryToken = "";
private string _remoteIPAddress = ""; private string _remoteIPAddress = "";
@ -136,6 +138,8 @@
_renderMode = site.RenderMode; _renderMode = site.RenderMode;
_runtime = site.Runtime; _runtime = site.Runtime;
_prerender = site.Prerender; _prerender = site.Prerender;
_fingerprint = site.Fingerprint;
var modules = new List<Module>(); var modules = new List<Module>();
Route route = new Route(url, alias.Path); Route route = new Route(url, alias.Path);
@ -174,7 +178,7 @@
// get jwt token for downstream APIs // get jwt token for downstream APIs
if (Context.User.Identity.IsAuthenticated) if (Context.User.Identity.IsAuthenticated)
{ {
CreateJwtToken(alias); await GetJwtToken(alias);
} }
// includes resources // includes resources
@ -441,13 +445,19 @@
} }
} }
private void CreateJwtToken(Alias alias) private async Task GetJwtToken(Alias alias)
{ {
var sitesettings = Context.GetSiteSettings(); // bearer token may have been provided by remote Identity Provider and persisted using SaveTokens = true
var secret = sitesettings.GetValue("JwtOptions:Secret", ""); _authorizationToken = await Context.GetTokenAsync("access_token");
if (!string.IsNullOrEmpty(secret)) if (string.IsNullOrEmpty(_authorizationToken))
{ {
_authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); // generate bearer token if a secret has been configured in User Settings
var sitesettings = Context.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret))
{
_authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
}
} }
} }
@ -514,7 +524,7 @@
private void AddScript(Resource resource, Alias alias) private void AddScript(Resource resource, Alias alias)
{ {
var script = CreateScript(resource, alias); var script = CreateScript(resource, alias);
if (resource.Location == Shared.ResourceLocation.Head && !resource.Reload) if (resource.Location == Shared.ResourceLocation.Head && resource.LoadBehavior != ResourceLoadBehavior.BlazorPageScript)
{ {
if (!_headResources.Contains(script)) if (!_headResources.Contains(script))
{ {
@ -532,11 +542,27 @@
private string CreateScript(Resource resource, Alias alias) private string CreateScript(Resource resource, Alias alias)
{ {
if (!resource.Reload) if (resource.LoadBehavior == ResourceLoadBehavior.BlazorPageScript)
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
else
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = ""; var dataAttributes = "";
if (!resource.DataAttributes.ContainsKey("data-reload"))
{
switch (resource.LoadBehavior)
{
case ResourceLoadBehavior.Once:
dataAttributes += " data-reload=\"once\"";
break;
case ResourceLoadBehavior.Always:
dataAttributes += " data-reload=\"always\"";
break;
}
}
if (resource.DataAttributes != null && resource.DataAttributes.Count > 0) if (resource.DataAttributes != null && resource.DataAttributes.Count > 0)
{ {
foreach (var attribute in resource.DataAttributes) foreach (var attribute in resource.DataAttributes)
@ -552,10 +578,6 @@
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>"; "></script>";
} }
else
{
return "<page-script src=\"" + resource.Url + "\"></page-script>";
}
} }
private void SetLocalizationCookie(string cookieValue) private void SetLocalizationCookie(string cookieValue)
@ -583,13 +605,13 @@
var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType)); var theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == themeType));
if (theme != null) if (theme != null)
{ {
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
} }
else else
{ {
// fallback to default Oqtane theme // fallback to default Oqtane theme
theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme)); theme = site.Themes.FirstOrDefault(item => item.Themes.Any(item => item.TypeName == Constants.DefaultTheme));
resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), site.RenderMode); resources = AddResources(resources, theme.Resources, ResourceLevel.Page, alias, "Themes", Utilities.GetTypeName(theme.ThemeName), theme.Fingerprint, site.RenderMode);
} }
var type = Type.GetType(themeType); var type = Type.GetType(themeType);
if (type != null) if (type != null)
@ -597,7 +619,7 @@
var obj = Activator.CreateInstance(type) as IThemeControl; var obj = Activator.CreateInstance(type) as IThemeControl;
if (obj != null) if (obj != null)
{ {
resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, site.RenderMode); resources = AddResources(resources, obj.Resources, ResourceLevel.Page, alias, "Themes", type.Namespace, theme.Fingerprint, site.RenderMode);
} }
} }
// theme settings components are dynamically loaded within the framework Page Management module // theme settings components are dynamically loaded within the framework Page Management module
@ -607,7 +629,7 @@
if (settingsType != null) if (settingsType != null)
{ {
var objSettings = Activator.CreateInstance(settingsType) as IModuleControl; var objSettings = Activator.CreateInstance(settingsType) as IModuleControl;
resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, site.RenderMode); resources = AddResources(resources, objSettings.Resources, ResourceLevel.Module, alias, "Modules", settingsType.Namespace, theme.Fingerprint, site.RenderMode);
} }
} }
@ -616,7 +638,7 @@
var typename = ""; var typename = "";
if (module.ModuleDefinition != null) if (module.ModuleDefinition != null)
{ {
resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); resources = AddResources(resources, module.ModuleDefinition.Resources, ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode);
// handle default action // handle default action
if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction)) if (action == Constants.DefaultAction && !string.IsNullOrEmpty(module.ModuleDefinition.DefaultAction))
@ -662,7 +684,7 @@
var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; var moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
if (moduleobject != null) if (moduleobject != null)
{ {
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode);
// settings components are dynamically loaded within the framework Settings module // settings components are dynamically loaded within the framework Settings module
if (action.ToLower() == "settings" && module.ModuleDefinition != null) if (action.ToLower() == "settings" && module.ModuleDefinition != null)
@ -683,7 +705,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, module.ModuleDefinition?.Fingerprint, site.RenderMode);
} }
// container settings component // container settings component
@ -693,7 +715,7 @@
if (moduletype != null) if (moduletype != null)
{ {
moduleobject = Activator.CreateInstance(moduletype) as IModuleControl; moduleobject = Activator.CreateInstance(moduletype) as IModuleControl;
resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, site.RenderMode); resources = AddResources(resources, moduleobject.Resources, ResourceLevel.Module, alias, "Modules", moduletype.Namespace, theme.Fingerprint, site.RenderMode);
} }
} }
} }
@ -709,7 +731,7 @@
{ {
if (module.ModuleDefinition?.Resources != null) if (module.ModuleDefinition?.Resources != null)
{ {
resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), site.RenderMode); resources = AddResources(resources, module.ModuleDefinition.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Site).ToList(), ResourceLevel.Module, alias, "Modules", Utilities.GetTypeName(module.ModuleDefinition.ModuleDefinitionName), module.ModuleDefinition.Fingerprint, site.RenderMode);
} }
} }
} }
@ -717,7 +739,7 @@
return resources; return resources;
} }
private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string rendermode) private List<Resource> AddResources(List<Resource> pageresources, List<Resource> resources, ResourceLevel level, Alias alias, string type, string name, string fingerprint, string rendermode)
{ {
if (resources != null) if (resources != null)
{ {
@ -737,7 +759,7 @@
// ensure resource does not exist already // ensure resource does not exist already
if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower()))
{ {
pageresources.Add(resource.Clone(level, name)); pageresources.Add(resource.Clone(level, name, fingerprint));
} }
} }
} }

View File

@ -21,6 +21,8 @@ using System.Net.Http;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using System.IO.Compression; using System.IO.Compression;
using Oqtane.Services; using Oqtane.Services;
using Microsoft.Extensions.Primitives;
using Microsoft.AspNetCore.Http.HttpResults;
// ReSharper disable StringIndexOfIsCultureSpecific.1 // ReSharper disable StringIndexOfIsCultureSpecific.1
@ -427,75 +429,98 @@ namespace Oqtane.Controllers
// POST api/<controller>/upload // POST api/<controller>/upload
[EnableCors(Constants.MauiCorsPolicy)] [EnableCors(Constants.MauiCorsPolicy)]
[HttpPost("upload")] [HttpPost("upload")]
public async Task<IActionResult> UploadFile(string folder, IFormFile formfile) public async Task<IActionResult> UploadFile([FromForm] string folder, IFormFile formfile)
{ {
if (string.IsNullOrEmpty(folder))
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A Folder");
return StatusCode((int)HttpStatusCode.Forbidden);
}
if (formfile == null || formfile.Length <= 0) if (formfile == null || formfile.Length <= 0)
{ {
return NoContent(); _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Does Not Contain A File");
return StatusCode((int)HttpStatusCode.Forbidden);
} }
// ensure filename is valid // ensure filename is valid
string token = ".part_"; if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName))
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 Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName);
return NoContent(); return StatusCode((int)HttpStatusCode.Forbidden);
} }
// ensure headers exist
if (!Request.Headers.TryGetValue("PartCount", out StringValues partcount) || !int.TryParse(partcount, out int partCount) || partCount <= 0 ||
!Request.Headers.TryGetValue("TotalParts", out StringValues totalparts) || !int.TryParse(totalparts, out int totalParts) || totalParts <= 0)
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload Is Missing Required Headers");
return StatusCode((int)HttpStatusCode.Forbidden);
}
// create file name using header values
string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000");
string folderPath = ""; string folderPath = "";
int FolderId; try
if (int.TryParse(folder, out FolderId))
{ {
Folder Folder = _folders.GetFolder(FolderId); int FolderId;
if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList)) if (int.TryParse(folder, out FolderId))
{ {
folderPath = _folders.GetFolderPath(Folder); Folder Folder = _folders.GetFolder(FolderId);
} if (Folder != null && Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Edit, Folder.PermissionList))
}
else
{
FolderId = -1;
if (User.IsInRole(RoleNames.Host))
{
folderPath = GetFolderPath(folder);
}
}
if (!string.IsNullOrEmpty(folderPath))
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, formfile.FileName), FileMode.Create))
{
await formfile.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, formfile.FileName);
if (upload != "" && FolderId != -1)
{
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
if (file != null)
{ {
if (file.FileId == 0) folderPath = _folders.GetFolderPath(Folder);
{ }
file = _files.AddFile(file); }
} else
else {
{ FolderId = -1;
file = _files.UpdateFile(file); if (User.IsInRole(RoleNames.Host))
} {
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload)); folderPath = GetFolderPath(folder);
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
} }
} }
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
return NoContent(); if (!string.IsNullOrEmpty(folderPath))
{
CreateDirectory(folderPath);
using (var stream = new FileStream(Path.Combine(folderPath, fileName), FileMode.Create))
{
await formfile.CopyToAsync(stream);
}
string upload = await MergeFile(folderPath, fileName);
if (upload != "" && FolderId != -1)
{
var file = CreateFile(upload, FolderId, Path.Combine(folderPath, upload));
if (file != null)
{
if (file.FileId == 0)
{
file = _files.AddFile(file);
}
else
{
file = _files.UpdateFile(file);
}
_logger.Log(LogLevel.Information, this, LogFunction.Create, "File Uploaded {File}", Path.Combine(folderPath, upload));
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, SyncEventActions.Create);
}
}
return NoContent();
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Upload Attempt {Folder} {File}", folder, formfile.FileName);
return StatusCode((int)HttpStatusCode.Forbidden);
}
}
catch (Exception ex)
{
_logger.Log(LogLevel.Error, this, LogFunction.Create, ex, "File Upload Attempt Failed {Folder} {File}", folder, formfile.FileName);
return StatusCode((int)HttpStatusCode.InternalServerError);
}
} }
private async Task<string> MergeFile(string folder, string filename) private async Task<string> MergeFile(string folder, string filename)
@ -510,10 +535,10 @@ namespace Oqtane.Controllers
filename = Path.GetFileNameWithoutExtension(filename); // base filename filename = Path.GetFileNameWithoutExtension(filename); // base filename
string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts
// if all of the file parts exist ( note that file parts can arrive out of order ) // if all of the file parts exist (note that file parts can arrive out of order)
if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) if (fileparts.Length == totalparts && CanAccessFiles(fileparts))
{ {
// merge file parts into temp file ( in case another user is trying to get the file ) // merge file parts into temp file (in case another user is trying to get the file)
bool success = true; bool success = true;
using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create))
{ {
@ -536,17 +561,23 @@ namespace Oqtane.Controllers
// clean up file parts // clean up file parts
foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) foreach (var file in Directory.GetFiles(folder, "*" + token + "*"))
{ {
// file name matches part or is more than 2 hours old (ie. a prior file upload failed) if (fileparts.Contains(file))
if (fileparts.Contains(file) || System.IO.File.GetCreationTime(file).ToUniversalTime() < DateTime.UtcNow.AddHours(-2))
{ {
System.IO.File.Delete(file); try
{
System.IO.File.Delete(file);
}
catch
{
// unable to delete part - ignore
}
} }
} }
// rename temp file // rename temp file
if (success) if (success)
{ {
// remove file if it already exists (as well as any thumbnails) // remove file if it already exists (as well as any thumbnails which may exist)
foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*"))
{ {
if (Path.GetExtension(file) != ".tmp") if (Path.GetExtension(file) != ".tmp")

View File

@ -60,9 +60,9 @@ namespace Oqtane.Controllers
{ {
installation = _databaseManager.Install(config); installation = _databaseManager.Install(config);
if (installation.Success && config.Register) if (installation.Success)
{ {
await RegisterContact(config.HostEmail); await RegisterContact(config.HostEmail, config.HostName, config.Register);
} }
} }
else else
@ -257,7 +257,7 @@ namespace Oqtane.Controllers
} }
} }
private async Task RegisterContact(string email) private async Task RegisterContact(string email, string name, bool register)
{ {
try try
{ {
@ -268,7 +268,7 @@ namespace Oqtane.Controllers
{ {
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}")).ConfigureAwait(false); var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}&register={register.ToString().ToLower()}")).ConfigureAwait(false);
} }
} }
} }
@ -278,14 +278,6 @@ namespace Oqtane.Controllers
} }
} }
// GET api/<controller>/register?email=x
[HttpPost("register")]
[Authorize(Roles = RoleNames.Host)]
public async Task Register(string email)
{
await RegisterContact(email);
}
public struct ClientAssembly public struct ClientAssembly
{ {
public ClientAssembly(string filepath, bool hashfilename) public ClientAssembly(string filepath, bool hashfilename)
@ -294,7 +286,7 @@ namespace Oqtane.Controllers
DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath); DateTime lastwritetime = System.IO.File.GetLastWriteTime(filepath);
if (hashfilename) if (hashfilename)
{ {
HashedName = GetDeterministicHashCode(filepath).ToString("X8") + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath); HashedName = Utilities.GenerateSimpleHash(filepath) + "." + lastwritetime.ToString("yyyyMMddHHmmss") + Path.GetExtension(filepath);
} }
else else
{ {
@ -305,25 +297,5 @@ namespace Oqtane.Controllers
public string FilePath { get; private set; } public string FilePath { get; private set; }
public string HashedName { get; private set; } public string HashedName { get; private set; }
} }
private static int GetDeterministicHashCode(string value)
{
unchecked
{
int hash1 = (5381 << 16) + 5381;
int hash2 = hash1;
for (int i = 0; i < value.Length; i += 2)
{
hash1 = ((hash1 << 5) + hash1) ^ value[i];
if (i == value.Length - 1)
break;
hash2 = ((hash2 << 5) + hash2) ^ value[i + 1];
}
return hash1 + (hash2 * 1566083941);
}
}
} }
} }

View File

@ -155,7 +155,7 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)] [Authorize(Roles = RoleNames.Registered)]
public Notification Post([FromBody] Notification notification) public Notification Post([FromBody] Notification notification)
{ {
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && IsAuthorized(notification.FromUserId)) if (ModelState.IsValid && notification.SiteId == _alias.SiteId && (IsAuthorized(notification.FromUserId) || (notification.FromUserId == null && User.IsInRole(RoleNames.Admin))))
{ {
if (!User.IsInRole(RoleNames.Admin)) if (!User.IsInRole(RoleNames.Admin))
{ {
@ -181,17 +181,45 @@ namespace Oqtane.Controllers
[Authorize(Roles = RoleNames.Registered)] [Authorize(Roles = RoleNames.Registered)]
public Notification Put(int id, [FromBody] Notification notification) public Notification Put(int id, [FromBody] Notification notification)
{ {
if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null && (IsAuthorized(notification.FromUserId) || IsAuthorized(notification.ToUserId))) if (ModelState.IsValid && notification.SiteId == _alias.SiteId && notification.NotificationId == id && _notifications.GetNotification(notification.NotificationId, false) != null)
{ {
if (!User.IsInRole(RoleNames.Admin) && notification.FromUserId != null) bool update = false;
if (IsAuthorized(notification.FromUserId))
{ {
// content must be HTML encoded for non-admins to prevent HTML injection // notification belongs to current authenticated user - update is allowed
notification.Subject = WebUtility.HtmlEncode(notification.Subject); if (!User.IsInRole(RoleNames.Admin))
notification.Body = WebUtility.HtmlEncode(notification.Body); {
// content must be HTML encoded for non-admins to prevent HTML injection
notification.Subject = WebUtility.HtmlEncode(notification.Subject);
notification.Body = WebUtility.HtmlEncode(notification.Body);
}
update = true;
}
else
{
if (IsAuthorized(notification.ToUserId))
{
// notification was sent to current authenticated user - only isread and isdeleted properties can be updated
var isread = notification.IsRead;
var isdeleted = notification.IsDeleted;
notification = _notifications.GetNotification(notification.NotificationId);
notification.IsRead = isread;
notification.IsDeleted = isdeleted;
update = true;
}
}
if (update)
{
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Put Attempt {Notification}", notification);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
notification = null;
} }
notification = _notifications.UpdateNotification(notification);
_syncManager.AddSyncEvent(_alias, EntityNames.Notification, notification.NotificationId, SyncEventActions.Update);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Notification Updated {NotificationId}", notification.NotificationId);
} }
else else
{ {

View File

@ -12,6 +12,8 @@ using Oqtane.Infrastructure;
using Oqtane.Enums; using Oqtane.Enums;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Text.Json; using System.Text.Json;
using Oqtane.Managers;
using System.Net;
// ReSharper disable PartialTypeWithSinglePart // ReSharper disable PartialTypeWithSinglePart
namespace Oqtane.Controllers namespace Oqtane.Controllers
@ -20,13 +22,15 @@ namespace Oqtane.Controllers
public class PackageController : Controller public class PackageController : Controller
{ {
private readonly IInstallationManager _installationManager; private readonly IInstallationManager _installationManager;
private readonly IUserManager _userManager;
private readonly IWebHostEnvironment _environment; private readonly IWebHostEnvironment _environment;
private readonly IConfigManager _configManager; private readonly IConfigManager _configManager;
private readonly ILogManager _logger; private readonly ILogManager _logger;
public PackageController(IInstallationManager installationManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger) public PackageController(IInstallationManager installationManager, IUserManager userManager, IWebHostEnvironment environment, IConfigManager configManager, ILogManager logger)
{ {
_installationManager = installationManager; _installationManager = installationManager;
_userManager = userManager;
_environment = environment; _environment = environment;
_configManager = configManager; _configManager = configManager;
_logger = logger; _logger = logger;
@ -45,7 +49,7 @@ namespace Oqtane.Controllers
{ {
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}"); packages = await GetJson<List<Package>>(client, url + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={WebUtility.UrlEncode(search)}&price={price}&package={package}&sort={sort}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
} }
} }
return packages; return packages;
@ -64,7 +68,7 @@ namespace Oqtane.Controllers
{ {
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
packages = await GetJson<List<Package>>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}"); packages = await GetJson<List<Package>>(client, url + $"/api/registry/updates/?id={_configManager.GetInstallationId()}&version={Constants.Version}&type={type}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
} }
} }
return packages; return packages;
@ -83,7 +87,7 @@ namespace Oqtane.Controllers
{ {
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
package = await GetJson<Package>(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}"); package = await GetJson<Package>(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}");
} }
if (package != null) if (package != null)
@ -117,6 +121,24 @@ namespace Oqtane.Controllers
return package; return package;
} }
private string GetPackageRegistryEmail()
{
var email = _configManager.GetSetting("PackageRegistryEmail", "");
if (string.IsNullOrEmpty(email))
{
if (User.Identity.IsAuthenticated)
{
var user = _userManager.GetUser(User.Identity.Name, -1);
if (user != null)
{
email = user.Email;
_configManager.AddOrUpdateSetting("PackageRegistryEmail", email, true);
}
}
}
return email;
}
private async Task<T> GetJson<T>(HttpClient httpClient, string url) private async Task<T> GetJson<T>(HttpClient httpClient, string url)
{ {
try try

View File

@ -9,7 +9,8 @@ using System.Net;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using System; using System.Xml.Linq;
using Microsoft.AspNetCore.Diagnostics;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -189,15 +190,16 @@ namespace Oqtane.Controllers
User user = _userPermissions.GetUser(User); User user = _userPermissions.GetUser(User);
if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid)) if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid))
{ {
page = _pages.GetPage(parent.Path + "/" + user.Username, parent.SiteId); var path = Utilities.GetFriendlyUrl(user.Username);
page = _pages.GetPage(parent.Path + "/" + path, parent.SiteId);
if (page == null) if (page == null)
{ {
page = new Page(); page = new Page();
page.SiteId = parent.SiteId; page.SiteId = parent.SiteId;
page.ParentId = parent.PageId; page.ParentId = parent.PageId;
page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username; page.Name = user.Username;
page.Path = parent.Path + "/" + user.Username; page.Path = parent.Path + "/" + path;
page.Title = page.Name + " - " + parent.Name; page.Title = ((!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username) + " - " + parent.Name;
page.Order = 0; page.Order = 0;
page.IsNavigation = false; page.IsNavigation = false;
page.Url = ""; page.Url = "";
@ -250,6 +252,11 @@ namespace Oqtane.Controllers
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Create); _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Create);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// set user personalized page path
var setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = $"PersonalizedPagePath:{page.SiteId}:{parent.PageId}", SettingValue = path, IsPrivate = false };
_settings.AddSetting(setting);
_syncManager.AddSyncEvent(_alias, EntityNames.User, user.UserId, SyncEventActions.Update);
} }
} }
else else
@ -274,18 +281,14 @@ namespace Oqtane.Controllers
// get current page permissions // get current page permissions
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList(); var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();
page = _pages.UpdatePage(page); // preserve new path and deleted status
var newPath = page.Path;
var deleted = page.IsDeleted;
page.Path = currentPage.Path;
page.IsDeleted = currentPage.IsDeleted;
// save url mapping if page path changed // update page
if (currentPage.Path != page.Path) UpdatePage(page, page.PageId, page.Path, newPath, deleted);
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
if (urlMapping != null)
{
urlMapping.MappedUrl = page.Path;
_urlMappings.UpdateUrlMapping(urlMapping);
}
}
// get differences between current and new page permissions // get differences between current and new page permissions
var added = GetPermissionsDifferences(page.PermissionList, currentPermissions); var added = GetPermissionsDifferences(page.PermissionList, currentPermissions);
@ -315,6 +318,7 @@ namespace Oqtane.Controllers
}); });
} }
} }
// permissions removed // permissions removed
foreach (Permission permission in removed) foreach (Permission permission in removed)
{ {
@ -338,8 +342,29 @@ namespace Oqtane.Controllers
} }
} }
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// personalized page
if (page.UserId != null && currentPage.Path != page.Path)
{
// set user personalized page path
var settingName = $"PersonalizedPagePath:{page.SiteId}:{page.ParentId}";
var path = page.Path.Substring(page.Path.LastIndexOf("/") + 1);
var settings = _settings.GetSettings(EntityNames.User, page.UserId.Value).ToList();
var setting = settings.FirstOrDefault(item => item.SettingName == settingName);
if (setting == null)
{
setting = new Setting { EntityName = EntityNames.User, EntityId = page.UserId.Value, SettingName = settingName, SettingValue = path, IsPrivate = false };
_settings.AddSetting(setting);
}
else
{
setting.SettingValue = path;
_settings.UpdateSetting(setting);
}
_syncManager.AddSyncEvent(_alias, EntityNames.User, page.UserId.Value, SyncEventActions.Update);
}
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page); _logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page);
} }
else else
@ -351,6 +376,39 @@ namespace Oqtane.Controllers
return page; return page;
} }
private void UpdatePage(Page page, int pageId, string oldPath, string newPath, bool deleted)
{
var update = (page.PageId == pageId);
if (oldPath != newPath)
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, page.Path);
if (urlMapping != null)
{
urlMapping.MappedUrl = newPath + page.Path.Substring(oldPath.Length);
_urlMappings.UpdateUrlMapping(urlMapping);
}
page.Path = newPath + page.Path.Substring(oldPath.Length);
update = true;
}
if (deleted != page.IsDeleted)
{
page.IsDeleted = deleted;
update = true;
}
if (update)
{
_pages.UpdatePage(page);
_syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update);
}
// update any children
foreach (var _page in _pages.GetPages(page.SiteId).Where(item => item.ParentId == page.PageId))
{
UpdatePage(_page, pageId, oldPath, newPath, deleted);
}
}
private List<Permission> GetPermissionsDifferences(List<Permission> permissions1, List<Permission> permissions2) private List<Permission> GetPermissionsDifferences(List<Permission> permissions1, List<Permission> permissions2)
{ {
var differences = new List<Permission>(); var differences = new List<Permission>();

View File

@ -64,7 +64,7 @@ namespace Oqtane.Controllers
} }
else else
{ {
// suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies // suppress unauthorized visitor logging as it is usually caused by clients that do not support cookies or private browsing sessions
if (entityName != EntityNames.Visitor) if (entityName != EntityNames.Visitor)
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings {EntityName} {EntityId}", entityName, entityId); _logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Settings {EntityName} {EntityId}", entityName, entityId);

View File

@ -53,7 +53,9 @@ namespace Oqtane.Controllers
systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information")); systeminfo.Add("Logging:LogLevel:Default", _configManager.GetSetting("Logging:LogLevel:Default", "Information"));
systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error")); systeminfo.Add("Logging:LogLevel:Notify", _configManager.GetSetting("Logging:LogLevel:Notify", "Error"));
systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true")); systeminfo.Add("UseSwagger", _configManager.GetSetting("UseSwagger", "true"));
systeminfo.Add("CacheControl", _configManager.GetSetting("CacheControl", ""));
systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl)); systeminfo.Add("PackageRegistryUrl", _configManager.GetSetting("PackageRegistryUrl", Constants.PackageRegistryUrl));
systeminfo.Add("PackageRegistryEmail", _configManager.GetSetting("PackageRegistryEmail", ""));
break; break;
case "log": case "log":
string log = ""; string log = "";

View File

@ -280,7 +280,7 @@ namespace Oqtane.Controllers
{ {
{ "FrameworkVersion", theme.Version }, { "FrameworkVersion", theme.Version },
{ "ClientReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{theme.Version}\" />" }, { "ClientReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{theme.Version}\" />" },
{ "SharedReference", $"<PackageReference Include=\"Oqtane.Client\" Version=\"{theme.Version}\" />" }, { "SharedReference", $"<PackageReference Include=\"Oqtane.Shared\" Version=\"{theme.Version}\" />" },
}; };
}); });
} }

View File

@ -217,7 +217,7 @@ namespace Oqtane.Controllers
// DELETE api/<controller>/5?siteid=x // DELETE api/<controller>/5?siteid=x
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")] [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")]
public async Task Delete(int id, string siteid) public async Task Delete(int id, string siteid)
{ {
User user = _users.GetUser(id, false); User user = _users.GetUser(id, false);

View File

@ -42,7 +42,7 @@ namespace Oqtane.Controllers
int UserId = -1; int UserId = -1;
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && (userid != null && int.TryParse(userid, out UserId) || rolename != null)) if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && (userid != null && int.TryParse(userid, out UserId) || rolename != null))
{ {
if (IsAuthorized(UserId, rolename)) if (IsAuthorized(UserId, rolename, SiteId))
{ {
var userroles = _userRoles.GetUserRoles(SiteId).ToList(); var userroles = _userRoles.GetUserRoles(SiteId).ToList();
if (UserId != -1) if (UserId != -1)
@ -82,7 +82,7 @@ namespace Oqtane.Controllers
public UserRole Get(int id) public UserRole Get(int id)
{ {
var userrole = _userRoles.GetUserRole(id); var userrole = _userRoles.GetUserRole(id);
if (userrole != null && SiteValid(userrole.Role.SiteId) && IsAuthorized(userrole.UserId, userrole.Role.Name)) if (userrole != null && SiteValid(userrole.Role.SiteId) && IsAuthorized(userrole.UserId, userrole.Role.Name, userrole.Role.SiteId ?? -1))
{ {
return Filter(userrole, _userPermissions.GetUser().UserId); return Filter(userrole, _userPermissions.GetUser().UserId);
} }
@ -101,57 +101,59 @@ namespace Oqtane.Controllers
} }
} }
private bool IsAuthorized(int userId, string roleName) private bool IsAuthorized(int userId, string roleName, int siteId)
{ {
bool authorized = true; bool authorized = true;
if (userId != -1) if (userId != -1)
{ {
authorized = _userPermissions.GetUser(User).UserId == userId; authorized = (_userPermissions.GetUser(User).UserId == userId);
} }
if (authorized && !string.IsNullOrEmpty(roleName)) if (authorized && !string.IsNullOrEmpty(roleName))
{ {
authorized = User.IsInRole(roleName); authorized = User.IsInRole(roleName);
} }
if (!authorized)
{
authorized = _userPermissions.IsAuthorized(User, siteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin);
}
return authorized; return authorized;
} }
private UserRole Filter(UserRole userrole, int userid) private UserRole Filter(UserRole userrole, int userid)
{ {
// clone object to avoid mutating cache // include all properties if authorized
UserRole filtered = null; if (_userPermissions.IsAuthorized(User, userrole.User.SiteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin))
if (userrole != null)
{ {
filtered = new UserRole(); return userrole;
// public properties
filtered.UserRoleId = userrole.UserRoleId;
filtered.UserId = userrole.UserId;
filtered.RoleId = userrole.RoleId;
filtered.User = new User();
filtered.User.SiteId = userrole.User.SiteId;
filtered.User.UserId = userrole.User.UserId;
filtered.User.Username = userrole.User.Username;
filtered.User.DisplayName = userrole.User.DisplayName;
filtered.Role = new Role();
filtered.Role.SiteId = userrole.Role.SiteId;
filtered.Role.RoleId = userrole.Role.RoleId;
filtered.Role.Name = userrole.Role.Name;
// include private properties if administrator
if (_userPermissions.IsAuthorized(User, filtered.User.SiteId, EntityNames.UserRole, -1, PermissionNames.Write, RoleNames.Admin))
{
filtered.User.Email = userrole.User.Email;
filtered.User.PhotoFileId = userrole.User.PhotoFileId;
filtered.User.LastLoginOn = userrole.User.LastLoginOn;
filtered.User.LastIPAddress = userrole.User.LastIPAddress;
filtered.User.CreatedOn = userrole.User.CreatedOn;
}
} }
else
{
// clone object to avoid mutating cache
UserRole filtered = null;
return filtered; if (userrole != null)
{
filtered = new UserRole();
// include public properties
filtered.UserRoleId = userrole.UserRoleId;
filtered.UserId = userrole.UserId;
filtered.RoleId = userrole.RoleId;
filtered.User = new User();
filtered.User.SiteId = userrole.User.SiteId;
filtered.User.UserId = userrole.User.UserId;
filtered.User.Username = userrole.User.Username;
filtered.User.DisplayName = userrole.User.DisplayName;
filtered.Role = new Role();
filtered.Role.SiteId = userrole.Role.SiteId;
filtered.Role.RoleId = userrole.Role.RoleId;
filtered.Role.Name = userrole.Role.Name;
}
return filtered;
}
} }
// POST api/<controller> // POST api/<controller>

View File

@ -47,7 +47,6 @@ namespace Oqtane.Extensions
// default options // default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.RequireHttpsMetadata = true; options.RequireHttpsMetadata = true;
options.SaveTokens = false;
options.GetClaimsFromUserInfoEndpoint = true; options.GetClaimsFromUserInfoEndpoint = true;
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OpenIDConnect : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OpenIDConnect;
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
@ -63,6 +62,7 @@ namespace Oqtane.Extensions
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow options.ResponseType = sitesettings.GetValue("ExternalLogin:AuthResponseType", "code"); // default is authorization code flow
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false"));
if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", ""))) if (!string.IsNullOrEmpty(sitesettings.GetValue("ExternalLogin:RoleClaimType", "")))
{ {
options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", ""); options.TokenValidationParameters.RoleClaimType = sitesettings.GetValue("ExternalLogin:RoleClaimType", "");
@ -102,7 +102,6 @@ namespace Oqtane.Extensions
// default options // default options
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2; options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-" + AuthenticationProviderTypes.OAuth2 : "/" + alias.Path + "/signin-" + AuthenticationProviderTypes.OAuth2;
options.SaveTokens = false;
// site options // site options
options.AuthorizationEndpoint = sitesettings.GetValue("ExternalLogin:AuthorizationUrl", ""); options.AuthorizationEndpoint = sitesettings.GetValue("ExternalLogin:AuthorizationUrl", "");
@ -111,6 +110,7 @@ namespace Oqtane.Extensions
options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", ""); options.ClientId = sitesettings.GetValue("ExternalLogin:ClientId", "");
options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", ""); options.ClientSecret = sitesettings.GetValue("ExternalLogin:ClientSecret", "");
options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false")); options.UsePkce = bool.Parse(sitesettings.GetValue("ExternalLogin:PKCE", "false"));
options.SaveTokens = bool.Parse(sitesettings.GetValue("ExternalLogin:SaveTokens", "false"));
options.Scope.Clear(); options.Scope.Clear();
foreach (var scope in sitesettings.GetValue("ExternalLogin:Scopes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) foreach (var scope in sitesettings.GetValue("ExternalLogin:Scopes", "").Split(',', StringSplitOptions.RemoveEmptyEntries))
{ {
@ -228,7 +228,6 @@ namespace Oqtane.Extensions
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.AccessToken));
context.Principal = new ClaimsPrincipal(identity); context.Principal = new ClaimsPrincipal(identity);
} }
@ -304,8 +303,6 @@ namespace Oqtane.Extensions
var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal); var identity = await ValidateUser(id, name, email, claims, context.HttpContext, context.Principal);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
// include access token
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
context.Principal = new ClaimsPrincipal(identity); context.Principal = new ClaimsPrincipal(identity);
} }
else else
@ -527,11 +524,6 @@ namespace Oqtane.Extensions
// manage user // manage user
if (user != null) if (user != null)
{ {
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
// manage roles // manage roles
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>(); var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
@ -591,64 +583,78 @@ namespace Oqtane.Extensions
} }
} }
// create claims identity var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered);
identityuser = await _identityUserManager.FindByNameAsync(user.Username); if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate))
user.SecurityStamp = identityuser.SecurityStamp;
identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles);
identity.Label = ExternalLoginStatus.Success;
// user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{ {
var _settings = httpContext.RequestServices.GetRequiredService<ISettingRepository>(); // update user
var _profiles = httpContext.RequestServices.GetRequiredService<IProfileRepository>(); user.LastLoginOn = DateTime.UtcNow;
var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) _users.UpdateUser(user);
// create claims identity
identityuser = await _identityUserManager.FindByNameAsync(user.Username);
user.SecurityStamp = identityuser.SecurityStamp;
identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles);
identity.Label = ExternalLoginStatus.Success;
// user profile claims
if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "")))
{ {
if (mapping.Contains(":")) var _settings = httpContext.RequestServices.GetRequiredService<ISettingRepository>();
var _profiles = httpContext.RequestServices.GetRequiredService<IProfileRepository>();
var profiles = _profiles.GetProfiles(alias.SiteId).ToList();
foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries))
{ {
var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); if (mapping.Contains(":"))
if (claim != null)
{ {
var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]);
if (profile != null) if (claim != null)
{ {
if (!string.IsNullOrEmpty(claim.Value)) var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]);
if (profile != null)
{ {
var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); if (!string.IsNullOrEmpty(claim.Value))
if (setting != null)
{ {
setting.SettingValue = claim.Value; var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name);
_settings.UpdateSetting(setting); if (setting != null)
} {
else setting.SettingValue = claim.Value;
{ _settings.UpdateSetting(setting);
setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; }
_settings.AddSetting(setting); else
{
setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate };
_settings.AddSetting(setting);
}
} }
} }
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]);
}
} }
else else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]);
} }
} }
else else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping);
} }
} }
else
{
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping);
}
} }
var _syncManager = httpContext.RequestServices.GetRequiredService<ISyncManager>();
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login");
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
}
else
{
identity.Label = ExternalLoginStatus.AccessDenied;
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External User Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId);
} }
var _syncManager = httpContext.RequestServices.GetRequiredService<ISyncManager>();
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login");
_logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName);
} }
} }
else // claims invalid else // claims invalid

View File

@ -175,6 +175,12 @@ namespace Oqtane.Infrastructure
installationid = Guid.NewGuid().ToString(); installationid = Guid.NewGuid().ToString();
AddOrUpdateSetting("InstallationId", installationid, true); AddOrUpdateSetting("InstallationId", installationid, true);
} }
var version = GetSetting("InstallationVersion", "");
if (version != Constants.Version)
{
AddOrUpdateSetting("InstallationVersion", Constants.Version, true);
AddOrUpdateSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm"), true);
}
return installationid; return installationid;
} }
} }

View File

@ -91,7 +91,7 @@ namespace Oqtane.Infrastructure
// get configuration // get configuration
if (install == null) if (install == null)
{ {
// startup or silent installation // startup or automated installation
install = new InstallConfig install = new InstallConfig
{ {
ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey), ConnectionString = _config.GetConnectionString(SettingKeys.ConnectionStringKey),
@ -111,7 +111,7 @@ namespace Oqtane.Infrastructure
if (!string.IsNullOrEmpty(install.ConnectionString) && !string.IsNullOrEmpty(install.Aliases) && !string.IsNullOrEmpty(install.HostPassword) && !string.IsNullOrEmpty(install.HostEmail)) if (!string.IsNullOrEmpty(install.ConnectionString) && !string.IsNullOrEmpty(install.Aliases) && !string.IsNullOrEmpty(install.HostPassword) && !string.IsNullOrEmpty(install.HostEmail))
{ {
// silent install // automated install
install.SiteTemplate = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate); install.SiteTemplate = GetInstallationConfig(SettingKeys.SiteTemplateKey, Constants.DefaultSiteTemplate);
install.DefaultTheme = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme); install.DefaultTheme = GetInstallationConfig(SettingKeys.DefaultThemeKey, Constants.DefaultTheme);
install.DefaultContainer = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer); install.DefaultContainer = GetInstallationConfig(SettingKeys.DefaultContainerKey, Constants.DefaultContainer);
@ -120,7 +120,11 @@ namespace Oqtane.Infrastructure
} }
else else
{ {
// silent installation is missing required information if (!string.IsNullOrEmpty(install.ConnectionString))
{
// automated installation is missing required information
result.Message = $"Error Installing Master Database For {SettingKeys.ConnectionStringKey}: {install.ConnectionString}. If You Are Trying To Execute An Automated Installation You Must Include The HostEmail, HostPassword, And DefaultAlias In appsettings.json.";
}
install.ConnectionString = ""; install.ConnectionString = "";
} }
} }
@ -261,6 +265,7 @@ namespace Oqtane.Infrastructure
var installation = IsInstalled(); var installation = IsInstalled();
try try
{ {
UpdateInstallation();
UpdateConnectionString(install.ConnectionString); UpdateConnectionString(install.ConnectionString);
UpdateDatabaseType(install.DatabaseType); UpdateDatabaseType(install.DatabaseType);
@ -487,6 +492,7 @@ namespace Oqtane.Infrastructure
moduleDefinition.Categories = moduledef.Categories; moduleDefinition.Categories = moduledef.Categories;
// update version // update version
moduleDefinition.Version = versions[versions.Length - 1]; moduleDefinition.Version = versions[versions.Length - 1];
moduleDefinition.ModifiedOn = DateTime.UtcNow;
db.Entry(moduleDefinition).State = EntityState.Modified; db.Entry(moduleDefinition).State = EntityState.Modified;
db.SaveChanges(); db.SaveChanges();
} }
@ -662,6 +668,11 @@ namespace Oqtane.Infrastructure
return connectionString; return connectionString;
} }
public void UpdateInstallation()
{
_config.GetInstallationId();
}
public void UpdateConnectionString(string connectionString) public void UpdateConnectionString(string connectionString)
{ {
connectionString = DenormalizeConnectionString(connectionString); connectionString = DenormalizeConnectionString(connectionString);
@ -673,7 +684,10 @@ namespace Oqtane.Infrastructure
public void UpdateDatabaseType(string databaseType) public void UpdateDatabaseType(string databaseType)
{ {
_configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true); if (_config.GetSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", "") != databaseType)
{
_configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", databaseType, true);
}
} }
public void AddEFMigrationsHistory(ISqlRepository sql, string connectionString, string databaseType, string version, bool isMaster) public void AddEFMigrationsHistory(ISqlRepository sql, string connectionString, string databaseType, string version, bool isMaster)

View File

@ -32,6 +32,7 @@ namespace Oqtane.Infrastructure
var logRepository = provider.GetRequiredService<ILogRepository>(); var logRepository = provider.GetRequiredService<ILogRepository>();
var visitorRepository = provider.GetRequiredService<IVisitorRepository>(); var visitorRepository = provider.GetRequiredService<IVisitorRepository>();
var notificationRepository = provider.GetRequiredService<INotificationRepository>(); var notificationRepository = provider.GetRequiredService<INotificationRepository>();
var urlMappingRepository = provider.GetRequiredService<IUrlMappingRepository>();
var installationManager = provider.GetRequiredService<IInstallationManager>(); var installationManager = provider.GetRequiredService<IInstallationManager>();
// iterate through sites for current tenant // iterate through sites for current tenant
@ -95,6 +96,22 @@ namespace Oqtane.Infrastructure
{ {
log += $"Error Purging Notifications - {ex.Message}<br />"; log += $"Error Purging Notifications - {ex.Message}<br />";
} }
// purge broken urls
retention = 30; // 30 days
if (settings.ContainsKey("UrlMappingRetention") && !string.IsNullOrEmpty(settings["UrlMappingRetention"]))
{
retention = int.Parse(settings["UrlMappingRetention"]);
}
try
{
count = urlMappingRepository.DeleteUrlMappings(site.SiteId, retention);
log += count.ToString() + " Broken Urls Purged<br />";
}
catch (Exception ex)
{
log += $"Error Purging Broken Urls - {ex.Message}<br />";
}
} }
// register assemblies // register assemblies

View File

@ -1,8 +1,8 @@
using Oqtane.Models;
using Oqtane.Infrastructure;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Shared;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates namespace Oqtane.SiteTemplates
{ {
@ -266,6 +266,7 @@ namespace Oqtane.SiteTemplates
PermissionList = new List<Permission> PermissionList = new List<Permission>
{ {
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true), // required to support personalized pages
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
PageTemplateModules = new List<PageTemplateModule> PageTemplateModules = new List<PageTemplateModule>
@ -276,6 +277,7 @@ namespace Oqtane.SiteTemplates
PermissionList = new List<Permission> PermissionList = new List<Permission>
{ {
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.View, RoleNames.Registered, true), // required to support personalized pages
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
Content = "" Content = ""

View File

@ -1,12 +1,11 @@
using Oqtane.Models;
using Oqtane.Infrastructure;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Repository;
using Microsoft.AspNetCore.Hosting;
using Oqtane.Extensions;
using Oqtane.Shared;
using System.IO; using System.IO;
using Microsoft.AspNetCore.Hosting;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Repository;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates namespace Oqtane.SiteTemplates
{ {
@ -68,7 +67,7 @@ namespace Oqtane.SiteTemplates
new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.View, RoleNames.Admin, true),
new Permission(PermissionNames.Edit, RoleNames.Admin, true) new Permission(PermissionNames.Edit, RoleNames.Admin, true)
}, },
Content = "<p>Copyright (c) 2018-2024 .NET Foundation</p>" + Content = "<p>Copyright (c) 2018-2025 .NET Foundation</p>" +
"<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>" + "<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p>" +
"<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" + "<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p>" +
"<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>" "<p>THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p>"

View File

@ -1,10 +1,8 @@
using Oqtane.Models;
using Oqtane.Infrastructure;
using System.Collections.Generic; using System.Collections.Generic;
using Oqtane.Extensions;
using Oqtane.Repository;
using Oqtane.Shared;
using Oqtane.Documentation; using Oqtane.Documentation;
using Oqtane.Infrastructure;
using Oqtane.Models;
using Oqtane.Shared;
namespace Oqtane.SiteTemplates namespace Oqtane.SiteTemplates
{ {

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
@ -71,8 +72,8 @@ namespace Oqtane.Infrastructure
case "5.2.1": case "5.2.1":
Upgrade_5_2_1(tenant, scope); Upgrade_5_2_1(tenant, scope);
break; break;
case "6.0.1": case "6.1.0":
Upgrade_6_0_1(tenant, scope); Upgrade_6_1_0(tenant, scope);
break; break;
} }
} }
@ -446,52 +447,14 @@ namespace Oqtane.Infrastructure
AddPagesToSites(scope, tenant, pageTemplates); AddPagesToSites(scope, tenant, pageTemplates);
} }
private void Upgrade_6_0_1(Tenant tenant, IServiceScope scope) private void Upgrade_6_1_0(Tenant tenant, IServiceScope scope)
{ {
// assemblies which have been relocated to the bin/refs folder in .NET 9 // remove MySql.EntityFrameworkCore package (replaced by Pomelo.EntityFrameworkCore.MySql)
string[] assemblies = { string[] assemblies = {
"Microsoft.AspNetCore.Authorization.dll", "MySql.EntityFrameworkCore.dll"
"Microsoft.AspNetCore.Components.Authorization.dll",
"Microsoft.AspNetCore.Components.dll",
"Microsoft.AspNetCore.Components.Forms.dll",
"Microsoft.AspNetCore.Components.Web.dll",
"Microsoft.AspNetCore.Cryptography.Internal.dll",
"Microsoft.AspNetCore.Cryptography.KeyDerivation.dll",
"Microsoft.AspNetCore.Metadata.dll",
"Microsoft.Extensions.Caching.Memory.dll",
"Microsoft.Extensions.Configuration.Binder.dll",
"Microsoft.Extensions.Configuration.FileExtensions.dll",
"Microsoft.Extensions.Configuration.Json.dll",
"Microsoft.Extensions.DependencyInjection.Abstractions.dll",
"Microsoft.Extensions.DependencyInjection.dll",
"Microsoft.Extensions.Diagnostics.Abstractions.dll",
"Microsoft.Extensions.Diagnostics.dll",
"Microsoft.Extensions.Http.dll",
"Microsoft.Extensions.Identity.Core.dll",
"Microsoft.Extensions.Identity.Stores.dll",
"Microsoft.Extensions.Localization.Abstractions.dll",
"Microsoft.Extensions.Localization.dll",
"Microsoft.Extensions.Logging.Abstractions.dll",
"Microsoft.Extensions.Logging.dll",
"Microsoft.Extensions.Options.dll",
"Microsoft.JSInterop.dll",
"System.Text.Json.dll"
}; };
foreach (var assembly in assemblies) RemoveAssemblies(tenant, assemblies, "6.1.0");
{
try
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var filepath = Path.Combine(binFolder, assembly);
if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath);
}
catch (Exception ex)
{
// error deleting asesmbly
_filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: 6.0.1 Upgrade Error Removing {assembly} - {ex}"));
}
}
} }
private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates) private void AddPagesToSites(IServiceScope scope, Tenant tenant, List<PageTemplate> pageTemplates)
@ -504,5 +467,27 @@ namespace Oqtane.Infrastructure
sites.CreatePages(site, pageTemplates, null); sites.CreatePages(site, pageTemplates, null);
} }
} }
private void RemoveAssemblies(Tenant tenant, string[] assemblies, string version)
{
// in a development environment assemblies cannot be removed as the debugger runs fron /bin folder and locks the files
if (tenant.Name == TenantNames.Master && !_environment.IsDevelopment())
{
foreach (var assembly in assemblies)
{
try
{
var binFolder = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var filepath = Path.Combine(binFolder, assembly);
if (System.IO.File.Exists(filepath)) System.IO.File.Delete(filepath);
}
catch (Exception ex)
{
// error deleting asesmbly
_filelogger.LogError(Utilities.LogMessage(this, $"Oqtane Error: {version} Upgrade Error Removing {assembly} - {ex}"));
}
}
}
}
} }
} }

View File

@ -12,6 +12,7 @@ using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Models; using Oqtane.Models;
using Oqtane.Repository; using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Managers namespace Oqtane.Managers
@ -363,28 +364,36 @@ namespace Oqtane.Managers
} }
else else
{ {
user = _users.GetUser(identityuser.UserName); if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
if (user != null)
{ {
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) user = GetUser(identityuser.UserName, alias.SiteId);
if (user != null)
{ {
user.IsAuthenticated = true; // ensure user is registered for site
user.LastLoginOn = DateTime.UtcNow; if (UserSecurity.ContainsRole(user.Roles, RoleNames.Registered))
user.LastIPAddress = LastIPAddress;
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress);
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login");
if (setCookie)
{ {
await _identitySignInManager.SignInAsync(identityuser, isPersistent); user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = LastIPAddress;
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress);
_syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login");
if (setCookie)
{
await _identitySignInManager.SignInAsync(identityuser, isPersistent);
}
}
else
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId);
} }
} }
else }
{ else
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); {
} _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username);
} }
} }
} }
@ -490,6 +499,9 @@ namespace Oqtane.Managers
var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password); var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
if (result.Succeeded) if (result.Succeeded)
{ {
user = _users.GetUser(user.Username);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Reload);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username);
user.Password = ""; user.Password = "";
} }
@ -512,7 +524,10 @@ namespace Oqtane.Managers
user = _users.GetUser(user.Username); user = _users.GetUser(user.Username);
if (user != null) if (user != null)
{ {
if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry) var alias = _tenantManager.GetAlias();
var twoFactorSetting = _settings.GetSetting(EntityNames.Site, alias.SiteId, "LoginOptions:TwoFactor")?.SettingValue ?? "false";
var twoFactorRequired = twoFactorSetting == "required" || user.TwoFactorRequired;
if (twoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
{ {
user.IsAuthenticated = true; user.IsAuthenticated = true;
} }

View File

@ -67,6 +67,7 @@ namespace Oqtane.Migrations.EntityBuilders
return ActiveDatabase.AddAutoIncrementColumn(table, RewriteName(name)); return ActiveDatabase.AddAutoIncrementColumn(table, RewriteName(name));
} }
// boolean
public void AddBooleanColumn(string name, bool nullable = false) public void AddBooleanColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<bool>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -87,6 +88,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<bool>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<bool>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// datetime
public void AddDateTimeColumn(string name, bool nullable = false) public void AddDateTimeColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<DateTime>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -107,6 +109,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<DateTime>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<DateTime>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// datetimeoffset
public void AddDateTimeOffsetColumn(string name, bool nullable = false) public void AddDateTimeOffsetColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<DateTimeOffset>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -127,6 +130,7 @@ 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);
} }
// dateonly
public void AddDateOnlyColumn(string name, bool nullable = false) public void AddDateOnlyColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<DateOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -147,6 +151,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<DateOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// timeonly
public void AddTimeOnlyColumn(string name, bool nullable = false) public void AddTimeOnlyColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<TimeOnly>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -167,6 +172,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<TimeOnly>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// btye
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);
@ -187,6 +193,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<byte>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<byte>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// integer
public void AddIntegerColumn(string name, bool nullable = false) public void AddIntegerColumn(string name, bool nullable = false)
{ {
_migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema); _migrationBuilder.AddColumn<int>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
@ -207,6 +214,8 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<int>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue); return table.Column<int>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
} }
// maxstring
public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true) public void AddMaxStringColumn(string name, bool nullable = false, bool unicode = true)
{ {
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, schema: Schema); _migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, unicode: unicode, schema: Schema);
@ -227,6 +236,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue); return table.Column<string>(name: RewriteName(name), nullable: nullable, unicode: unicode, defaultValue: defaultValue);
} }
// string
public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true) public void AddStringColumn(string name, int length, bool nullable = false, bool unicode = true)
{ {
_migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, schema: Schema); _migrationBuilder.AddColumn<string>(RewriteName(name), RewriteName(EntityTableName), maxLength: length, nullable: nullable, unicode: unicode, schema: Schema);
@ -247,6 +257,7 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue); return table.Column<string>(name: RewriteName(name), maxLength: length, nullable: nullable, unicode: unicode, defaultValue: defaultValue);
} }
// decimal
public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false) public void AddDecimalColumn(string name, int precision, int scale, bool nullable = false)
{ {
_migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, schema: Schema); _migrationBuilder.AddColumn<decimal>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, precision: precision, scale: scale, schema: Schema);
@ -267,6 +278,28 @@ namespace Oqtane.Migrations.EntityBuilders
return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue); return table.Column<decimal>(name: RewriteName(name), nullable: nullable, precision: precision, scale: scale, defaultValue: defaultValue);
} }
// guid
public void AddGuidColumn(string name, bool nullable = false)
{
_migrationBuilder.AddColumn<Guid>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, schema: Schema);
}
public void AddGuidColumn(string name, bool nullable, Guid defaultValue)
{
_migrationBuilder.AddColumn<Guid>(RewriteName(name), RewriteName(EntityTableName), nullable: nullable, defaultValue: defaultValue, schema: Schema);
}
protected OperationBuilder<AddColumnOperation> AddGuidColumn(ColumnsBuilder table, string name, bool nullable = false)
{
return table.Column<Guid>(name: RewriteName(name), nullable: nullable);
}
protected OperationBuilder<AddColumnOperation> AddGuidColumn(ColumnsBuilder table, string name, bool nullable, Guid defaultValue)
{
return table.Column<Guid>(name: RewriteName(name), nullable: nullable, defaultValue: defaultValue);
}
// alter string
public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true, string index = "") public void AlterStringColumn(string name, int length, bool nullable = false, bool unicode = true, string index = "")
{ {
if (index != "") if (index != "")
@ -283,6 +316,7 @@ namespace Oqtane.Migrations.EntityBuilders
ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode, index); ActiveDatabase.AlterStringColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName), length, nullable, unicode, index);
} }
// drop column
public void DropColumn(string name) public void DropColumn(string name)
{ {
ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName)); ActiveDatabase.DropColumn(_migrationBuilder, RewriteName(name), RewriteName(EntityTableName));

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Master
{
[DbContext(typeof(MasterDBContext))]
[Migration("Master.06.01.00.01")]
public class AddThemeVersion : MultiDatabaseMigration
{
public AddThemeVersion(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var themeEntityBuilder = new ThemeEntityBuilder(migrationBuilder, ActiveDatabase);
themeEntityBuilder.AddStringColumn("Version", 50, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Oqtane.Databases.Interfaces;
using Oqtane.Migrations.EntityBuilders;
using Oqtane.Repository;
namespace Oqtane.Migrations.Tenant
{
[DbContext(typeof(TenantDBContext))]
[Migration("Tenant.06.01.00.01")]
public class AddFolderCacheControl : MultiDatabaseMigration
{
public AddFolderCacheControl(IDatabase database) : base(database)
{
}
protected override void Up(MigrationBuilder migrationBuilder)
{
var folderEntityBuilder = new FolderEntityBuilder(migrationBuilder, ActiveDatabase);
folderEntityBuilder.AddStringColumn("CacheControl", 50, true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// not implemented
}
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
<Version>6.0.1</Version> <Version>6.1.0</Version>
<Product>Oqtane</Product> <Product>Oqtane</Product>
<Authors>Shaun Walker</Authors> <Authors>Shaun Walker</Authors>
<Company>.NET Foundation</Company> <Company>.NET Foundation</Company>
@ -11,7 +11,7 @@
<Copyright>.NET Foundation</Copyright> <Copyright>.NET Foundation</Copyright>
<PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl> <PackageProjectUrl>https://www.oqtane.org</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl> <PackageLicenseUrl>https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE</PackageLicenseUrl>
<PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.0.1</PackageReleaseNotes> <PackageReleaseNotes>https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.0</PackageReleaseNotes>
<RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl> <RepositoryUrl>https://github.com/oqtane/oqtane.framework</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<RootNamespace>Oqtane</RootNamespace> <RootNamespace>Oqtane</RootNamespace>
@ -34,21 +34,21 @@
<EmbeddedResource Include="Scripts\MigrateTenant.sql" /> <EmbeddedResource Include="Scripts\MigrateTenant.sql" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.0-preview2.24304.8" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.1" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite.Core" Version="9.0.1" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.11-pre20241216174303" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.71" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" /> <ProjectReference Include="..\Oqtane.Client\Oqtane.Client.csproj" />
@ -57,7 +57,7 @@
<ItemGroup> <ItemGroup>
<ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" /> <ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" />
<ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" /> <ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" />
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" /> <MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)Pomelo.EntityFrameworkCore.MySql.dll;$(OutputPath)MySql.Data.dll" />
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" /> <PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" />
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" /> <SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" />
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" /> <SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" />

View File

@ -122,17 +122,23 @@ namespace Oqtane.Pages
if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList)) if (file.Folder.SiteId != _alias.SiteId || !_userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path); if (!User.Identity.IsAuthenticated && download)
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; {
return BrokenFile(); return Redirect(Utilities.NavigateUrl(_alias.Path, "login", "?returnurl=" + WebUtility.UrlEncode(Request.Path)));
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Access Attempt For Site {SiteId} And Path {Path}", _alias.SiteId, path);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return BrokenFile();
}
} }
string etag; string etag;
string downloadName = file.Name; string downloadName = file.Name;
string filepath = _files.GetFilePath(file); string filepath = _files.GetFilePath(file);
var etagValue = file.ModifiedOn.Ticks ^ file.Size; // evaluate any querystring parameters
bool isRequestingImageManipulation = false; bool isRequestingImageManipulation = false;
int width = 0; int width = 0;
@ -140,39 +146,34 @@ namespace Oqtane.Pages
if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0) if (Request.Query.TryGetValue("width", out var widthStr) && int.TryParse(widthStr, out width) && width > 0)
{ {
isRequestingImageManipulation = true; isRequestingImageManipulation = true;
etagValue ^= (width * 31);
} }
if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0) if (Request.Query.TryGetValue("height", out var heightStr) && int.TryParse(heightStr, out height) && height > 0)
{ {
isRequestingImageManipulation = true; isRequestingImageManipulation = true;
etagValue ^= (height * 17);
} }
Request.Query.TryGetValue("mode", out var mode); Request.Query.TryGetValue("mode", out var mode);
Request.Query.TryGetValue("position", out var position); Request.Query.TryGetValue("position", out var position);
Request.Query.TryGetValue("background", out var background); Request.Query.TryGetValue("background", out var background);
if (width > 0 || height > 0)
{
if (!string.IsNullOrWhiteSpace(mode)) etagValue ^= mode.ToString().GetHashCode();
if (!string.IsNullOrWhiteSpace(position)) etagValue ^= position.ToString().GetHashCode();
if (!string.IsNullOrWhiteSpace(background)) etagValue ^= background.ToString().GetHashCode();
}
int rotate; int rotate;
if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0) if (Request.Query.TryGetValue("rotate", out var rotateStr) && int.TryParse(rotateStr, out rotate) && 360 > rotate && rotate > 0)
{ {
isRequestingImageManipulation = true; isRequestingImageManipulation = true;
etagValue ^= (rotate * 13);
} }
if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString())) if (Request.Query.TryGetValue("format", out var format) && _imageService.GetAvailableFormats().Contains(format.ToString()))
{ {
isRequestingImageManipulation = true; isRequestingImageManipulation = true;
etagValue ^= format.ToString().GetHashCode();
} }
etag = Convert.ToString(etagValue, 16); if (isRequestingImageManipulation)
{
etag = Utilities.GenerateSimpleHash(Request.QueryString.Value);
}
else
{
etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16);
}
var header = ""; var header = "";
if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch))
@ -253,12 +254,16 @@ namespace Oqtane.Pages
if (download) if (download)
{ {
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
return PhysicalFile(filepath, file.GetMimeType(), downloadName); return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName), downloadName);
} }
else else
{ {
if (!string.IsNullOrEmpty(file.Folder.CacheControl))
{
HttpContext.Response.Headers.Append(HeaderNames.CacheControl, value: file.Folder.CacheControl);
}
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
return PhysicalFile(filepath, file.GetMimeType()); return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName));
} }
} }

View File

@ -0,0 +1,3 @@
@page "/pages/impersonate"
@namespace Oqtane.Pages
@model Oqtane.Pages.ImpersonateModel

View File

@ -0,0 +1,79 @@
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Enums;
using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared;
namespace Oqtane.Pages
{
public class ImpersonateModel : PageModel
{
private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly IUserManager _userManager;
private readonly ILogManager _logger;
public ImpersonateModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager, ILogManager logger)
{
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_userManager = userManager;
_logger = logger;
}
public async Task<IActionResult> OnPostAsync(string username, string returnurl)
{
if (User.IsInRole(RoleNames.Admin) && !string.IsNullOrEmpty(username))
{
bool validuser = false;
IdentityUser identityuser = await _identityUserManager.FindByNameAsync(username);
if (identityuser != null)
{
var alias = HttpContext.GetAlias();
var user = _userManager.GetUser(identityuser.UserName, alias.SiteId);
if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered) && !UserSecurity.ContainsRole(user.Roles, RoleNames.Host))
{
validuser = true;
}
}
if (validuser)
{
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Successfully Impersonated By Administrator {Administrator}", username, User.Identity.Name);
// note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync
await _identitySignInManager.SignInAsync(identityuser, false);
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Impersonation By Administrator {Administrator} Failed For User {Username} ", User.Identity.Name, username);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Impersonate User {Username} By User {User}", username, User.Identity.Name);
}
if (returnurl == null)
{
returnurl = "";
}
else
{
returnurl = WebUtility.UrlDecode(returnurl);
}
if (!returnurl.StartsWith("/"))
{
returnurl = "/" + returnurl;
}
return LocalRedirect(Url.Content("~" + returnurl));
}
}
}

View File

@ -4,8 +4,11 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Enums;
using Oqtane.Extensions; using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Managers; using Oqtane.Managers;
using Oqtane.Security;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Pages namespace Oqtane.Pages
@ -16,12 +19,14 @@ namespace Oqtane.Pages
private readonly UserManager<IdentityUser> _identityUserManager; private readonly UserManager<IdentityUser> _identityUserManager;
private readonly SignInManager<IdentityUser> _identitySignInManager; private readonly SignInManager<IdentityUser> _identitySignInManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly ILogManager _logger;
public LoginModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager) public LoginModel(UserManager<IdentityUser> identityUserManager, SignInManager<IdentityUser> identitySignInManager, IUserManager userManager, ILogManager logger)
{ {
_identityUserManager = identityUserManager; _identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager; _identitySignInManager = identitySignInManager;
_userManager = userManager; _userManager = userManager;
_logger = logger;
} }
public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl) public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl)
@ -37,7 +42,7 @@ namespace Oqtane.Pages
{ {
var alias = HttpContext.GetAlias(); var alias = HttpContext.GetAlias();
var user = _userManager.GetUser(identityuser.UserName, alias.SiteId); var user = _userManager.GetUser(identityuser.UserName, alias.SiteId);
if (user != null && !user.IsDeleted) if (user != null && !user.IsDeleted && UserSecurity.ContainsRole(user.Roles, RoleNames.Registered))
{ {
validuser = true; validuser = true;
} }
@ -48,7 +53,16 @@ namespace Oqtane.Pages
{ {
// note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync // note that .NET Identity uses a hardcoded ApplicationScheme of "Identity.Application" in SignInAsync
await _identitySignInManager.SignInAsync(identityuser, remember); await _identitySignInManager.SignInAsync(identityuser, remember);
_logger.Log(LogLevel.Information, this, LogFunction.Security, "Login Successful For User {Username}", username);
} }
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Login Failed For User {Username}", username);
}
}
else
{
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Attempt To Login User {Username}", username);
} }
if (returnurl == null) if (returnurl == null)

View File

@ -25,7 +25,10 @@ namespace Oqtane.Server
filelogger.LogError($"[Oqtane.Server.Program.Main] {install.Message}"); filelogger.LogError($"[Oqtane.Server.Program.Main] {install.Message}");
} }
} }
host.Run(); else
{
host.Run();
}
} }
public static IWebHost BuildWebHost(string[] args) => public static IWebHost BuildWebHost(string[] args) =>

View File

@ -46,10 +46,10 @@ namespace Oqtane.Repository
files = db.File.AsNoTracking().Where(item => item.FolderId == folderId).Include(item => item.Folder).ToList(); files = db.File.AsNoTracking().Where(item => item.FolderId == folderId).Include(item => item.Folder).ToList();
} }
var alias = _tenants.GetAlias();
foreach (var file in files) foreach (var file in files)
{ {
file.Folder.PermissionList = permissions.ToList(); file.Folder.PermissionList = permissions.ToList();
var alias = _tenants.GetAlias();
file.Url = GetFileUrl(file, alias); file.Url = GetFileUrl(file, alias);
} }
return files; return files;

View File

@ -13,5 +13,6 @@ namespace Oqtane.Repository
UrlMapping GetUrlMapping(int urlMappingId, bool tracking); UrlMapping GetUrlMapping(int urlMappingId, bool tracking);
UrlMapping GetUrlMapping(int siteId, string url); UrlMapping GetUrlMapping(int siteId, string url);
void DeleteUrlMapping(int urlMappingId); void DeleteUrlMapping(int urlMappingId);
int DeleteUrlMappings(int siteId, int age);
} }
} }

View File

@ -101,6 +101,7 @@ namespace Oqtane.Repository
ModuleDefinition.Resources = moduleDefinition.Resources; ModuleDefinition.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled; ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
ModuleDefinition.PackageName = moduleDefinition.PackageName; ModuleDefinition.PackageName = moduleDefinition.PackageName;
ModuleDefinition.Fingerprint = Utilities.GenerateSimpleHash(moduleDefinition.ModifiedOn.ToString("yyyyMMddHHmm"));
} }
return ModuleDefinition; return ModuleDefinition;

View File

@ -4,6 +4,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.Metadata;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
@ -87,6 +88,7 @@ namespace Oqtane.Repository
Theme.ThemeSettingsType = theme.ThemeSettingsType; Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType; Theme.ContainerSettingsType = theme.ContainerSettingsType;
Theme.PackageName = theme.PackageName; Theme.PackageName = theme.PackageName;
Theme.Fingerprint = Utilities.GenerateSimpleHash(theme.ModifiedOn.ToString("yyyyMMddHHmm"));
Themes.Add(Theme); Themes.Add(Theme);
} }
@ -126,6 +128,13 @@ namespace Oqtane.Repository
} }
else else
{ {
if (theme.Version != Theme.Version)
{
// update theme version
theme.Version = Theme.Version;
_db.SaveChanges();
}
// override user customizable property values // override user customizable property values
Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name; Theme.Name = (!string.IsNullOrEmpty(theme.Name)) ? theme.Name : Theme.Name;

View File

@ -22,11 +22,11 @@ namespace Oqtane.Repository
using var db = _dbContextFactory.CreateDbContext(); using var db = _dbContextFactory.CreateDbContext();
if (isMapped) if (isMapped)
{ {
return db.UrlMapping.Where(item => item.SiteId == siteId && !string.IsNullOrEmpty(item.MappedUrl)).Take(200).ToList(); return db.UrlMapping.Where(item => item.SiteId == siteId && !string.IsNullOrEmpty(item.MappedUrl)).ToList();
} }
else else
{ {
return db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl)).Take(200).ToList(); return db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl)).ToList();
} }
} }
@ -101,5 +101,24 @@ namespace Oqtane.Repository
db.UrlMapping.Remove(urlMapping); db.UrlMapping.Remove(urlMapping);
db.SaveChanges(); db.SaveChanges();
} }
public int DeleteUrlMappings(int siteId, int age)
{
using var db = _dbContextFactory.CreateDbContext();
// delete in batches of 100 records
var count = 0;
var purgedate = DateTime.UtcNow.AddDays(-age);
var urlMappings = db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl) && item.RequestedOn < purgedate)
.OrderBy(item => item.RequestedOn).Take(100).ToList();
while (urlMappings.Count > 0)
{
count += urlMappings.Count;
db.UrlMapping.RemoveRange(urlMappings);
db.SaveChanges();
urlMappings = db.UrlMapping.Where(item => item.SiteId == siteId && string.IsNullOrEmpty(item.MappedUrl) && item.RequestedOn < purgedate)
.OrderBy(item => item.RequestedOn).Take(100).ToList();
}
return count;
}
} }
} }

View File

@ -29,12 +29,13 @@ namespace Oqtane.Services
private readonly ISettingRepository _settings; private readonly ISettingRepository _settings;
private readonly ITenantManager _tenantManager; private readonly ITenantManager _tenantManager;
private readonly ISyncManager _syncManager; private readonly ISyncManager _syncManager;
private readonly IConfigManager _configManager;
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]"; 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, IConfigManager configManager, ILogManager logger, IMemoryCache cache, IHttpContextAccessor accessor)
{ {
_sites = sites; _sites = sites;
_pages = pages; _pages = pages;
@ -46,6 +47,7 @@ namespace Oqtane.Services
_settings = settings; _settings = settings;
_tenantManager = tenantManager; _tenantManager = tenantManager;
_syncManager = syncManager; _syncManager = syncManager;
_configManager = configManager;
_logger = logger; _logger = logger;
_cache = cache; _cache = cache;
_accessor = accessor; _accessor = accessor;
@ -143,6 +145,9 @@ namespace Oqtane.Services
// themes // themes
site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList()); site.Themes = _themes.FilterThemes(_themes.GetThemes().ToList());
// installation date used for fingerprinting static assets
site.Fingerprint = Utilities.GenerateSimpleHash(_configManager.GetSetting("InstallationDate", DateTime.UtcNow.ToString("yyyyMMddHHmm")));
} }
else else
{ {
@ -155,46 +160,6 @@ namespace Oqtane.Services
return site; return site;
} }
private static List<Page> GetPagesHierarchy(List<Page> pages)
{
List<Page> hierarchy = new List<Page>();
Action<List<Page>, Page> getPath = null;
getPath = (pageList, page) =>
{
IEnumerable<Page> children;
int level;
if (page == null)
{
level = -1;
children = pages.Where(item => item.ParentId == null);
}
else
{
level = page.Level;
children = pages.Where(item => item.ParentId == page.PageId);
}
foreach (Page child in children)
{
child.Level = level + 1;
child.HasChildren = pages.Any(item => item.ParentId == child.PageId && !item.IsDeleted && item.IsNavigation);
hierarchy.Add(child);
getPath(pageList, child);
}
};
pages = pages.OrderBy(item => item.Order).ToList();
getPath(pages, null);
// add any non-hierarchical items to the end of the list
foreach (Page page in pages)
{
if (hierarchy.Find(item => item.PageId == page.PageId) == null)
{
hierarchy.Add(page);
}
}
return hierarchy;
}
public Task<Site> AddSiteAsync(Site site) public Task<Site> AddSiteAsync(Site site)
{ {
if (_accessor.HttpContext.User.IsInRole(RoleNames.Host)) if (_accessor.HttpContext.User.IsInRole(RoleNames.Host))

View File

@ -23,6 +23,7 @@ using OqtaneSSR.Extensions;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Oqtane.Providers; using Oqtane.Providers;
using Microsoft.AspNetCore.Cors.Infrastructure; using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.Net.Http.Headers;
namespace Oqtane namespace Oqtane
{ {
@ -98,7 +99,7 @@ namespace Oqtane
{ {
options.HeaderName = Constants.AntiForgeryTokenHeaderName; options.HeaderName = Constants.AntiForgeryTokenHeaderName;
options.Cookie.Name = Constants.AntiForgeryTokenCookieName; options.Cookie.Name = Constants.AntiForgeryTokenCookieName;
options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
}); });
@ -202,9 +203,15 @@ namespace Oqtane
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles(new StaticFileOptions app.UseStaticFiles(new StaticFileOptions
{ {
ServeUnknownFileTypes = true,
OnPrepareResponse = (ctx) => OnPrepareResponse = (ctx) =>
{ {
// static asset caching
var cachecontrol = Configuration.GetSection("CacheControl");
if (!string.IsNullOrEmpty(cachecontrol.Value))
{
ctx.Context.Response.Headers.Append(HeaderNames.CacheControl, cachecontrol.Value);
}
// CORS headers for .NET MAUI clients
var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy) var policy = corsPolicyProvider.GetPolicyAsync(ctx.Context, Constants.MauiCorsPolicy)
.ConfigureAwait(false).GetAwaiter().GetResult(); .ConfigureAwait(false).GetAwaiter().GetResult();
corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response); corsService.ApplyResult(corsService.EvaluatePolicy(ctx.Context, policy), ctx.Context.Response);

View File

@ -55,4 +55,4 @@
"Default": "Information" "Default": "Information"
} }
} }
} }

View File

@ -21,7 +21,7 @@ namespace [Owner].Module.[Module].Services
public async Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId) public async Task<Models.[Module]> Get[Module]Async(int [Module]Id, int ModuleId)
{ {
return await GetJsonAsync<Models.[Module]>(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", EntityNames.Module, ModuleId)); return await GetJsonAsync<Models.[Module]>(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
} }
public async Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module]) public async Task<Models.[Module]> Add[Module]Async(Models.[Module] [Module])
@ -36,7 +36,7 @@ namespace [Owner].Module.[Module].Services
public async Task Delete[Module]Async(int [Module]Id, int ModuleId) public async Task Delete[Module]Async(int [Module]Id, int ModuleId)
{ {
await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}", EntityNames.Module, ModuleId)); await DeleteAsync(CreateAuthorizationPolicyUrl($"{Apiurl}/{[Module]Id}/{ModuleId}", EntityNames.Module, ModuleId));
} }
} }
} }

View File

@ -13,11 +13,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.0" /> <PackageReference Include="System.Net.Http.Json" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -5,31 +5,32 @@ using Microsoft.AspNetCore.Http;
using Oqtane.Shared; using Oqtane.Shared;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using [Owner].Module.[Module].Repository; using [Owner].Module.[Module].Services;
using Oqtane.Controllers; using Oqtane.Controllers;
using System.Net; using System.Net;
using System.Threading.Tasks;
namespace [Owner].Module.[Module].Controllers namespace [Owner].Module.[Module].Controllers
{ {
[Route(ControllerRoutes.ApiRoute)] [Route(ControllerRoutes.ApiRoute)]
public class [Module]Controller : ModuleControllerBase public class [Module]Controller : ModuleControllerBase
{ {
private readonly I[Module]Repository _[Module]Repository; private readonly I[Module]Service _[Module]Service;
public [Module]Controller(I[Module]Repository [Module]Repository, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor) public [Module]Controller(I[Module]Service [Module]Service, ILogManager logger, IHttpContextAccessor accessor) : base(logger, accessor)
{ {
_[Module]Repository = [Module]Repository; _[Module]Service = [Module]Service;
} }
// GET: api/<controller>?moduleid=x // GET: api/<controller>?moduleid=x
[HttpGet] [HttpGet]
[Authorize(Policy = PolicyNames.ViewModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public IEnumerable<Models.[Module]> Get(string moduleid) public async Task<IEnumerable<Models.[Module]>> Get(string moduleid)
{ {
int ModuleId; int ModuleId;
if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId)) if (int.TryParse(moduleid, out ModuleId) && IsAuthorizedEntityId(EntityNames.Module, ModuleId))
{ {
return _[Module]Repository.Get[Module]s(ModuleId); return await _[Module]Service.Get[Module]sAsync(ModuleId);
} }
else else
{ {
@ -40,18 +41,18 @@ namespace [Owner].Module.[Module].Controllers
} }
// GET api/<controller>/5 // GET api/<controller>/5
[HttpGet("{id}")] [HttpGet("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.ViewModule)] [Authorize(Policy = PolicyNames.ViewModule)]
public Models.[Module] Get(int id) public async Task<Models.[Module]> Get(int id, int moduleid)
{ {
Models.[Module] [Module] = _[Module]Repository.Get[Module](id); Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{ {
return [Module]; return [Module];
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id}", id); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Get Attempt {[Module]Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
return null; return null;
} }
@ -60,12 +61,11 @@ namespace [Owner].Module.[Module].Controllers
// POST api/<controller> // POST api/<controller>
[HttpPost] [HttpPost]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.EditModule)]
public Models.[Module] Post([FromBody] Models.[Module] [Module]) public async Task<Models.[Module]> Post([FromBody] Models.[Module] [Module])
{ {
if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) if (ModelState.IsValid && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{ {
[Module] = _[Module]Repository.Add[Module]([Module]); [Module] = await _[Module]Service.Add[Module]Async([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "[Module] Added {[Module]}", [Module]);
} }
else else
{ {
@ -79,12 +79,11 @@ namespace [Owner].Module.[Module].Controllers
// PUT api/<controller>/5 // PUT api/<controller>/5
[HttpPut("{id}")] [HttpPut("{id}")]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.EditModule)]
public Models.[Module] Put(int id, [FromBody] Models.[Module] [Module]) public async Task<Models.[Module]> Put(int id, [FromBody] Models.[Module] [Module])
{ {
if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId) && _[Module]Repository.Get[Module]([Module].[Module]Id, false) != null) if (ModelState.IsValid && [Module].[Module]Id == id && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{ {
[Module] = _[Module]Repository.Update[Module]([Module]); [Module] = await _[Module]Service.Update[Module]Async([Module]);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "[Module] Updated {[Module]}", [Module]);
} }
else else
{ {
@ -96,19 +95,18 @@ namespace [Owner].Module.[Module].Controllers
} }
// DELETE api/<controller>/5 // DELETE api/<controller>/5
[HttpDelete("{id}")] [HttpDelete("{id}/{moduleid}")]
[Authorize(Policy = PolicyNames.EditModule)] [Authorize(Policy = PolicyNames.EditModule)]
public void Delete(int id) public async Task Delete(int id, int moduleid)
{ {
Models.[Module] [Module] = _[Module]Repository.Get[Module](id); Models.[Module] [Module] = await _[Module]Service.Get[Module]Async(id, moduleid);
if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId)) if ([Module] != null && IsAuthorizedEntityId(EntityNames.Module, [Module].ModuleId))
{ {
_[Module]Repository.Delete[Module](id); await _[Module]Service.Delete[Module]Async(id, [Module].ModuleId);
_logger.Log(LogLevel.Information, this, LogFunction.Delete, "[Module] Deleted {[Module]Id}", id);
} }
else else
{ {
_logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id}", id); _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized [Module] Delete Attempt {[Module]Id} {ModuleId}", id, moduleid);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
} }
} }

View File

@ -19,10 +19,10 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -13,9 +13,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Localization" Version="9.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -308,97 +308,107 @@ Oqtane.Interop = {
} }
return files; return files;
}, },
uploadFiles: function (posturl, folder, id, antiforgerytoken, jwt) { uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) {
var success = true;
var fileinput = document.getElementById('FileInput_' + id); var fileinput = document.getElementById('FileInput_' + id);
var progressinfo = document.getElementById('ProgressInfo_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id); var progressbar = document.getElementById('ProgressBar_' + id);
var totalSize = 0;
for (var i = 0; i < fileinput.files.length; i++) {
totalSize += fileinput.files[i].size;
}
let uploadSize = 0;
if (!chunksize || chunksize < 1) {
chunksize = 1; // 1 MB default
}
if (progressinfo !== null && progressbar !== null) { if (progressinfo !== null && progressbar !== null) {
progressinfo.setAttribute("style", "display: inline;"); progressinfo.setAttribute('style', 'display: inline;');
progressinfo.innerHTML = ''; if (fileinput.files.length > 1) {
progressbar.setAttribute("style", "width: 100%; display: inline;"); progressinfo.innerHTML = fileinput.files[0].name + ', ...';
}
else {
progressinfo.innerHTML = fileinput.files[0].name;
}
progressbar.setAttribute('style', 'width: 100%; display: inline;');
progressbar.value = 0; progressbar.value = 0;
} }
var files = fileinput.files; const uploadFile = (file) => {
var totalSize = 0; const chunkSize = chunksize * (1024 * 1024);
for (var i = 0; i < files.length; i++) { const totalParts = Math.ceil(file.size / chunkSize);
totalSize = totalSize + files[i].size; let partCount = 0;
const uploadPart = () => {
const start = partCount * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
return new Promise((resolve, reject) => {
let formdata = new FormData();
formdata.append('__RequestVerificationToken', antiforgerytoken);
formdata.append('folder', folder);
formdata.append('formfile', chunk, file.name);
var credentials = 'same-origin';
var headers = new Headers();
headers.append('PartCount', partCount + 1);
headers.append('TotalParts', totalParts);
if (jwt !== "") {
headers.append('Authorization', 'Bearer ' + jwt);
credentials = 'include';
}
return fetch(posturl, {
method: 'POST',
headers: headers,
credentials: credentials,
body: formdata
})
.then(response => {
if (!response.ok) {
if (progressinfo !== null) {
progressinfo.innerHTML = 'Error: ' + response.statusText;
}
throw new Error('Failed');
}
return;
})
.then(data => {
partCount++;
if (progressbar !== null) {
uploadSize += chunk.size;
var percent = Math.ceil((uploadSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
if (partCount < totalParts) {
uploadPart().then(resolve).catch(reject);
}
else {
resolve(data);
}
})
.catch(error => {
reject(error);
});
});
};
return uploadPart();
};
try {
for (const file of fileinput.files) {
await uploadFile(file);
}
} catch (error) {
success = false;
} }
var maxChunkSizeMB = 1; fileinput.value = '';
var bufferChunkSize = maxChunkSizeMB * (1024 * 1024); return success;
var uploadedSize = 0;
for (var i = 0; i < files.length; i++) {
var fileChunk = [];
var file = files[i];
var fileStreamPos = 0;
var endPos = bufferChunkSize;
while (fileStreamPos < file.size) {
fileChunk.push(file.slice(fileStreamPos, endPos));
fileStreamPos = endPos;
endPos = fileStreamPos + bufferChunkSize;
}
var totalParts = fileChunk.length;
var partCount = 0;
while (chunk = fileChunk.shift()) {
partCount++;
var fileName = file.name + ".part_" + partCount.toString().padStart(3, '0') + "_" + totalParts.toString().padStart(3, '0');
var data = new FormData();
data.append('__RequestVerificationToken', antiforgerytoken);
data.append('folder', folder);
data.append('formfile', chunk, fileName);
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
if (jwt !== "") {
request.setRequestHeader('Authorization', 'Bearer ' + jwt);
request.withCredentials = true;
}
request.upload.onloadstart = function (e) {
if (progressinfo !== null && progressbar !== null && progressinfo.innerHTML === '') {
if (files.length === 1) {
progressinfo.innerHTML = file.name;
}
else {
progressinfo.innerHTML = file.name + ", ...";
}
}
};
request.upload.onprogress = function (e) {
if (progressinfo !== null && progressbar !== null) {
var percent = Math.ceil(((uploadedSize + e.loaded) / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onloadend = function (e) {
if (progressinfo !== null && progressbar !== null) {
uploadedSize = uploadedSize + e.total;
var percent = Math.ceil((uploadedSize / totalSize) * 100);
progressbar.value = (percent / 100);
}
};
request.upload.onerror = function() {
if (progressinfo !== null && progressbar !== null) {
if (files.length === 1) {
progressinfo.innerHTML = file.name + ' Error: ' + request.statusText;
}
else {
progressinfo.innerHTML = ' Error: ' + request.statusText;
}
}
};
request.send(data);
}
if (i === files.length - 1) {
fileinput.value = '';
}
}
}, },
refreshBrowser: function (verify, wait) { refreshBrowser: function (verify, wait) {
async function attemptReload (verify) { async function attemptReload (verify) {

View File

@ -1,67 +1,70 @@
const scriptInfoBySrc = new Map(); const scriptKeys = new Set();
export function onUpdate() {
// determine if this is an enhanced navigation
let enhancedNavigation = scriptKeys.size !== 0;
// iterate over all script elements in document
const scripts = document.getElementsByTagName('script');
for (const script of Array.from(scripts)) {
// only process scripts that include a data-reload attribute
if (script.hasAttribute('data-reload')) {
let key = getKey(script);
if (enhancedNavigation) {
// reload the script if data-reload is "always" or "true"... or if the script has not been loaded previously and data-reload is "once"
let dataReload = script.getAttribute('data-reload');
if ((dataReload === 'always' || dataReload === 'true') || (!scriptKeys.has(key) && dataReload == 'once')) {
reloadScript(script);
}
}
// save the script key
if (!scriptKeys.has(key)) {
scriptKeys.add(key);
}
}
}
}
function getKey(script) { function getKey(script) {
if (script.hasAttribute("src") && script.src !== "") { if (script.src) {
return script.src; return script.src;
} else if (script.id) {
return script.id;
} else { } else {
return script.innerHTML; return script.innerHTML;
} }
} }
export function onUpdate() { function reloadScript(script) {
let timestamp = Date.now(); try {
let enhancedNavigation = scriptInfoBySrc.size !== 0; if (isValid(script)) {
replaceScript(script);
// iterate over all script elements in page
const scripts = document.getElementsByTagName("script");
for (const script of Array.from(scripts)) {
let key = getKey(script);
let scriptInfo = scriptInfoBySrc.get(key);
if (!scriptInfo) {
// new script added
scriptInfo = { timestamp: timestamp };
scriptInfoBySrc.set(key, scriptInfo);
if (enhancedNavigation) {
reloadScript(script);
}
} else {
// existing script
scriptInfo.timestamp = timestamp;
if (script.hasAttribute("data-reload") && script.getAttribute("data-reload") === "true") {
reloadScript(script);
}
}
}
// remove scripts that are no longer referenced
for (const [key, scriptInfo] of scriptInfoBySrc) {
if (scriptInfo.timestamp !== timestamp) {
scriptInfoBySrc.delete(key);
} }
} catch (error) {
console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error);
} }
} }
function reloadScript(script) { function isValid(script) {
try { if (script.innerHTML.includes('document.write(')) {
replaceScript(script); console.log(`Blazor Script Reload does not support scripts using document.write(): ${script.innerHTML}`);
} catch (error) { return false;
if (script.hasAttribute("src") && script.src !== "") {
console.error("Failed to load external script: ${script.src}", error);
} else {
console.error("Failed to load inline script: ${script.innerHtml}", error);
}
} }
return true;
} }
function replaceScript(script) { function replaceScript(script) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
var newScript = document.createElement("script"); var newScript = document.createElement('script');
// replicate attributes and content // replicate attributes and content
for (let i = 0; i < script.attributes.length; i++) { for (let i = 0; i < script.attributes.length; i++) {
newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); newScript.setAttribute(script.attributes[i].name, script.attributes[i].value);
} }
newScript.innerHTML = script.innerHTML; newScript.innerHTML = script.innerHTML;
newScript.removeAttribute('data-reload');
// dynamically injected scripts cannot be async or deferred // dynamically injected scripts cannot be async or deferred
newScript.async = false; newScript.async = false;
@ -70,11 +73,10 @@ function replaceScript(script) {
newScript.onload = () => resolve(); newScript.onload = () => resolve();
newScript.onerror = (error) => reject(error); newScript.onerror = (error) => reject(error);
// remove existing script // remove existing script element
script.remove(); script.remove();
// replace with new script to force reload in Blazor // replace with new script element to force reload in Blazor
document.head.appendChild(newScript); document.head.appendChild(newScript);
}); });
} }

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