diff --git a/.deployment b/.deployment deleted file mode 100644 index 3f3f33f2..00000000 --- a/.deployment +++ /dev/null @@ -1,2 +0,0 @@ -[config] -project = Oqtane.Server/Oqtane.Server.csproj diff --git a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs index c5590d51..9a89bce8 100644 --- a/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Client/Extensions/OqtaneServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Client/Installer/Controls/AzureSqlConfig.razor b/Oqtane.Client/Installer/Controls/AzureSqlConfig.razor new file mode 100644 index 00000000..18bb881f --- /dev/null +++ b/Oqtane.Client/Installer/Controls/AzureSqlConfig.razor @@ -0,0 +1,119 @@ +@namespace Oqtane.Installer.Controls +@implements Oqtane.Interfaces.IDatabaseConfigControl +@inject IStringLocalizer Localizer +@inject IStringLocalizer SharedLocalizer + +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+@if (_security == "custom") +{ +
+ +
+ +
+
+
+ +
+
+ + +
+
+
+} +
+ +
+ +
+
+@if (_encryption == "true") +{ +
+ +
+ +
+
+} + +@code { + private string _server = "tcp:{SQL Server Name}.database.windows.net,1433"; + private string _database = "{SQL Database Name}"; + private string _security = "custom"; + private string _uid = "{SQL Administrator Login}"; + private string _pwd = String.Empty; + private string _passwordType = "password"; + private string _togglePassword = string.Empty; + private string _encryption = "true"; + private string _trustservercertificate = "false"; + + protected override void OnInitialized() + { + _togglePassword = SharedLocalizer["ShowPassword"]; + } + + public string GetConnectionString() + { + var connectionString = String.Empty; + + if (!String.IsNullOrEmpty(_server) && !String.IsNullOrEmpty(_database)) + { + connectionString = $"Data Source={_server};Initial Catalog={_database};"; + } + + if (_security == "integrated") + { + connectionString += "Integrated Security=SSPI;"; + } + else + { + connectionString += $"User ID={_uid};Password={_pwd};"; + } + connectionString += $"Encrypt={_encryption};"; + connectionString += $"TrustServerCertificate={_trustservercertificate};"; + + return connectionString; + } + + private void TogglePassword() + { + if (_passwordType == "password") + { + _passwordType = "text"; + _togglePassword = SharedLocalizer["HidePassword"]; + } + else + { + _passwordType = "password"; + _togglePassword = SharedLocalizer["ShowPassword"]; + } + } +} \ No newline at end of file diff --git a/Oqtane.Client/Installer/Controls/SqlServerConfig.razor b/Oqtane.Client/Installer/Controls/SqlServerConfig.razor index 0f96c9ff..5dcc6423 100644 --- a/Oqtane.Client/Installer/Controls/SqlServerConfig.razor +++ b/Oqtane.Client/Installer/Controls/SqlServerConfig.razor @@ -4,7 +4,7 @@ @inject IStringLocalizer SharedLocalizer
- +
diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index c00a0d7e..5d2a487a 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -15,7 +15,7 @@
-
@SharedLocalizer["Version"] @Constants.Version (.NET 9)
+
@SharedLocalizer["Version"] @Constants.Version (.NET @Environment.Version.Major)

diff --git a/Oqtane.Client/Modules/Admin/Files/Edit.razor b/Oqtane.Client/Modules/Admin/Files/Edit.razor index ec8881b5..49ac137a 100644 --- a/Oqtane.Client/Modules/Admin/Files/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Files/Edit.razor @@ -15,22 +15,34 @@
- - } + + } + else + { + + + }
- + @if (_isSystem) + { + + } + else + { + + }
@@ -229,7 +241,6 @@ if (folder != null) { - await FolderService.UpdateFolderOrderAsync(folder.SiteId, folder.FolderId, folder.ParentId); await logger.LogInformation("Folder Saved {Folder}", folder); NavigationManager.NavigateTo(NavigateUrl()); } @@ -265,17 +276,9 @@ } if (!isparent) { - var files = await FileService.GetFilesAsync(_folderId); - if (files.Count == 0) - { - await FolderService.DeleteFolderAsync(_folderId); - await logger.LogInformation("Folder Deleted {Folder}", _folderId); - NavigationManager.NavigateTo(NavigateUrl()); - } - else - { - AddModuleMessage(Localizer["Message.Folder.Files.InvalidDelete"], MessageType.Warning); - } + await FolderService.DeleteFolderAsync(_folderId); + await logger.LogInformation("Folder Deleted {Folder}", _folderId); + NavigationManager.NavigateTo(NavigateUrl()); } else { diff --git a/Oqtane.Client/Modules/Admin/Files/Index.razor b/Oqtane.Client/Modules/Admin/Files/Index.razor index 06f7e61c..b730603b 100644 --- a/Oqtane.Client/Modules/Admin/Files/Index.razor +++ b/Oqtane.Client/Modules/Admin/Files/Index.razor @@ -53,7 +53,7 @@ else @context.Name - @context.ModifiedOn + @UtcToLocal(context.ModifiedOn) @context.Extension.ToUpper() @SharedLocalizer["File"] @string.Format("{0:0.00}", ((decimal)context.Size / 1000)) KB diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor index 848ab8e5..bcb500ff 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -45,7 +45,7 @@
- +
@@ -132,13 +132,13 @@ _isEnabled = job.IsEnabled.ToString(); _interval = job.Interval.ToString(); _frequency = job.Frequency; - _startDate = Utilities.UtcAsLocalDate(job.StartDate); - _startTime = Utilities.UtcAsLocalDateTime(job.StartDate); - _endDate = Utilities.UtcAsLocalDate(job.EndDate); - _endTime = Utilities.UtcAsLocalDateTime(job.EndDate); + _startDate = UtcToLocal(job.StartDate); + _startTime = UtcToLocal(job.StartDate); + _endDate = UtcToLocal(job.EndDate); + _endTime = UtcToLocal(job.EndDate); _retentionHistory = job.RetentionHistory.ToString(); - _nextDate = Utilities.UtcAsLocalDate(job.NextExecution); - _nextTime = Utilities.UtcAsLocalDateTime(job.NextExecution); + _nextDate = UtcToLocal(job.NextExecution); + _nextTime = UtcToLocal(job.NextExecution); createdby = job.CreatedBy; createdon = job.CreatedOn; modifiedby = job.ModifiedBy; @@ -176,10 +176,10 @@ { job.Interval = int.Parse(_interval); } - job.StartDate = Utilities.LocalDateAndTimeAsUtc(_startDate, _startTime); - job.EndDate = Utilities.LocalDateAndTimeAsUtc(_endDate, _endTime); + job.StartDate = LocalToUtc(_startDate.Value.Date.Add(_startTime.Value.TimeOfDay)); + job.EndDate = LocalToUtc(_endDate.Value.Date.Add(_endTime.Value.TimeOfDay)); job.RetentionHistory = int.Parse(_retentionHistory); - job.NextExecution = Utilities.LocalDateAndTimeAsUtc(_nextDate, _nextTime); + job.NextExecution = LocalToUtc(_nextDate.Value.Date.Add(_nextTime.Value.TimeOfDay)); try { diff --git a/Oqtane.Client/Modules/Admin/Jobs/Index.razor b/Oqtane.Client/Modules/Admin/Jobs/Index.razor index 4a2f5de6..033c3787 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Index.razor @@ -29,7 +29,7 @@ else @context.Name @DisplayStatus(context.IsEnabled, context.IsExecuting) @DisplayFrequency(context.Interval, context.Frequency) - @context.NextExecution?.ToLocalTime() + @UtcToLocal(context.NextExecution) @if (context.IsStarted) { diff --git a/Oqtane.Client/Modules/Admin/Jobs/Log.razor b/Oqtane.Client/Modules/Admin/Jobs/Log.razor index 7dbc6a3f..55efc788 100644 --- a/Oqtane.Client/Modules/Admin/Jobs/Log.razor +++ b/Oqtane.Client/Modules/Admin/Jobs/Log.razor @@ -23,8 +23,8 @@ else @context.Job.Name @DisplayStatus(context.Job.IsExecuting, context.Succeeded) - @context.StartDate - @context.FinishDate + @UtcToLocal(context.StartDate) + @UtcToLocal(context.FinishDate) @((MarkupString)context.Notes) @@ -44,14 +44,12 @@ else private async Task GetJobLogs() { - _jobLogs = await JobLogService.GetJobLogsAsync(); - + var jobId = -1; if (PageState.QueryString.ContainsKey("id")) { - _jobLogs = _jobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList(); + jobId = int.Parse(PageState.QueryString["id"]); } - - _jobLogs = _jobLogs.OrderByDescending(item => item.JobLogId).ToList(); + _jobLogs = await JobLogService.GetJobLogsAsync(jobId); } private string DisplayStatus(bool isExecuting, bool? succeeded) diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index f10c0203..f0a057d0 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -144,7 +144,7 @@ else user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]); if (user != null) { - await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username); + await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username); AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info); } else diff --git a/Oqtane.Client/Modules/Admin/Logs/Detail.razor b/Oqtane.Client/Modules/Admin/Logs/Detail.razor index fd980bce..379a4f3c 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Detail.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Detail.razor @@ -141,7 +141,7 @@ var log = await LogService.GetLogAsync(_logId); if (log != null) { - _logDate = log.LogDate.ToString(CultureInfo.CurrentCulture); + _logDate = UtcToLocal(log.LogDate).Value.ToString(CultureInfo.CurrentCulture); _level = log.Level; _feature = log.Feature; _function = log.Function; diff --git a/Oqtane.Client/Modules/Admin/Logs/Index.razor b/Oqtane.Client/Modules/Admin/Logs/Index.razor index dfb0cf8d..594393ca 100644 --- a/Oqtane.Client/Modules/Admin/Logs/Index.razor +++ b/Oqtane.Client/Modules/Admin/Logs/Index.razor @@ -64,7 +64,7 @@ else - @context.LogDate + @UtcToLocal(context.LogDate) @context.Level @context.Feature @context.Function diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor index 803f2437..ef8bbc95 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Edit.razor @@ -73,7 +73,7 @@
- +
diff --git a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor index 89b10381..fcfe6114 100644 --- a/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor +++ b/Oqtane.Client/Modules/Admin/ModuleDefinitions/Index.razor @@ -13,32 +13,32 @@ } else { -
-
-
- - @((MarkupString)" ") - -
-
- -
-
-
- - +
+
+
+ + + +
+
+ +
+
+
+ +
    @@ -61,17 +61,17 @@ else @context.Name @context.Version - @if (context.IsEnabled) - { - @SharedLocalizer["Yes"] - } - else - { - @SharedLocalizer["No"] - } + @if (context.IsEnabled) + { + @SharedLocalizer["Yes"] + } + else + { + @SharedLocalizer["No"] + } - @if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null) + @if (context.AssemblyName == Constants.ClientId || _modules.Where(m => m.ModuleDefinition?.ModuleDefinitionId == context.ModuleDefinitionId).FirstOrDefault() != null) { @SharedLocalizer["Yes"] } @@ -87,9 +87,9 @@ else @((MarkupString)PurchaseLink(context.PackageName)) - @{ - var version = UpgradeAvailable(context.PackageName, context.Version); - } + @{ + var version = UpgradeAvailable(context.PackageName, context.Version); + } @if (version != context.Version) { @@ -153,10 +153,10 @@ else link = "" + package.ExpiryDate.Value.Date.ToString("MMM dd, yyyy") + ""; } } - } - } - return link; - } + } + } + return link; + } private string SupportLink(string packagename, string version) { @@ -172,52 +172,75 @@ else return link; } - private string UpgradeAvailable(string packagename, string version) - { - if (!string.IsNullOrEmpty(packagename) && _packages != null) - { - var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault(); - if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0) - { - return package.Version; - } - } - return version; - } + private string UpgradeAvailable(string packagename, string version) + { + if (!string.IsNullOrEmpty(packagename) && _packages != null) + { + var package = _packages.Where(item => item.PackageId == packagename).FirstOrDefault(); + if (package != null && Version.Parse(package.Version).CompareTo(Version.Parse(version)) > 0) + { + return package.Version; + } + } + return version; + } - private async Task DownloadModule(string packagename, string version) - { - try - { - await PackageService.DownloadPackageAsync(packagename, version); - await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version); - AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message); - AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); - } - } + private async Task DownloadModule(string packagename, string version) + { + try + { + await PackageService.DownloadPackageAsync(packagename, version); + await logger.LogInformation("Module Downloaded {ModuleDefinitionName} {Version}", packagename, version); + AddModuleMessage(string.Format(Localizer["Success.Module.Install"], NavigateUrl("admin/system")), MessageType.Success); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Downloading Module {ModuleDefinitionName} {Version} {Error}", packagename, version, ex.Message); + AddModuleMessage(Localizer["Error.Module.Download"], MessageType.Error); + } + } - private async Task DeleteModule(ModuleDefinition moduleDefinition) - { - try - { - await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId); - AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success); - NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message); - AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error); - } - } + private async Task DeleteModule(ModuleDefinition moduleDefinition) + { + try + { + await ModuleDefinitionService.DeleteModuleDefinitionAsync(moduleDefinition.ModuleDefinitionId, moduleDefinition.SiteId); + AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success); + NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting Module {ModuleDefinition} {Error}", moduleDefinition, ex.Message); + AddModuleMessage(Localizer["Error.Module.Delete"], MessageType.Error); + } + } - private async Task CategoryChanged(ChangeEventArgs e) - { - _category = (string)e.Value; + private async Task CategoryChanged(ChangeEventArgs e) + { + _category = (string)e.Value; await LoadModuleDefinitions(); - } + } + + private async Task Synchronize() + { + try + { + ShowProgressIndicator(); + foreach (var moduleDefinition in _moduleDefinitions) + { + if (!string.IsNullOrEmpty(moduleDefinition.PackageName) && !_packages.Any(item => item.PackageId == moduleDefinition.PackageName)) + { + var package = await PackageService.GetPackageAsync(moduleDefinition.PackageName, moduleDefinition.Version, false); + } + } + HideProgressIndicator(); + AddModuleMessage(Localizer["Success.Module.Synchronize"], MessageType.Success); + NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Synchronizing Modules {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Module.Synchronize"], MessageType.Error); + } + } } diff --git a/Oqtane.Client/Modules/Admin/Modules/Export.razor b/Oqtane.Client/Modules/Admin/Modules/Export.razor index d2e90193..10831a56 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Export.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Export.razor @@ -5,24 +5,57 @@ @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer -
-
- -
- + + +
+
+ +
+ +
+
-
-
+
+ + @SharedLocalizer["Cancel"] + + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ + @SharedLocalizer["Cancel"] +
+ + - -@SharedLocalizer["Cancel"] @code { private string _content = string.Empty; + private FileManager _filemanager; + private string _filename = string.Empty; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Export Content"; - private async Task ExportModule() + protected override void OnInitialized() + { + _filename = Utilities.GetFriendlyUrl(ModuleState.Title); + } + + private async Task ExportText() { try { @@ -35,4 +68,34 @@ AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); } } + + private async Task ExportFile() + { + try + { + var folderid = _filemanager.GetFolderId(); + if (folderid != -1 && !string.IsNullOrEmpty(_filename)) + { + var fileid = await ModuleService.ExportModuleAsync(ModuleState.ModuleId, PageState.Page.PageId, folderid, _filename); + if (fileid != -1) + { + AddModuleMessage(Localizer["Success.Content.Export"], MessageType.Success); + } + else + { + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.Content.Export"], MessageType.Warning); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Exporting Module {ModuleId} {Error}", ModuleState.ModuleId, ex.Message); + AddModuleMessage(Localizer["Error.Module.Export"], MessageType.Error); + } + } + } diff --git a/Oqtane.Client/Modules/Admin/Modules/Import.razor b/Oqtane.Client/Modules/Admin/Modules/Import.razor index 04b2557a..eacec260 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Import.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Import.razor @@ -2,20 +2,27 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IModuleService ModuleService +@inject IFileService FileService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer
- + +
+ +
+
+
+
+
-
- +
@SharedLocalizer["Cancel"]
@@ -28,6 +35,12 @@ public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; public override string Title => "Import Content"; + private async Task OnSelectFile(int fileId) + { + var bytes = await FileService.DownloadFileAsync(fileId); + _content = System.Text.Encoding.UTF8.GetString(bytes, 0, bytes.Length); + } + private async Task ImportModule() { validated = true; diff --git a/Oqtane.Client/Modules/Admin/Modules/Settings.razor b/Oqtane.Client/Modules/Admin/Modules/Settings.razor index a949271f..297a365a 100644 --- a/Oqtane.Client/Modules/Admin/Modules/Settings.razor +++ b/Oqtane.Client/Modules/Admin/Modules/Settings.razor @@ -97,6 +97,23 @@
+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
} @@ -144,6 +161,8 @@ private string _pane; private string _containerType; private string _allPages = "false"; + private string _header = ""; + private string _footer = ""; private string _permissionNames = ""; private List _permissions = null; private string _pageId; @@ -167,37 +186,47 @@ protected override async Task OnInitializedAsync() { SetModuleTitle(Localizer["ModuleSettings.Title"]); - - _title = ModuleState.Title; _moduleSettingsTitle = Localizer["ModuleSettings.Heading"]; - _pane = ModuleState.Pane; + _containers = ThemeService.GetContainerControls(PageState.Site.Themes, PageState.Page.ThemeType); - _containerType = ModuleState.ContainerType; - _allPages = ModuleState.AllPages.ToString(); - _permissions = ModuleState.PermissionList; - _pageId = ModuleState.PageId.ToString(); - createdby = ModuleState.CreatedBy; - createdon = ModuleState.CreatedOn; - modifiedby = ModuleState.ModifiedBy; - modifiedon = ModuleState.ModifiedOn; - _effectivedate = Utilities.UtcAsLocalDate(ModuleState.EffectiveDate); - _expirydate = Utilities.UtcAsLocalDate(ModuleState.ExpiryDate); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); - if (ModuleState.ModuleDefinition != null) - { - _module = ModuleState.ModuleDefinition.Name; - _permissionNames = ModuleState.ModuleDefinition?.PermissionNames; + var pagemodule = await PageModuleService.GetPageModuleAsync(ModuleState.PageModuleId); - if (!string.IsNullOrEmpty(ModuleState.ModuleDefinition.SettingsType)) + _pageId = pagemodule.PageId.ToString(); + _title = pagemodule.Title; + _pane = pagemodule.Pane; + _containerType = pagemodule.ContainerType; + if (string.IsNullOrEmpty(_containerType)) + { + _containerType = (!string.IsNullOrEmpty(PageState.Page.DefaultContainerType)) ? PageState.Page.DefaultContainerType : PageState.Site.DefaultContainerType; + } + _header = pagemodule.Header; + _footer = pagemodule.Footer; + _effectivedate = Utilities.UtcAsLocalDate(pagemodule.EffectiveDate); + _expirydate = Utilities.UtcAsLocalDate(pagemodule.ExpiryDate); + + _allPages = pagemodule.Module.AllPages.ToString(); + createdby = pagemodule.Module.CreatedBy; + createdon = pagemodule.Module.CreatedOn; + modifiedby = pagemodule.Module.ModifiedBy; + modifiedon = pagemodule.Module.ModifiedOn; + _permissions = pagemodule.Module.PermissionList; + + if (pagemodule.Module.ModuleDefinition != null) + { + _module = pagemodule.Module.ModuleDefinition.Name; + _permissionNames = pagemodule.Module.ModuleDefinition?.PermissionNames; + + if (!string.IsNullOrEmpty(pagemodule.Module.ModuleDefinition.SettingsType)) { // module settings type explicitly declared in IModule interface - _moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.SettingsType); + _moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.SettingsType); } else { // legacy support - module settings type determined by convention ( ie. existence of a "Settings.razor" component in module ) - _moduleSettingsType = Type.GetType(ModuleState.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true); + _moduleSettingsType = Type.GetType(pagemodule.Module.ModuleDefinition.ControlTypeTemplate.Replace(Constants.ActionToken, PageState.Action), false, true); } if (_moduleSettingsType != null) { @@ -218,7 +247,7 @@ } else { - AddModuleMessage(string.Format(Localizer["Error.Module.Load"], ModuleState.ModuleDefinitionName), MessageType.Error); + AddModuleMessage(string.Format(Localizer["Error.Module.Load"], pagemodule.Module.ModuleDefinitionName), MessageType.Error); } var theme = PageState.Site.Themes.FirstOrDefault(item => item.Containers.Any(themecontrol => themecontrol.TypeName.Equals(_containerType))); @@ -270,10 +299,12 @@ { pagemodule.ContainerType = string.Empty; } + pagemodule.Header = _header; + pagemodule.Footer = _footer; await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleOrderAsync(pagemodule.PageId, pagemodule.Pane); - var module = ModuleState; + var module = await ModuleService.GetModuleAsync(ModuleState.ModuleId); module.AllPages = bool.Parse(_allPages); module.PageModuleId = ModuleState.PageModuleId; module.PermissionList = _permissionGrid.GetPermissionList(); diff --git a/Oqtane.Client/Modules/Admin/Pages/Edit.razor b/Oqtane.Client/Modules/Admin/Pages/Edit.razor index 6d27b990..df9db9bb 100644 --- a/Oqtane.Client/Modules/Admin/Pages/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Pages/Edit.razor @@ -30,16 +30,16 @@
- + + @foreach (Page page in _pages) + { + if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId) { - if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.View, page.PermissionList) && page.PageId != _pageId) - { - - } + } - + } +
@@ -217,6 +217,9 @@

+ + +

@@ -225,15 +228,28 @@
+

+
+ +
+ +
+
+
+ +
-   -   - @Localizer["ModuleTitle"] - @Localizer["ModuleDefinition"] +   +   + @Localizer["ModuleTitle"] + @Localizer["ModuleDefinition"]
@@ -247,8 +263,10 @@ { @_themeSettingsComponent +
+ +
-
} } @@ -299,19 +317,21 @@ +
+ +
@if (_themeSettingsType != null) { @_themeSettingsComponent +
+ +
-
} } -
- - } @@ -348,6 +368,7 @@ private string _bodycontent; private List _permissions = null; private PermissionGrid _permissionGrid; + private string _updatemodulepermissions; private List _pageModules; private string _createdby; private DateTime _createdon; @@ -436,6 +457,7 @@ // permissions _permissions = _page.PermissionList; + _updatemodulepermissions = "True"; // page modules var modules = await ModuleService.GetModulesAsync(PageState.Site.SiteId); @@ -651,6 +673,7 @@ if (_page.UserId == null) { _page.PermissionList = _permissionGrid.GetPermissionList(); + _page.UpdateModulePermissions = bool.Parse(_updatemodulepermissions); } _page = await PageService.UpdatePageAsync(_page); diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 88ccdd56..712ba186 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -3,80 +3,98 @@ @inherits ModuleBase @inject NavigationManager NavigationManager @inject IUserService UserService +@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @inject ISettingService SettingService -@if (PageState.Site.AllowRegistration) +@if (_initialized) { - if (!_userCreated) + @if (PageState.Site.AllowRegistration) { - if (PageState.User != null) + if (!_userCreated) { - - } - else - { - -
-
-
- -
- + if (PageState.User != null) + { + + } + else + { + + +
+
+ +
+ +
-
-
- -
-
- - +
+ +
+
+ + +
+
+
+
+ +
+
+ + +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
-
- -
-
- - -
-
-
-
- -
- -
-
-
- -
- -
-
-
-
- - - @if (_allowsitelogin) - {
+ + + @if (_allowsitelogin) + { +
-
- @Localizer["Login"] - } - +
+ @Localizer["Login"] + } + + } } - } -} -else -{ - + } + else + { + + } } @code { + private bool _initialized = false; + private List _timezones; private string _passwordrequirements; private string _username = string.Empty; private ElementReference form; @@ -87,6 +105,7 @@ else private string _confirm = string.Empty; private string _email = string.Empty; private string _displayname = string.Empty; + private string _timezoneid = string.Empty; private bool _userCreated = false; private bool _allowsitelogin = true; @@ -96,6 +115,9 @@ else { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _allowsitelogin = bool.Parse(SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:AllowSiteLogin", "true")); + _timezones = await TimeZoneService.GetTimeZonesAsync(); + _timezoneid = PageState.Site.TimeZoneId; + _initialized = true; } protected override void OnParametersSet() @@ -124,6 +146,7 @@ else Password = _password, Email = _email, DisplayName = (_displayname == string.Empty ? _username : _displayname), + TimeZoneId = _timezoneid, PhotoFileId = null }; user = await UserService.AddUserAsync(user); diff --git a/Oqtane.Client/Modules/Admin/Site/Index.razor b/Oqtane.Client/Modules/Admin/Site/Index.razor index 5333cd3b..172c4225 100644 --- a/Oqtane.Client/Modules/Admin/Site/Index.razor +++ b/Oqtane.Client/Modules/Admin/Site/Index.razor @@ -10,6 +10,7 @@ @inject IAliasService AliasService @inject IThemeService ThemeService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @inject INotificationService NotificationService @@ -41,6 +42,18 @@
+
+ +
+ +
+
@@ -133,7 +146,7 @@
- +
@@ -416,9 +429,11 @@ private List _themes = new List(); private List _containers = new List(); private List _pages; + private List _timezones; private string _name = string.Empty; private string _homepageid = "-"; + private string _timezoneid = string.Empty; private string _isdeleted; private string _sitemap = ""; private string _siteguid = ""; @@ -435,7 +450,7 @@ private Dictionary _textEditors = new Dictionary(); private string _textEditor = ""; - private string _imageFiles = string.Empty; + private string _imagefiles = string.Empty; private string _headcontent = string.Empty; private string _bodycontent = string.Empty; @@ -493,11 +508,13 @@ Site site = await SiteService.GetSiteAsync(PageState.Site.SiteId); if (site != null) { + _timezones = await TimeZoneService.GetTimeZonesAsync(); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); _pages = await PageService.GetPagesAsync(PageState.Site.SiteId); _name = site.Name; + _timezoneid = site.TimeZoneId; if (site.HomePageId != null) { _homepageid = site.HomePageId.Value.ToString(); @@ -531,8 +548,8 @@ _textEditors.Add(textEditor.Name, Utilities.GetFullTypeName(textEditor.GetType().AssemblyQualifiedName)); } _textEditor = SettingService.GetSetting(settings, "TextEditor", Constants.DefaultTextEditor); - _imageFiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); - _imageFiles = (string.IsNullOrEmpty(_imageFiles)) ? Constants.ImageFiles : _imageFiles; + _imagefiles = SettingService.GetSetting(settings, "ImageFiles", Constants.ImageFiles); + _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles; // page content _headcontent = site.HeadContent; @@ -650,6 +667,7 @@ if (site != null) { site.Name = _name; + site.TimeZoneId = _timezoneid; site.HomePageId = (_homepageid != "-" ? int.Parse(_homepageid) : null); site.IsDeleted = (_isdeleted == null ? true : Boolean.Parse(_isdeleted)); diff --git a/Oqtane.Client/Modules/Admin/Themes/Edit.razor b/Oqtane.Client/Modules/Admin/Themes/Edit.razor index ed324ebf..125e2846 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Edit.razor @@ -55,7 +55,7 @@
- +
diff --git a/Oqtane.Client/Modules/Admin/Themes/Index.razor b/Oqtane.Client/Modules/Admin/Themes/Index.razor index b6b25345..2d8f83a8 100644 --- a/Oqtane.Client/Modules/Admin/Themes/Index.razor +++ b/Oqtane.Client/Modules/Admin/Themes/Index.razor @@ -15,8 +15,8 @@ else { - @((MarkupString)" ") - + +
@@ -173,4 +173,27 @@ else AddModuleMessage(Localizer["Error.Theme.Delete"], MessageType.Error); } } + + private async Task Synchronize() + { + try + { + ShowProgressIndicator(); + foreach (var theme in _themes) + { + if (!string.IsNullOrEmpty(theme.PackageName) && !_packages.Any(item => item.PackageId == theme.PackageName)) + { + await PackageService.GetPackageAsync(theme.PackageName, theme.Version, false); + } + } + HideProgressIndicator(); + AddModuleMessage(Localizer["Success.Theme.Synchronize"], MessageType.Success); + NavigationManager.NavigateTo(NavigateUrl(PageState.Page.Path, true)); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Synchronizing Themes {Error}", ex.Message); + AddModuleMessage(Localizer["Error.Theme.Synchronize"], MessageType.Error); + } + } } \ No newline at end of file diff --git a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor index c43ae566..aeda1366 100644 --- a/Oqtane.Client/Modules/Admin/Upgrade/Index.razor +++ b/Oqtane.Client/Modules/Admin/Upgrade/Index.razor @@ -13,9 +13,26 @@ @if (_package != null && _upgradeavailable) { - - - +
+
+ +
+ +
+
+
+
+ @if (!_downloaded) + { + + } + else + { + + } } else { @@ -23,7 +40,6 @@ }
-
@@ -31,7 +47,17 @@
+
+ +
+ +
+
+
@@ -39,8 +65,10 @@ @code { private bool _initialized = false; + private bool _downloaded = false; private Package _package; private bool _upgradeavailable = false; + private string _backup = "True"; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Host; @@ -86,7 +114,7 @@ ShowProgressIndicator(); var interop = new Interop(JSRuntime); await interop.RedirectBrowser(NavigateUrl(), 10); - await InstallationService.Upgrade(); + await InstallationService.Upgrade(bool.Parse(_backup)); } catch (Exception ex) { @@ -102,6 +130,7 @@ ShowProgressIndicator(); await PackageService.DownloadPackageAsync(packageid, version); await PackageService.DownloadPackageAsync(Constants.UpdaterPackageId, version); + _downloaded = true; HideProgressIndicator(); AddModuleMessage(Localizer["Success.Framework.Download"], MessageType.Success); } diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor index e57ad13e..68956c40 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Add.razor @@ -8,13 +8,16 @@
- +
- +
+ + +
- +
@@ -26,64 +29,80 @@ @code { - private ElementReference form; - private bool validated = false; + private ElementReference form; + private bool validated = false; - private string _url = string.Empty; - private string _mappedurl = string.Empty; + private string _url = string.Empty; + private string _mappedurl = string.Empty; - public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; + public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; - private async Task SaveUrlMapping() - { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) - { - if (_url != _mappedurl) - { - var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; - url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); + private async Task SaveUrlMapping() + { + validated = true; + var interop = new Interop(JSRuntime); + if (await interop.FormValid(form)) + { + if (_url != _mappedurl) + { + var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; + url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); - _url = (_url.StartsWith("/")) ? _url.Substring(1) : _url; - _url = (!_url.StartsWith("http")) ? url + _url : _url; + _url = (_url.StartsWith("/")) ? _url.Substring(1) : _url; + _url = (!_url.StartsWith("http")) ? url + _url : _url; - if (_url.StartsWith(url)) - { - var urlmapping = new UrlMapping(); - urlmapping.SiteId = PageState.Site.SiteId; - var route = new Route(_url, PageState.Alias.Path); - urlmapping.Url = route.PagePath; - urlmapping.MappedUrl = _mappedurl.Replace(url, ""); - urlmapping.Requests = 0; - urlmapping.CreatedOn = DateTime.UtcNow; - urlmapping.RequestedOn = DateTime.UtcNow; + _mappedurl = _mappedurl.Replace(url, ""); + _mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl; - try - { - urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping); - await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); - NavigationManager.NavigateTo(NavigateUrl()); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message); - AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error); - } - } - else - { - AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning); - } - } - else - { + if (_url.StartsWith(url)) + { + var urlmapping = new UrlMapping(); + urlmapping.SiteId = PageState.Site.SiteId; + urlmapping.Url = new Route(_url, PageState.Alias.Path).PagePath; + urlmapping.MappedUrl = _mappedurl; + urlmapping.Requests = 0; + urlmapping.CreatedOn = DateTime.UtcNow; + urlmapping.RequestedOn = DateTime.UtcNow; + + try + { + urlmapping = await UrlMappingService.AddUrlMappingAsync(urlmapping); + await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Saving UrlMapping {UrlMapping} {Error}", urlmapping, ex.Message); + AddModuleMessage(Localizer["Error.SaveUrlMapping"], MessageType.Error); + } + } + else + { + AddModuleMessage(Localizer["Message.SaveUrlMapping"], MessageType.Warning); + } + } + else + { AddModuleMessage(Localizer["Message.DuplicateUrlMapping"], MessageType.Warning); - } + } } else { AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); } } + + private void GenerateUrl() + { + var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; + url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); + + var chars = "abcdefghijklmnopqrstuvwxyz"; + Random rnd = new Random(); + for (int i = 0; i < 5; i++) + { + url += chars.Substring(rnd.Next(0, chars.Length - 1), 1); + } + _url = url; + } } diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor index 10c9a57f..c6cb2e0e 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Edit.razor @@ -8,13 +8,13 @@
- +
- +
@@ -67,8 +67,11 @@ var url = PageState.Uri.Scheme + "://" + PageState.Uri.Authority + "/"; url = url + (!string.IsNullOrEmpty(PageState.Alias.Path) ? PageState.Alias.Path + "/" : ""); + _mappedurl = _mappedurl.Replace(url, ""); + _mappedurl = (_mappedurl.StartsWith("/") && _mappedurl != "/") ? _mappedurl.Substring(1) : _mappedurl; + var urlmapping = await UrlMappingService.GetUrlMappingAsync(_urlmappingid); - urlmapping.MappedUrl = _mappedurl.Replace(url, ""); + urlmapping.MappedUrl = _mappedurl; urlmapping = await UrlMappingService.UpdateUrlMappingAsync(urlmapping); await logger.LogInformation("UrlMapping Saved {UrlMapping}", urlmapping); NavigationManager.NavigateTo(NavigateUrl()); diff --git a/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor index 75a4c845..554213fa 100644 --- a/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor +++ b/Oqtane.Client/Modules/Admin/UrlMappings/Index.razor @@ -48,7 +48,7 @@ else } @context.Requests - @context.RequestedOn + @UtcToLocal(context.RequestedOn) diff --git a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor index 63486fc7..2517c538 100644 --- a/Oqtane.Client/Modules/Admin/UserProfile/Index.razor +++ b/Oqtane.Client/Modules/Admin/UserProfile/Index.razor @@ -9,6 +9,7 @@ @inject INotificationService NotificationService @inject IFileService FileService @inject IFolderService FolderService +@inject ITimeZoneService TimeZoneService @inject IJSRuntime jsRuntime @inject IServiceProvider ServiceProvider @inject IStringLocalizer Localizer @@ -16,9 +17,9 @@ @if (_initialized) { - @if (PageState.User != null && photo != null) + @if (PageState.User != null && _photo != null) { - @displayname + @_displayname } else { @@ -31,7 +32,7 @@
- +
@@ -47,17 +48,17 @@
- +
- @if (allowtwofactor) + @if (_allowtwofactor) {
- @@ -67,19 +68,31 @@
- +
- +
- +
- + +
+
+
+ +
+
@@ -91,17 +104,17 @@
- @foreach (Profile profile in profiles) + @foreach (Profile profile in _profiles) { var p = profile; if (!p.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { - if (p.Category != category) + if (p.Category != _category) {
@p.Category
- category = p.Category; + _category = p.Category; }
@@ -150,12 +163,12 @@ @if (p.IsRequired) { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)" /> } else { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)" /> } } else @@ -163,12 +176,12 @@ @if (p.IsRequired) { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)" /> } else { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)" /> } } } @@ -179,12 +192,12 @@ @if (p.IsRequired) { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)"> } else { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)"> } } else @@ -192,12 +205,12 @@ @if (p.IsRequired) { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)"> } else { + @attributes="@(p.MaxLength > 0 ? new Dictionary {{"maxlength", p.MaxLength }} : null)"> } } } @@ -220,11 +233,11 @@
- @if (filter == "to") + @if (_filter == "to") { - @if (notifications.Any()) + @if (_notifications.Any()) { - +
    @@ -260,15 +273,15 @@ context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\r", ""); } - notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; + _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; } @if (context.IsRead) { - @notificationSummary + @_notificationSummary } else { - @notificationSummary + @_notificationSummary } @@ -285,9 +298,9 @@ } else { - @if (notifications.Any()) + @if (_notifications.Any()) { - +
@@ -324,15 +337,15 @@ context.Body = context.Body.Replace("\n", ""); context.Body = context.Body.Replace("\r", ""); } - notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; + _notificationSummary = context.Body.Length > 100 ? (context.Body.Substring(0, 97) + "...") : context.Body; } @if (context.IsRead) { - @notificationSummary + @_notificationSummary } else { - @notificationSummary + @_notificationSummary } @@ -354,29 +367,32 @@ } @code { + private List _timezones; private bool _initialized = false; private string _passwordrequirements; - private string username = string.Empty; + private string _username = string.Empty; private string _password = string.Empty; private string _passwordtype = "password"; private string _togglepassword = string.Empty; - private string confirm = string.Empty; - private bool allowtwofactor = false; - private string twofactor = "False"; - private string email = string.Empty; - private string displayname = string.Empty; - private FileManager filemanager; - private int folderid = -1; - private int photofileid = -1; - private File photo = null; - private string _ImageFiles = string.Empty; - private List profiles; - private Dictionary userSettings; - private string category = string.Empty; + private string _confirm = string.Empty; + private bool _allowtwofactor = false; + private string _twofactor = "False"; + private string _email = string.Empty; + private string _displayname = string.Empty; + private FileManager _filemanager; + private int _folderid = -1; + private string _timezoneid = string.Empty; + private int _photofileid = -1; + private File _photo = null; + private string _imagefiles = string.Empty; - private string filter = "to"; - private List notifications; - private string notificationSummary = string.Empty; + private List _profiles; + private Dictionary _userSettings; + private string _category = string.Empty; + + private string _filter = "to"; + private List _notifications; + private string _notificationSummary = string.Empty; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View; @@ -386,17 +402,19 @@ { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _togglepassword = SharedLocalizer["ShowPassword"]; - allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); - profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); + _allowtwofactor = (SettingService.GetSetting(PageState.Site.Settings, "LoginOptions:TwoFactor", "false") == "true"); + _profiles = await ProfileService.GetProfilesAsync(ModuleState.SiteId); + _timezones = await TimeZoneService.GetTimeZonesAsync(); if (PageState.User != null) { - username = PageState.User.Username; - twofactor = PageState.User.TwoFactorRequired.ToString(); - email = PageState.User.Email; - displayname = PageState.User.DisplayName; + _username = PageState.User.Username; + _twofactor = PageState.User.TwoFactorRequired.ToString(); + _email = PageState.User.Email; + _displayname = PageState.User.DisplayName; + _timezoneid = PageState.User.TimeZoneId; - if (string.IsNullOrEmpty(email)) + if (string.IsNullOrEmpty(_email)) { AddModuleMessage(Localizer["Message.User.NoEmail"], MessageType.Warning); } @@ -405,24 +423,24 @@ var folder = await FolderService.GetFolderAsync(ModuleState.SiteId, PageState.User.FolderPath); if (folder != null) { - folderid = folder.FolderId; + _folderid = folder.FolderId; } if (PageState.User.PhotoFileId != null) { - photofileid = PageState.User.PhotoFileId.Value; - photo = await FileService.GetFileAsync(photofileid); + _photofileid = PageState.User.PhotoFileId.Value; + _photo = await FileService.GetFileAsync(_photofileid); } else { - photofileid = -1; - photo = null; + _photofileid = -1; + _photo = null; } - userSettings = PageState.User.Settings; + _userSettings = PageState.User.Settings; var sitesettings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); - _ImageFiles = SettingService.GetSetting(userSettings, "ImageFiles", Constants.ImageFiles); - _ImageFiles = (string.IsNullOrEmpty(_ImageFiles)) ? Constants.ImageFiles : _ImageFiles; + _imagefiles = SettingService.GetSetting(_userSettings, "ImageFiles", Constants.ImageFiles); + _imagefiles = (string.IsNullOrEmpty(_imagefiles)) ? Constants.ImageFiles : _imagefiles; await LoadNotificationsAsync(); @@ -442,13 +460,13 @@ private async Task LoadNotificationsAsync() { - notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, filter, PageState.User.UserId); - notifications = notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); + _notifications = await NotificationService.GetNotificationsAsync(PageState.Site.SiteId, _filter, PageState.User.UserId); + _notifications = _notifications.Where(item => item.DeletedBy != PageState.User.Username).ToList(); } private string GetProfileValue(string SettingName, string DefaultValue) { - string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); + string value = SettingService.GetSetting(_userSettings, SettingName, DefaultValue); if (value.Contains("]")) { value = value.Substring(value.IndexOf("]") + 1); @@ -460,38 +478,39 @@ { try { - if (username != string.Empty && email != string.Empty) + if (_username != string.Empty && _email != string.Empty) { - if (_password == confirm) + if (_password == _confirm) { if (ValidateProfiles()) { var user = PageState.User; - user.Username = username; + user.Username = _username; user.Password = _password; - user.TwoFactorRequired = bool.Parse(twofactor); - user.Email = email; - user.DisplayName = (displayname == string.Empty ? username : displayname); - user.PhotoFileId = filemanager.GetFileId(); + user.TwoFactorRequired = bool.Parse(_twofactor); + user.Email = _email; + user.DisplayName = (_displayname == string.Empty ? _username : _displayname); + user.TimeZoneId = _timezoneid; + user.PhotoFileId = _filemanager.GetFileId(); if (user.PhotoFileId == -1) { user.PhotoFileId = null; } if (user.PhotoFileId != null) { - photofileid = user.PhotoFileId.Value; - photo = await FileService.GetFileAsync(photofileid); + _photofileid = user.PhotoFileId.Value; + _photo = await FileService.GetFileAsync(_photofileid); } else { - photofileid = -1; - photo = null; + _photofileid = -1; + _photo = null; } user = await UserService.UpdateUserAsync(user); if (user != null) { - await SettingService.UpdateUserSettingsAsync(userSettings, PageState.User.UserId); + await SettingService.UpdateUserSettingsAsync(_userSettings, PageState.User.UserId); await logger.LogInformation("User Profile Saved"); if (!string.IsNullOrEmpty(PageState.ReturnUrl)) @@ -557,12 +576,12 @@ private bool ValidateProfiles() { - foreach (Profile profile in profiles) + foreach (Profile profile in _profiles) { var value = GetProfileValue(profile.Name, string.Empty); if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) { - userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); + _userSettings = SettingService.SetSetting(_userSettings, profile.Name, profile.DefaultValue); } if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { @@ -594,7 +613,7 @@ private void ProfileChanged(ChangeEventArgs e, string SettingName) { var value = (string)e.Value; - userSettings = SettingService.SetSetting(userSettings, SettingName, value); + _userSettings = SettingService.SetSetting(_userSettings, SettingName, value); } private async Task Delete(Notification Notification) @@ -624,7 +643,7 @@ private async void FilterChanged(ChangeEventArgs e) { - filter = (string)e.Value; + _filter = (string)e.Value; await LoadNotificationsAsync(); StateHasChanged(); } @@ -634,7 +653,7 @@ try { ShowProgressIndicator(); - foreach(var Notification in notifications) + foreach(var Notification in _notifications) { if (!Notification.IsDeleted) { diff --git a/Oqtane.Client/Modules/Admin/Users/Add.razor b/Oqtane.Client/Modules/Admin/Users/Add.razor index 10cf19e8..510c87e7 100644 --- a/Oqtane.Client/Modules/Admin/Users/Add.razor +++ b/Oqtane.Client/Modules/Admin/Users/Add.razor @@ -5,6 +5,7 @@ @inject IUserService UserService @inject IProfileService ProfileService @inject ISettingService SettingService +@inject ITimeZoneService TimeZoneService @inject IStringLocalizer Localizer @inject IStringLocalizer SharedLocalizer @@ -12,7 +13,7 @@ { - @if (profiles != null) + @if (_profiles != null) {
@@ -33,6 +34,18 @@
+
+ +
+ +
+
@@ -48,20 +61,20 @@
- @foreach (Profile profile in profiles) + @foreach (Profile profile in _profiles) { var p = profile; - if (p.Category != category) + if (p.Category != _category) {
@p.Category
- category = p.Category; + _category = p.Category; }
-
- @if (!string.IsNullOrEmpty(p.Options)) +
+ @if (!string.IsNullOrEmpty(p.Options)) { +
- +
@@ -32,32 +33,53 @@
- +
- +
- +
- +
- +
- + +
+
+
+ +
+ +
+
+
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) {
- +
- @@ -65,15 +87,15 @@
}
- +
- +
- +
- +
@@ -81,15 +103,15 @@
- @foreach (Profile profile in profiles) + @foreach (Profile profile in _profiles) { var p = profile; - if (p.Category != category) + if (p.Category != _category) {
@p.Category
- category = p.Category; + _category = p.Category; }
@@ -128,47 +150,50 @@
- +
@SharedLocalizer["Cancel"] - @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !ishost) + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin) && PageState.Runtime != Shared.Runtime.Hybrid && !_ishost) { } - @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _isdeleted == "True") { }

- + } @code { + private List _timezones; private bool _initialized = false; private string _passwordrequirements; - private int userid; - private string username = string.Empty; + private int _userid; + private string _username = string.Empty; private string _password = string.Empty; private string _passwordtype = "password"; private string _togglepassword = string.Empty; - private string confirm = string.Empty; - private string email = string.Empty; - private string displayname = string.Empty; - private string isdeleted; - private string lastlogin; - private string lastipaddress; - private bool ishost = false; + private string _confirm = string.Empty; + private string _email = string.Empty; + private string _confirmed = string.Empty; + private string _displayname = string.Empty; + private string _timezoneid = string.Empty; + private string _isdeleted; + private string _lastlogin; + private string _lastipaddress; + private bool _ishost = false; - private List profiles; - private Dictionary userSettings; - private string category = string.Empty; + private List _profiles; + private Dictionary _settings; + private string _category = string.Empty; - private string createdby; - private DateTime createdon; - private string modifiedby; - private DateTime modifiedon; - private string deletedby; - private DateTime? deletedon; + private string _createdby; + private DateTime _createdon; + private string _modifiedby; + private DateTime _modifiedon; + private string _deletedby; + private DateTime? _deletedon; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit; @@ -178,29 +203,32 @@ { _passwordrequirements = await UserService.GetPasswordRequirementsAsync(PageState.Site.SiteId); _togglepassword = SharedLocalizer["ShowPassword"]; - profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); + _profiles = await ProfileService.GetProfilesAsync(PageState.Site.SiteId); + _timezones = await TimeZoneService.GetTimeZonesAsync(); if (PageState.QueryString.ContainsKey("id") && int.TryParse(PageState.QueryString["id"], out int UserId)) { - userid = UserId; - var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + _userid = UserId; + var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId); if (user != null) { - username = user.Username; - email = user.Email; - displayname = user.DisplayName; - isdeleted = user.IsDeleted.ToString(); - lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", user.LastLoginOn); - lastipaddress = user.LastIPAddress; - ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); + _username = user.Username; + _email = user.Email; + _confirmed = user.EmailConfirmed.ToString(); + _displayname = user.DisplayName; + _timezoneid = PageState.User.TimeZoneId; + _isdeleted = user.IsDeleted.ToString(); + _lastlogin = string.Format("{0:MMM dd yyyy HH:mm:ss}", UtcToLocal(user.LastLoginOn)); + _lastipaddress = user.LastIPAddress; + _ishost = UserSecurity.ContainsRole(user.Roles, RoleNames.Host); - userSettings = user.Settings; - createdby = user.CreatedBy; - createdon = user.CreatedOn; - modifiedby = user.ModifiedBy; - modifiedon = user.ModifiedOn; - deletedby = user.DeletedBy; - deletedon = user.DeletedOn; + _settings = user.Settings; + _createdby = user.CreatedBy; + _createdon = user.CreatedOn; + _modifiedby = user.ModifiedBy; + _modifiedon = user.ModifiedOn; + _deletedby = user.DeletedBy; + _deletedon = user.DeletedOn; } } @@ -208,14 +236,14 @@ } catch (Exception ex) { - await logger.LogError(ex, "Error Loading User {UserId} {Error}", userid, ex.Message); + await logger.LogError(ex, "Error Loading User {UserId} {Error}", _userid, ex.Message); AddModuleMessage(Localizer["Error.User.Load"], MessageType.Error); } } private string GetProfileValue(string SettingName, string DefaultValue) { - string value = SettingService.GetSetting(userSettings, SettingName, DefaultValue); + string value = SettingService.GetSetting(_settings, SettingName, DefaultValue); if (value.Contains("]")) { value = value.Substring(value.IndexOf("]") + 1); @@ -227,27 +255,29 @@ { try { - if (username != string.Empty && email != string.Empty) + if (_username != string.Empty && _email != string.Empty) { - if (_password == confirm) + if (_password == _confirm) { if (ValidateProfiles()) { - var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + var user = await UserService.GetUserAsync(_userid, PageState.Site.SiteId); user.SiteId = PageState.Site.SiteId; - user.Username = username; + user.Username = _username; user.Password = _password; - user.Email = email; - user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; + user.Email = _email; + user.EmailConfirmed = bool.Parse(_confirmed); + user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname; + user.TimeZoneId = _timezoneid; 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); if (user != null) { - await SettingService.UpdateUserSettingsAsync(userSettings, user.UserId); + await SettingService.UpdateUserSettingsAsync(_settings, user.UserId); await logger.LogInformation("User Saved {User}", user); NavigationManager.NavigateTo(NavigateUrl()); } @@ -269,7 +299,7 @@ } catch (Exception ex) { - await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", username, email, ex.Message); + await logger.LogError(ex, "Error Saving User {Username} {Email} {Error}", _username, _email, ex.Message); AddModuleMessage(Localizer["Error.User.Save"], MessageType.Error); } } @@ -278,17 +308,17 @@ { try { - await logger.LogInformation(LogFunction.Security, "User {Username} Impersonated By Administrator {Administrator}", username, PageState.User.Username); + 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 }; + 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); + await logger.LogError(ex, "Error Impersonating User {Username} {Error}", _username, ex.Message); AddModuleMessage(Localizer["Error.User.Impersonate"], MessageType.Error); } } @@ -297,9 +327,9 @@ { try { - if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && _userid != PageState.User.UserId) { - var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + 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()); @@ -307,19 +337,19 @@ } catch (Exception ex) { - await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); + await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", _userid, ex.Message); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); } } private bool ValidateProfiles() { - foreach (Profile profile in profiles) + foreach (Profile profile in _profiles) { var value = GetProfileValue(profile.Name, string.Empty); if (string.IsNullOrEmpty(value) && !string.IsNullOrEmpty(profile.DefaultValue)) { - userSettings = SettingService.SetSetting(userSettings, profile.Name, profile.DefaultValue); + _settings = SettingService.SetSetting(_settings, profile.Name, profile.DefaultValue); } if (!profile.IsPrivate || UserSecurity.IsAuthorized(PageState.User, RoleNames.Admin)) { @@ -346,7 +376,7 @@ private void ProfileChanged(ChangeEventArgs e, string SettingName) { var value = (string)e.Value; - userSettings = SettingService.SetSetting(userSettings, SettingName, value); + _settings = SettingService.SetSetting(_settings, SettingName, value); } private void TogglePassword() diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index e0d895c8..7d34670f 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -43,7 +43,7 @@ else @context.User.Username @context.User.DisplayName @((MarkupString)string.Format("{1}", @context.User.Email, @context.User.Email)) - @((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", context.User.LastLoginOn) : "") + @((context.User.LastLoginOn != DateTime.MinValue) ? string.Format("{0:dd-MMM-yyyy HH:mm:ss}", UtcToLocal(context.User.LastLoginOn)) : "") @@ -59,29 +59,23 @@ else
+ @if (_allowregistration == "true") + { +
+ +
+ +
+
+ } +
+ +
+ +
+
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - @if (_providertype != "") - { -
- -
- -
-
- } - else - { -
- -
- -
-
- }
@@ -421,6 +415,27 @@ else
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+ +
+ +
+
+
+ +
+ +
+
+ } }
@@ -473,7 +488,8 @@ else private List users; private string _allowregistration; - private string _allowsitelogin; + private string _registerurl; + private string _profileurl; private string _twofactor; private string _cookiename; private string _cookieexpiration; @@ -520,6 +536,8 @@ else private string _domainfilter; private string _createusers; private string _verifyusers; + private string _allowhostrole; + private string _allowsitelogin; private string _secret; private string _secrettype = "password"; @@ -540,7 +558,8 @@ else var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); _allowregistration = PageState.Site.AllowRegistration.ToString().ToLower(); - _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); + _registerurl = SettingService.GetSetting(settings, "LoginOptions:RegisterUrl", ""); + _profileurl = SettingService.GetSetting(settings, "LoginOptions:ProfileUrl", ""); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { @@ -602,6 +621,8 @@ else _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); _verifyusers = SettingService.GetSetting(settings, "ExternalLogin:VerifyUsers", "true"); + _allowhostrole = SettingService.GetSetting(settings, "ExternalLogin:AllowHostRole", "false"); + _allowsitelogin = SettingService.GetSetting(settings, "LoginOptions:AllowSiteLogin", "true"); } private async Task LoadUsersAsync(bool load) @@ -659,10 +680,11 @@ else await SiteService.UpdateSiteAsync(site); var settings = await SettingService.GetSiteSettingsAsync(site.SiteId); - settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { + settings = SettingService.SetSetting(settings, "LoginOptions:RegisterUrl", _registerurl, false); + settings = SettingService.SetSetting(settings, "LoginOptions:ProfileUrl", _profileurl, false); settings = SettingService.SetSetting(settings, "LoginOptions:TwoFactor", _twofactor, false); settings = SettingService.SetSetting(settings, "LoginOptions:CookieName", _cookiename, true); settings = SettingService.SetSetting(settings, "LoginOptions:CookieExpiration", _cookieexpiration, true); @@ -705,6 +727,8 @@ else settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); settings = SettingService.SetSetting(settings, "ExternalLogin:VerifyUsers", _verifyusers, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:AllowHostRole", _allowhostrole, true); + settings = SettingService.SetSetting(settings, "LoginOptions:AllowSiteLogin", _allowsitelogin, false); settings = SettingService.SetSetting(settings, "JwtOptions:Secret", _secret, true); settings = SettingService.SetSetting(settings, "JwtOptions:Issuer", _issuer, true); diff --git a/Oqtane.Client/Modules/Admin/Visitors/Detail.razor b/Oqtane.Client/Modules/Admin/Visitors/Detail.razor index e0570431..13dd456f 100644 --- a/Oqtane.Client/Modules/Admin/Visitors/Detail.razor +++ b/Oqtane.Client/Modules/Admin/Visitors/Detail.razor @@ -101,8 +101,8 @@ _url = visitor.Url; _referrer = visitor.Referrer; _visits = visitor.Visits.ToString(); - _visited = visitor.VisitedOn.ToString(CultureInfo.CurrentCulture); - _created = visitor.CreatedOn.ToString(CultureInfo.CurrentCulture); + _visited = UtcToLocal(visitor.VisitedOn).Value.ToString(CultureInfo.CurrentCulture); + _created = UtcToLocal(visitor.CreatedOn).Value.ToString(CultureInfo.CurrentCulture); if (visitor.UserId != null) { diff --git a/Oqtane.Client/Modules/Admin/Visitors/Index.razor b/Oqtane.Client/Modules/Admin/Visitors/Index.razor index ea5510c0..2dccb94b 100644 --- a/Oqtane.Client/Modules/Admin/Visitors/Index.razor +++ b/Oqtane.Client/Modules/Admin/Visitors/Index.razor @@ -53,8 +53,8 @@ else @context.Language @context.Visits - @context.VisitedOn - @context.CreatedOn + @UtcToLocal(context.VisitedOn) + @UtcToLocal(context.CreatedOn) @@ -100,6 +100,18 @@ else
+ +
+
+ +
+ +
+
+
+
+ +
} @@ -113,6 +125,7 @@ else private string _filter = ""; private int _retention = 30; private string _correlation = "true"; + private string _robots = ""; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -139,7 +152,8 @@ else _filter = SettingService.GetSetting(settings, "VisitorFilter", Constants.DefaultVisitorFilter); _retention = int.Parse(SettingService.GetSetting(settings, "VisitorRetention", "30")); _correlation = SettingService.GetSetting(settings, "VisitorCorrelation", "true"); - } + _robots = SettingService.GetSetting(settings, "Robots", ""); + } private async void TypeChanged(ChangeEventArgs e) { @@ -191,6 +205,7 @@ else settings = SettingService.SetSetting(settings, "VisitorFilter", _filter, true); settings = SettingService.SetSetting(settings, "VisitorRetention", _retention.ToString(), true); settings = SettingService.SetSetting(settings, "VisitorCorrelation", _correlation, true); + settings = SettingService.SetSetting(settings, "Robots", _robots, true); await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId); AddModuleMessage(Localizer["Success.SaveSiteSettings"], MessageType.Success); diff --git a/Oqtane.Client/Modules/Controls/AuditInfo.razor b/Oqtane.Client/Modules/Controls/AuditInfo.razor index acbb3dc6..e2a025ec 100644 --- a/Oqtane.Client/Modules/Controls/AuditInfo.razor +++ b/Oqtane.Client/Modules/Controls/AuditInfo.razor @@ -52,7 +52,7 @@ if (CreatedOn != null) { - _text += $" {Localizer["On"]} {CreatedOn.Value.ToString(DateTimeFormat)}"; + _text += $" {Localizer["On"]} {UtcToLocal(CreatedOn).Value.ToString(DateTimeFormat)}"; } _text += "

"; @@ -69,7 +69,7 @@ if (ModifiedOn != null) { - _text += $" {Localizer["On"]} {ModifiedOn.Value.ToString(DateTimeFormat)}"; + _text += $" {Localizer["On"]} {UtcToLocal(ModifiedOn).Value.ToString(DateTimeFormat)}"; } _text += "

"; @@ -86,7 +86,7 @@ if (DeletedOn != null) { - _text += $" {Localizer["On"]} {DeletedOn.Value.ToString(DateTimeFormat)}"; + _text += $" {Localizer["On"]} {UtcToLocal(DeletedOn).Value.ToString(DateTimeFormat)}"; } _text += "

"; diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 6a2b979d..1e5fa1c5 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -163,6 +163,13 @@ [Parameter] public EventCallback OnUpload { get; set; } // optional - executes a method in the calling component when a file is uploaded + [Parameter] + public EventCallback OnSelectFolder { get; set; } // optional - executes a method in the calling component when a folder is selected + + [Parameter] + public EventCallback OnSelectFile { get; set; } // optional - executes a method in the calling component when a file is selected + + [Obsolete("Use OnSelectFile instead.")] [Parameter] public EventCallback OnSelect { get; set; } // optional - executes a method in the calling component when a file is selected @@ -300,6 +307,8 @@ FileId = -1; _file = null; _image = string.Empty; + + await OnSelectFolder.InvokeAsync(FolderId); StateHasChanged(); } catch (Exception ex) @@ -315,7 +324,10 @@ _message = string.Empty; FileId = int.Parse((string)e.Value); await SetImage(); + #pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); + #pragma warning restore CS0618 + await OnSelectFile.InvokeAsync(FileId); StateHasChanged(); } @@ -438,7 +450,10 @@ { FileId = file.FileId; await SetImage(); +#pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); +#pragma warning restore CS0618 + await OnSelectFile.InvokeAsync(FileId); await OnUpload.InvokeAsync(FileId); } await GetFiles(); @@ -489,7 +504,10 @@ await GetFiles(); FileId = -1; await SetImage(); +#pragma warning disable CS0618 await OnSelect.InvokeAsync(FileId); +#pragma warning restore CS0618 + await OnSelectFile.InvokeAsync(FileId); StateHasChanged(); } catch (Exception ex) diff --git a/Oqtane.Client/Modules/Controls/PermissionGrid.razor b/Oqtane.Client/Modules/Controls/PermissionGrid.razor index 6bd700bd..a1f6e094 100644 --- a/Oqtane.Client/Modules/Controls/PermissionGrid.razor +++ b/Oqtane.Client/Modules/Controls/PermissionGrid.razor @@ -60,7 +60,7 @@ @foreach (User user in _users) { - @user.DisplayName + @user.DisplayName (@user.Username) @foreach (var permissionname in _permissionnames) { @@ -270,8 +270,8 @@ private async Task> GetUsers(string filter) { var users = await UserRoleService.GetUserRolesAsync(PageState.Site.SiteId, RoleNames.Registered); - return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase)) - .ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName); + return users.Where(item => item.User.DisplayName.Contains(filter, StringComparison.OrdinalIgnoreCase) || item.User.Username.Contains(filter, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(item => item.UserId.ToString(), item => item.User.DisplayName + " (" + item.User.Username + ")"); } private async Task AddUser() diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index b97b229b..39681067 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -1,23 +1,23 @@ -using Microsoft.AspNetCore.Components; -using Oqtane.Shared; -using Oqtane.Models; -using System.Threading.Tasks; -using Oqtane.Services; using System; -using Oqtane.Enums; -using Oqtane.UI; using System.Collections.Generic; -using Microsoft.JSInterop; -using System.Linq; using System.Dynamic; -using System.Reflection; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using Oqtane.Enums; +using Oqtane.Models; +using Oqtane.Services; +using Oqtane.Shared; +using Oqtane.UI; namespace Oqtane.Modules { public abstract class ModuleBase : ComponentBase, IModuleControl { private Logger _logger; - private string _urlparametersstate; + private string _urlparametersstate = string.Empty; private Dictionary _urlparameters; private bool _scriptsloaded = false; @@ -62,7 +62,7 @@ namespace Oqtane.Modules public Dictionary UrlParameters { get { - if (_urlparametersstate == null || _urlparametersstate != PageState.UrlParameters) + if (string.IsNullOrEmpty(_urlparametersstate) || _urlparametersstate != PageState.UrlParameters) { _urlparametersstate = PageState.UrlParameters; _urlparameters = GetUrlParameters(UrlParametersTemplate); @@ -79,18 +79,21 @@ namespace Oqtane.Modules { List resources = null; var type = GetType(); - if (type.BaseType == typeof(ModuleBase)) + if (type.IsSubclassOf(typeof(ModuleBase))) { - if (PageState.Page.Resources != null) + if (type.IsSubclassOf(typeof(ModuleControlBase))) { - resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList(); + if (Resources != null) + { + resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + } } - } - else // modulecontrolbase - { - if (Resources != null) + else // ModuleBase { - resources = Resources.Where(item => item.ResourceType == ResourceType.Script).ToList(); + if (PageState.Page.Resources != null) + { + resources = PageState.Page.Resources.Where(item => item.ResourceType == ResourceType.Script && item.Level == ResourceLevel.Module && item.Namespace == type.Namespace).ToList(); + } } } if (resources != null && resources.Any()) @@ -421,70 +424,109 @@ namespace Oqtane.Modules public string ReplaceTokens(string content, object obj) { - var tokens = new List(); - var pos = content.IndexOf("["); - if (pos != -1) - { - if (content.IndexOf("]", pos) != -1) - { - var token = content.Substring(pos, content.IndexOf("]", pos) - pos + 1); - if (token.Contains(":")) - { - tokens.Add(token.Substring(1, token.Length - 2)); - } - } - pos = content.IndexOf("[", pos + 1); - } - if (tokens.Count != 0) - { - foreach (string token in tokens) - { - var segments = token.Split(":"); - if (segments.Length >= 2 && segments.Length <= 3) - { - var objectName = string.Join(":", segments, 0, segments.Length - 1); - var propertyName = segments[segments.Length - 1]; - var propertyValue = ""; + // Using StringBuilder avoids the performance penalty of repeated string allocations + // that occur with string.Replace or string concatenation inside loops. + var sb = new StringBuilder(); + var cache = new Dictionary(); // Cache to store resolved tokens + int index = 0; - switch (objectName) + // Loop through content to find and replace all tokens + while (index < content.Length) + { + int start = content.IndexOf('[', index); // Find start of token + if (start == -1) + { + sb.Append(content, index, content.Length - index); // Append remaining content + break; + } + + int end = content.IndexOf(']', start); // Find end of token + if (end == -1) + { + sb.Append(content, index, content.Length - index); // Append unmatched content + break; + } + + sb.Append(content, index, start - index); // Append content before token + + string token = content.Substring(start + 1, end - start - 1); // Extract token without brackets + string[] parts = token.Split('|', 2); // Separate default fallback if present + string key = parts[0]; + string fallback = parts.Length == 2 ? parts[1] : null; + + if (!cache.TryGetValue(token, out string replacement)) // Check cache first + { + replacement = "[" + token + "]"; // Default replacement is original token + string[] segments = key.Split(':'); + + if (segments.Length >= 2) + { + object current = GetTarget(segments[0], obj); // Start from root object + for (int i = 1; i < segments.Length && current != null; i++) { - case "ModuleState": - propertyValue = ModuleState.GetType().GetProperty(propertyName)?.GetValue(ModuleState, null).ToString(); - break; - case "PageState": - propertyValue = PageState.GetType().GetProperty(propertyName)?.GetValue(PageState, null).ToString(); - break; - case "PageState:Alias": - propertyValue = PageState.Alias.GetType().GetProperty(propertyName)?.GetValue(PageState.Alias, null).ToString(); - break; - case "PageState:Site": - propertyValue = PageState.Site.GetType().GetProperty(propertyName)?.GetValue(PageState.Site, null).ToString(); - break; - case "PageState:Page": - propertyValue = PageState.Page.GetType().GetProperty(propertyName)?.GetValue(PageState.Page, null).ToString(); - break; - case "PageState:User": - propertyValue = PageState.User?.GetType().GetProperty(propertyName)?.GetValue(PageState.User, null).ToString(); - break; - case "PageState:Route": - propertyValue = PageState.Route.GetType().GetProperty(propertyName)?.GetValue(PageState.Route, null).ToString(); - break; - default: - if (obj != null && obj.GetType().Name == objectName) - { - propertyValue = obj.GetType().GetProperty(propertyName)?.GetValue(obj, null).ToString(); - } - break; - } - if (propertyValue != null) - { - content = content.Replace("[" + token + "]", propertyValue); + var type = current.GetType(); + var prop = type.GetProperty(segments[i]); + current = prop?.GetValue(current); } + if (current != null) + { + replacement = current.ToString(); + } + else if (fallback != null) + { + replacement = fallback; // Use fallback if available + } } + cache[token] = replacement; // Store in cache } + + sb.Append(replacement); // Append replacement value + index = end + 1; // Move index past token } - return content; + + return sb.ToString(); + } + + // Resolve the object instance for a given object name + // Easy to extend with additional object types + private object GetTarget(string name, object obj) + { + return name switch + { + "ModuleState" => ModuleState, + "PageState" => PageState, + _ => (obj != null && obj.GetType().Name == name) ? obj : null // Fallback to obj + }; + } + + // date methods + public DateTime? UtcToLocal(DateTime? datetime) + { + TimeZoneInfo timezone = null; + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } + return Utilities.UtcAsLocalDateTime(datetime, timezone); + } + + public DateTime? LocalToUtc(DateTime? datetime) + { + TimeZoneInfo timezone = null; + if (PageState.User != null && !string.IsNullOrEmpty(PageState.User.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.User.TimeZoneId); + } + else if (!string.IsNullOrEmpty(PageState.Site.TimeZoneId)) + { + timezone = TimeZoneInfo.FindSystemTimeZoneById(PageState.Site.TimeZoneId); + } + return Utilities.LocalDateAndTimeAsUtc(datetime, timezone); } // logging methods diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj index d8fca67b..bb7671d6 100644 --- a/Oqtane.Client/Oqtane.Client.csproj +++ b/Oqtane.Client/Oqtane.Client.csproj @@ -4,7 +4,7 @@ net9.0 Exe Debug;Release - 6.1.1 + 6.1.3 Oqtane Shaun Walker .NET Foundation @@ -12,7 +12,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -22,16 +22,22 @@ - - - - + + + + + + + true + + + false false diff --git a/Oqtane.Client/Resources/Installer/Controls/SqlServerConfig.resx b/Oqtane.Client/Resources/Installer/Controls/SqlServerConfig.resx index 7909364d..6f249723 100644 --- a/Oqtane.Client/Resources/Installer/Controls/SqlServerConfig.resx +++ b/Oqtane.Client/Resources/Installer/Controls/SqlServerConfig.resx @@ -121,7 +121,7 @@ Server: - Enter the database server name. This might include a port number as well if you are using a cloud service (ie. servername.database.windows.net,1433) + Enter the database server name. This might include a port number as well if you are using a cloud service. Database: diff --git a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx index c35e179e..c0c4cb8c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Files/Edit.resx @@ -135,9 +135,6 @@ Error Saving Folder - - Folder Has Files And Cannot Be Deleted - Folder Has Subfolders And Cannot Be Deleted diff --git a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx index 0b1a8780..c60c0716 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Login/Index.resx @@ -121,10 +121,10 @@ Forgot Password - User Account Verified Successfully. You Can Now Login With Your Username And Password Below. + User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password. - User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions. + User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions. User Account Linked Successfully. You Can Now Login With Your External Login Below. @@ -133,7 +133,7 @@ External Login Could Not Be Linked. Please Contact Your Administrator For Further Instructions. - Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Require Verification When They Are Initially Created So You May Wish To Check Your Email If You Are A New User. + Login Failed. Please Remember That Passwords Are Case Sensitive. If You Have Attempted To Sign In Multiple Times Unsuccessfully, Your Account Will Be Locked Out For A Period Of Time. Note That User Accounts Often Require Email Address Verification So You May Wish To Check Your Email For A Notification. Please Provide All Required Fields diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx index 186fdd65..4421b90a 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Edit.resx @@ -147,8 +147,8 @@ The owner or creator of the module - - The reference url of the module + + The url of the module The contact for the module @@ -171,8 +171,8 @@ Owner: - - Reference Url: + + Url: Contact: diff --git a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx index ce91ee3b..8d8e5cac 100644 --- a/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/ModuleDefinitions/Index.resx @@ -159,4 +159,13 @@ Enabled? + + Check For Updates + + + Module Information Has Been Retrieved From The Marketplace + + + Error Retrieving Module Information From The Marketplace + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx index 3ed705f8..23378774 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Export.resx @@ -121,7 +121,7 @@ Export - The Exported Module Content + Select the Export option and you will be able to view the module content Content: @@ -135,4 +135,25 @@ Export Content + + Content + + + File + + + Folder: + + + Select a folder where you wish to save the exported content + + + Please Select A Folder And Provide A Filename Before Choosing Export + + + Filename: + + + Specify a name for the file (without an extension) + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx index a91aa36e..4f8f895c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Modules/Settings.resx @@ -189,4 +189,19 @@ Module Settings + + Header: + + + Optionally provide content to be injected above the module instance + + + Footer: + + + Optionally provide content to be injected below the module instance + + + Content + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx index cf720e19..ded50502 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Pages/Edit.resx @@ -303,4 +303,10 @@ Provide a url path for your personalized page. Please note that spaces and punctuation will be replaced by a dash. + + Update Module Permissions? + + + Specify if changes made to page permissions should be propagated to the modules on this page + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx index 5a0af9ed..47e95add 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Register/Index.resx @@ -180,4 +180,10 @@ Already have account? Login now. + + Time Zone: + + + Your time zone + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx index 0127c7cc..acfd022e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Site/Index.resx @@ -447,4 +447,10 @@ Site Map Cache Cleared + + Time Zone: + + + The default time zone for the site + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx index c18f9761..69f9bdd8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Edit.resx @@ -132,8 +132,8 @@ Owner: - - Reference Url: + + Url: Contact: @@ -153,8 +153,8 @@ The owner or creator of the theme - - The reference url of the theme + + The url of the theme The contact for the theme diff --git a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx index 7ff1b618..0d3b97ed 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Themes/Index.resx @@ -159,4 +159,13 @@ Assign + + Check For Updates + + + Theme Information Has Been Retrieved From The Marketplace + + + Error Retrieving Theme Information From The Marketplace + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx index a833fa20..a38e830c 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Upgrade/Index.resx @@ -124,7 +124,7 @@ Error Downloading Framework Package - Upload a framework package and select Install to complete the installation + Upload A Framework Package (Oqtane.Framework.#.#.#.nupkg) And Then Select Upgrade Framework: @@ -144,13 +144,16 @@ Framework Is Already Up To Date - - Upload A Framework Package (Oqtane.Framework.version.nupkg) And Then Select Upgrade - You Cannot Perform A System Update In A Development Environment - Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. Also Note That The System Update Capability Is Not Recommended When Using Microsoft Azure Due To Environmental Limitations. + Please Note That The System Update Capability Is A Simplified Upgrade Process Intended For Small To Medium Sized Installations. For Larger Enterprise Installations You Will Want To Use A Manual Upgrade Process. + + + Backup Files? + + + Specify if you want to backup files during the upgrade process. Disabling this option will reduce the time required for the upgrade. \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx index e1185498..aeae4b29 100644 --- a/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx +++ b/Oqtane.Client/Resources/Modules/Admin/UrlMappings/Add.resx @@ -1,4 +1,4 @@ - + Exe - 6.1.1 + 6.1.3 Oqtane Shaun Walker .NET Foundation @@ -14,7 +14,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 https://github.com/oqtane/oqtane.framework Git Oqtane.Maui @@ -30,7 +30,7 @@ com.oqtane.maui - 6.1.1 + 6.1.3 1 @@ -67,14 +67,14 @@ - - - - - - - - + + + + + + + + diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec index 3ace78bf..b50b816c 100644 --- a/Oqtane.Package/Oqtane.Client.nuspec +++ b/Oqtane.Package/Oqtane.Client.nuspec @@ -2,7 +2,7 @@ Oqtane.Client - 6.1.1 + 6.1.3 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec index 0ab95f9b..a9e739dc 100644 --- a/Oqtane.Package/Oqtane.Framework.nuspec +++ b/Oqtane.Package/Oqtane.Framework.nuspec @@ -2,7 +2,7 @@ Oqtane.Framework - 6.1.1 + 6.1.3 Shaun Walker .NET Foundation Oqtane Framework @@ -11,8 +11,8 @@ .NET Foundation false MIT - https://github.com/oqtane/oqtane.framework/releases/download/v6.1.1/Oqtane.Framework.6.1.1.Upgrade.zip - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Upgrade.zip + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 readme.md icon.png oqtane framework diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec index 9331b895..b5e58742 100644 --- a/Oqtane.Package/Oqtane.Server.nuspec +++ b/Oqtane.Package/Oqtane.Server.nuspec @@ -2,7 +2,7 @@ Oqtane.Server - 6.1.1 + 6.1.3 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec index 937ac8bf..9c2041be 100644 --- a/Oqtane.Package/Oqtane.Shared.nuspec +++ b/Oqtane.Package/Oqtane.Shared.nuspec @@ -2,7 +2,7 @@ Oqtane.Shared - 6.1.1 + 6.1.3 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 readme.md icon.png oqtane diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec index f55170f2..09453309 100644 --- a/Oqtane.Package/Oqtane.Updater.nuspec +++ b/Oqtane.Package/Oqtane.Updater.nuspec @@ -2,7 +2,7 @@ Oqtane.Updater - 6.1.1 + 6.1.3 Shaun Walker .NET Foundation Oqtane Framework @@ -12,7 +12,7 @@ false MIT https://github.com/oqtane/oqtane.framework - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 readme.md icon.png oqtane diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1 index 9f6f5f82..fb0b6d7f 100644 --- a/Oqtane.Package/install.ps1 +++ b/Oqtane.Package/install.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Install.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Install.zip" -Force diff --git a/Oqtane.Package/release.cmd b/Oqtane.Package/release.cmd index 0452d806..8bc6518a 100644 --- a/Oqtane.Package/release.cmd +++ b/Oqtane.Package/release.cmd @@ -9,6 +9,8 @@ nuget.exe pack Oqtane.Framework.nuspec del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" > NUL rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish" dotnet publish ..\Oqtane.Server\Oqtane.Server.csproj /p:Configuration=Release +del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content" > NUL +rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\Content" del /F/Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" > NUL rmdir /Q/S "..\Oqtane.Server\bin\Release\net9.0\publish\wwwroot\Content" setlocal ENABLEDELAYEDEXPANSION diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1 index 70b816f3..e2f26321 100644 --- a/Oqtane.Package/upgrade.ps1 +++ b/Oqtane.Package/upgrade.ps1 @@ -1 +1 @@ -Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.1.Upgrade.zip" -Force +Compress-Archive -Path "..\Oqtane.Server\bin\Release\net9.0\publish\*" -DestinationPath "Oqtane.Framework.6.1.3.Upgrade.zip" -Force diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index 868755aa..570e4426 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -297,7 +297,7 @@ if (urlMapping != null && !string.IsNullOrEmpty(urlMapping.MappedUrl)) { // redirect to mapped url - var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + "/" + urlMapping.MappedUrl; + var url = (urlMapping.MappedUrl.StartsWith("http")) ? urlMapping.MappedUrl : route.SiteUrl + (!urlMapping.MappedUrl.StartsWith("/") ? "/" : "") + urlMapping.MappedUrl + ((!urlMapping.MappedUrl.Contains("?")) ? route.Query : ""); NavigationManager.NavigateTo(url, true); } else // no url mapping exists @@ -764,7 +764,7 @@ } // ensure resource does not exist already - if (!pageresources.Exists(item => item.Url.ToLower() == resource.Url.ToLower())) + if (!pageresources.Exists(item => Utilities.GetUrlPath(item.Url).ToLower() == Utilities.GetUrlPath(resource.Url).ToLower())) { pageresources.Add(resource.Clone(level, name, fingerprint)); } diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index 4bdb8a90..f0b72f22 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -22,7 +22,7 @@ using Microsoft.AspNetCore.Cors; using System.IO.Compression; using Oqtane.Services; using Microsoft.Extensions.Primitives; -using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Net.Http.Headers; // ReSharper disable StringIndexOfIsCultureSpecific.1 @@ -735,6 +735,10 @@ namespace Oqtane.Controllers } if (!string.IsNullOrEmpty(imagepath)) { + if (!string.IsNullOrEmpty(file.Folder.CacheControl)) + { + HttpContext.Response.Headers.Append(HeaderNames.CacheControl, value: file.Folder.CacheControl); + } return PhysicalFile(imagepath, file.GetMimeType()); } else diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs index 23d3db9b..12a9c3fa 100644 --- a/Oqtane.Server/Controllers/FolderController.cs +++ b/Oqtane.Server/Controllers/FolderController.cs @@ -20,14 +20,16 @@ namespace Oqtane.Controllers { private readonly IFolderRepository _folders; private readonly IUserPermissions _userPermissions; + private readonly IFileRepository _files; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - public FolderController(IFolderRepository folders, IUserPermissions userPermissions, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) + public FolderController(IFolderRepository folders, IUserPermissions userPermissions, IFileRepository files, ISyncManager syncManager, ILogManager logger, ITenantManager tenantManager) { _folders = folders; _userPermissions = userPermissions; + _files = files; _syncManager = syncManager; _logger = logger; _alias = tenantManager.GetAlias(); @@ -41,7 +43,8 @@ namespace Oqtane.Controllers int SiteId; if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId) { - foreach (Folder folder in _folders.GetFolders(SiteId)) + var hierarchy = GetFoldersHierarchy(_folders.GetFolders(SiteId).ToList()); + foreach (Folder folder in hierarchy) { // note that Browse permission is used for this method if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList)) @@ -49,7 +52,6 @@ namespace Oqtane.Controllers folders.Add(folder); } } - folders = GetFoldersHierarchy(folders); } else { @@ -244,34 +246,6 @@ namespace Oqtane.Controllers return folder; } - // PUT api//?siteid=x&folderid=y&parentid=z - [HttpPut] - [Authorize(Roles = RoleNames.Registered)] - public void Put(int siteid, int folderid, int? parentid) - { - if (siteid == _alias.SiteId && _folders.GetFolder(folderid, false) != null && _userPermissions.IsAuthorized(User, siteid, EntityNames.Folder, folderid, PermissionNames.Edit)) - { - int order = 1; - List folders = _folders.GetFolders(siteid).ToList(); - foreach (Folder folder in folders.Where(item => item.ParentId == parentid).OrderBy(item => item.Order)) - { - if (folder.Order != order) - { - folder.Order = order; - _folders.UpdateFolder(folder); - _syncManager.AddSyncEvent(_alias, EntityNames.Folder, folder.FolderId, SyncEventActions.Update); - } - order += 2; - } - _logger.Log(LogLevel.Information, this, LogFunction.Update, "Folder Order Updated {SiteId} {FolderId} {ParentId}", siteid, folderid, parentid); - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Update, "Unauthorized Folder Put Attempt {SiteId} {FolderId} {ParentId}", siteid, folderid, parentid); - HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; - } - } - // DELETE api//5 [HttpDelete("{id}")] [Authorize(Roles = RoleNames.Registered)] @@ -280,10 +254,23 @@ namespace Oqtane.Controllers var folder = _folders.GetFolder(id, false); if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, folder.SiteId, EntityNames.Folder, id, PermissionNames.Edit)) { - if (Directory.Exists(_folders.GetFolderPath(folder))) + var folderPath = _folders.GetFolderPath(folder); + if (Directory.Exists(folderPath)) { - Directory.Delete(_folders.GetFolderPath(folder)); + // remove all files from disk (including thumbnails, etc...) + foreach (var filePath in Directory.GetFiles(folderPath)) + { + System.IO.File.Delete(filePath); + } + Directory.Delete(folderPath); } + + // remove files from database + foreach (var file in _files.GetFiles(id)) + { + _files.DeleteFile(file.FileId); + } + _folders.DeleteFolder(id); _syncManager.AddSyncEvent(_alias, EntityNames.Folder, folder.FolderId, SyncEventActions.Delete); _logger.Log(LogLevel.Information, this, LogFunction.Delete, "Folder Deleted {FolderId}", id); @@ -299,7 +286,6 @@ namespace Oqtane.Controllers { List hierarchy = new List(); Action, Folder> getPath = null; - var folders1 = folders; getPath = (folderList, folder) => { IEnumerable children; @@ -307,23 +293,23 @@ namespace Oqtane.Controllers if (folder == null) { level = -1; - children = folders1.Where(item => item.ParentId == null); + children = folders.Where(item => item.ParentId == null); } else { level = folder.Level; - children = folders1.Where(item => item.ParentId == folder.FolderId); + children = folders.Where(item => item.ParentId == folder.FolderId); } foreach (Folder child in children) { child.Level = level + 1; - child.HasChildren = folders1.Any(item => item.ParentId == child.FolderId); + child.HasChildren = folders.Any(item => item.ParentId == child.FolderId); hierarchy.Add(child); - if (getPath != null) getPath(folderList, child); + getPath(folderList, child); } }; - folders = folders.OrderBy(item => item.Order).ToList(); + folders = folders.OrderBy(item => item.Name).ToList(); getPath(folders, null); // add any non-hierarchical items to the end of the list diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 0ad80eb8..534e05c3 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -88,10 +88,10 @@ namespace Oqtane.Controllers [HttpGet("upgrade")] [Authorize(Roles = RoleNames.Host)] - public Installation Upgrade() + public Installation Upgrade(string backup) { var installation = new Installation { Success = true, Message = "" }; - _installationManager.UpgradeFramework(); + _installationManager.UpgradeFramework(bool.Parse(backup)); return installation; } diff --git a/Oqtane.Server/Controllers/JobLogController.cs b/Oqtane.Server/Controllers/JobLogController.cs index 5f711e4f..dac2db68 100644 --- a/Oqtane.Server/Controllers/JobLogController.cs +++ b/Oqtane.Server/Controllers/JobLogController.cs @@ -17,12 +17,12 @@ namespace Oqtane.Controllers _jobLogs = jobLogs; } - // GET: api/ + // GET: api/?jobid=x [HttpGet] [Authorize(Roles = RoleNames.Host)] - public IEnumerable Get() + public IEnumerable Get(string jobid) { - return _jobLogs.GetJobLogs(); + return _jobLogs.GetJobLogs(int.Parse(jobid)); } // GET api//5 diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs index a7c09fcb..61f69f33 100644 --- a/Oqtane.Server/Controllers/ModuleController.cs +++ b/Oqtane.Server/Controllers/ModuleController.cs @@ -9,6 +9,7 @@ using Oqtane.Infrastructure; using Oqtane.Repository; using Oqtane.Security; using System.Net; +using System.IO; namespace Oqtane.Controllers { @@ -20,18 +21,22 @@ namespace Oqtane.Controllers private readonly IPageRepository _pages; private readonly IModuleDefinitionRepository _moduleDefinitions; private readonly ISettingRepository _settings; + private readonly IFolderRepository _folders; + private readonly IFileRepository _files; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; private readonly ILogManager _logger; private readonly Alias _alias; - public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) + public ModuleController(IModuleRepository modules, IPageModuleRepository pageModules, IPageRepository pages, IModuleDefinitionRepository moduleDefinitions, ISettingRepository settings, IFolderRepository folders, IFileRepository files, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger) { _modules = modules; _pageModules = pageModules; _pages = pages; _moduleDefinitions = moduleDefinitions; _settings = settings; + _folders = folders; + _files = files; _userPermissions = userPermissions; _syncManager = syncManager; _logger = logger; @@ -76,6 +81,8 @@ namespace Oqtane.Controllers module.ContainerType = pagemodule.ContainerType; module.EffectiveDate = pagemodule.EffectiveDate; module.ExpiryDate = pagemodule.ExpiryDate; + module.Header = pagemodule.Header; + module.Footer = pagemodule.Footer; module.ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == module.ModuleDefinitionName)); @@ -246,6 +253,61 @@ namespace Oqtane.Controllers return content; } + // POST api//export?moduleid=x&pageid=y&folderid=z&filename=a + [HttpPost("export")] + [Authorize(Roles = RoleNames.Registered)] + public int Export(int moduleid, int pageid, int folderid, string filename) + { + var fileid = -1; + var module = _modules.GetModule(moduleid); + if (module != null && module.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Page, pageid, PermissionNames.Edit) && + _userPermissions.IsAuthorized(User, module.SiteId, EntityNames.Folder, folderid, PermissionNames.Edit) && !string.IsNullOrEmpty(filename)) + { + // get content + var content = _modules.ExportModule(moduleid); + + // get folder + var folder = _folders.GetFolder(folderid, false); + string folderPath = _folders.GetFolderPath(folder); + if (!Directory.Exists(folderPath)) + { + Directory.CreateDirectory(folderPath); + } + + // create json file + filename = Utilities.GetFriendlyUrl(Path.GetFileNameWithoutExtension(filename)) + ".json"; + string filepath = Path.Combine(folderPath, filename); + if (System.IO.File.Exists(filepath)) + { + System.IO.File.Delete(filepath); + } + System.IO.File.WriteAllText(filepath, content); + + // register file + var file = _files.GetFile(folderid, filename); + if (file == null) + { + file = new Models.File { FolderId = folderid, Name = filename, Extension = "json", Size = (int)new FileInfo(filepath).Length, ImageWidth = 0, ImageHeight = 0 }; + _files.AddFile(file); + } + else + { + file.Size = (int)new FileInfo(filepath).Length; + _files.UpdateFile(file); + } + fileid = file.FileId; + + _logger.Log(LogLevel.Information, this, LogFunction.Read, "Content Exported For Module {ModuleId} To Folder {FolderId}", moduleid, folderid); + } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Export Attempt For Module {Module} To Folder {FolderId}", moduleid, folderid); + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + } + + return fileid; + } + // POST api//import?moduleid=x&pageid=y [HttpPost("import")] [Authorize(Roles = RoleNames.Registered)] diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs index c0b943bc..eb678a3b 100644 --- a/Oqtane.Server/Controllers/PackageController.cs +++ b/Oqtane.Server/Controllers/PackageController.cs @@ -90,33 +90,26 @@ namespace Oqtane.Controllers package = await GetJson(client, url + $"/api/registry/package/?id={_configManager.GetInstallationId()}&package={packageid}&version={version}&download={download}&email={WebUtility.UrlEncode(GetPackageRegistryEmail())}"); } - if (package != null) + if (package != null && bool.Parse(install)) { - if (bool.Parse(install)) + using (var httpClient = new HttpClient()) { - using (var httpClient = new HttpClient()) + var folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); + var response = await httpClient.GetAsync(package.PackageUrl).ConfigureAwait(false); + if (response.IsSuccessStatusCode) { - var folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); - var response = await httpClient.GetAsync(package.PackageUrl).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + string filename = packageid + "." + version + ".nupkg"; + using (var fileStream = new FileStream(Path.Combine(Constants.PackagesFolder, filename), FileMode.Create, FileAccess.Write, FileShare.None)) { - string filename = packageid + "." + version + ".nupkg"; - using (var fileStream = new FileStream(Path.Combine(Constants.PackagesFolder, filename), FileMode.Create, FileAccess.Write, FileShare.None)) - { - await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); - } - } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Create, "Could Not Download {PackageUrl}", package.PackageUrl); + await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); } } + else + { + _logger.Log(LogLevel.Error, this, LogFunction.Create, "Could Not Download {PackageUrl}", package.PackageUrl); + } } } - else - { - _logger.Log(LogLevel.Error, this, LogFunction.Create, "Package {PackageId}.{Version} Is Not Registered In The Marketplace", packageid, version); - } } return package; } diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index f8ad1925..6086bd0f 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -246,6 +246,10 @@ namespace Oqtane.Controllers pagemodule.Pane = pm.Pane; pagemodule.Order = pm.Order; pagemodule.ContainerType = pm.ContainerType; + pagemodule.EffectiveDate = pm.EffectiveDate; + pagemodule.ExpiryDate = pm.ExpiryDate; + pagemodule.Header = pm.Header; + pagemodule.Footer = pm.Footer; _pageModules.AddPageModule(pagemodule); } @@ -295,38 +299,43 @@ namespace Oqtane.Controllers var removed = GetPermissionsDifferences(currentPermissions, page.PermissionList); // synchronize module permissions - if (added.Count > 0 || removed.Count > 0) + if (page.UpdateModulePermissions && (added.Count > 0 || removed.Count > 0)) { - foreach (PageModule pageModule in _pageModules.GetPageModules(page.SiteId).Where(item => item.PageId == page.PageId).ToList()) + var pageModules = _pageModules.GetPageModules(page.SiteId); + foreach (PageModule pageModule in pageModules.Where(item => item.PageId == page.PageId).ToList()) { - var modulePermissions = _permissionRepository.GetPermissions(pageModule.Module.SiteId, EntityNames.Module, pageModule.Module.ModuleId).ToList(); - // permissions added - foreach (Permission permission in added) + // ignore "shared" modules + if (!pageModules.Any(item => item.ModuleId == pageModule.ModuleId && item.PageId != pageModule.PageId)) { - if (!modulePermissions.Any(item => item.PermissionName == permission.PermissionName - && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized)) + var modulePermissions = _permissionRepository.GetPermissions(pageModule.Module.SiteId, EntityNames.Module, pageModule.Module.ModuleId).ToList(); + // permissions added + foreach (Permission permission in added) { - _permissionRepository.AddPermission(new Permission + if (!modulePermissions.Any(item => item.PermissionName == permission.PermissionName + && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized)) { - SiteId = page.SiteId, - EntityName = EntityNames.Module, - EntityId = pageModule.ModuleId, - PermissionName = permission.PermissionName, - RoleId = permission.RoleId, - UserId = permission.UserId, - IsAuthorized = permission.IsAuthorized - }); + _permissionRepository.AddPermission(new Permission + { + SiteId = page.SiteId, + EntityName = EntityNames.Module, + EntityId = pageModule.ModuleId, + PermissionName = permission.PermissionName, + RoleId = permission.RoleId, + UserId = permission.UserId, + IsAuthorized = permission.IsAuthorized + }); + } } - } - // permissions removed - foreach (Permission permission in removed) - { - var modulePermission = modulePermissions.FirstOrDefault(item => item.PermissionName == permission.PermissionName - && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized); - if (modulePermission != null) + // permissions removed + foreach (Permission permission in removed) { - _permissionRepository.DeletePermission(modulePermission.PermissionId); + var modulePermission = modulePermissions.FirstOrDefault(item => item.PermissionName == permission.PermissionName + && item.RoleId == permission.RoleId && item.UserId == permission.UserId && item.IsAuthorized == permission.IsAuthorized); + if (modulePermission != null) + { + _permissionRepository.DeletePermission(modulePermission.PermissionId); + } } } } diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs index 2579d379..fd8708c1 100644 --- a/Oqtane.Server/Controllers/SettingController.cs +++ b/Oqtane.Server/Controllers/SettingController.cs @@ -24,26 +24,50 @@ namespace Oqtane.Controllers private readonly IPageModuleRepository _pageModules; private readonly IUserPermissions _userPermissions; private readonly ISyncManager _syncManager; - private readonly IAliasAccessor _aliasAccessor; - private readonly IOptionsMonitorCache _cookieCache; - private readonly IOptionsMonitorCache _oidcCache; - private readonly IOptionsMonitorCache _oauthCache; - private readonly IOptionsMonitorCache _identityCache; + + private readonly IOptions _cookieOptions; + private readonly IOptionsSnapshot _cookieOptionsSnapshot; + private readonly IOptionsMonitorCache _cookieOptionsMonitorCache; + + private readonly IOptions _oidcOptions; + private readonly IOptionsSnapshot _oidcOptionsSnapshot; + private readonly IOptionsMonitorCache _oidcOptionsMonitorCache; + + private readonly IOptions _oauthOptions; + private readonly IOptionsSnapshot _oauthOptionsSnapshot; + private readonly IOptionsMonitorCache _oauthOptionsMonitorCache; + + private readonly IOptions _identityOptions; + private readonly IOptionsSnapshot _identityOptionsSnapshot; + private readonly IOptionsMonitorCache _identityOptionsMonitorCache; + private readonly ILogManager _logger; private readonly Alias _alias; private readonly string _visitorCookie; - public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IAliasAccessor aliasAccessor, IOptionsMonitorCache cookieCache, IOptionsMonitorCache oidcCache, IOptionsMonitorCache oauthCache, IOptionsMonitorCache identityCache, ILogManager logger) + public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, + IOptions cookieOptions, IOptionsSnapshot cookieOptionsSnapshot, IOptionsMonitorCache cookieOptionsMonitorCache, + IOptions oidcOptions, IOptionsSnapshot oidcOptionsSnapshot, IOptionsMonitorCache oidcOptionsMonitorCache, + IOptions oauthOptions, IOptionsSnapshot oauthOptionsSnapshot, IOptionsMonitorCache oauthOptionsMonitorCache, + IOptions identityOptions, IOptionsSnapshot identityOptionsSnapshot, IOptionsMonitorCache identityOptionsMonitorCache, + ILogManager logger) { _settings = settings; _pageModules = pageModules; _userPermissions = userPermissions; _syncManager = syncManager; - _aliasAccessor = aliasAccessor; - _cookieCache = cookieCache; - _oidcCache = oidcCache; - _oauthCache = oauthCache; - _identityCache = identityCache; + _cookieOptions = cookieOptions; + _cookieOptionsSnapshot = cookieOptionsSnapshot; + _cookieOptionsMonitorCache = cookieOptionsMonitorCache; + _oidcOptions = oidcOptions; + _oidcOptionsSnapshot = oidcOptionsSnapshot; + _oidcOptionsMonitorCache = oidcOptionsMonitorCache; + _oauthOptions = oauthOptions; + _oauthOptionsSnapshot = oauthOptionsSnapshot; + _oauthOptionsMonitorCache = oauthOptionsMonitorCache; + _identityOptions = identityOptions; + _identityOptionsSnapshot = identityOptionsSnapshot; + _identityOptionsMonitorCache = identityOptionsMonitorCache; _logger = logger; _alias = tenantManager.GetAlias(); _visitorCookie = Constants.VisitorCookiePrefix + _alias.SiteId.ToString(); @@ -210,21 +234,21 @@ namespace Oqtane.Controllers [Authorize(Roles = RoleNames.Admin)] public void Clear() { - // clear SiteOptionsCache for each option type - var cookieCache = new SiteOptionsCache(_aliasAccessor); - cookieCache.Clear(); - var oidcCache = new SiteOptionsCache(_aliasAccessor); - oidcCache.Clear(); - var oauthCache = new SiteOptionsCache(_aliasAccessor); - oauthCache.Clear(); - var identityCache = new SiteOptionsCache(_aliasAccessor); - identityCache.Clear(); + (_cookieOptions as SiteOptionsManager).Reset(); + (_cookieOptionsSnapshot as SiteOptionsManager).Reset(); + _cookieOptionsMonitorCache.Clear(); - // clear IOptionsMonitorCache for each option type - _cookieCache.Clear(); - _oidcCache.Clear(); - _oauthCache.Clear(); - _identityCache.Clear(); + (_oidcOptions as SiteOptionsManager).Reset(); + (_oidcOptionsSnapshot as SiteOptionsManager).Reset(); + _oidcOptionsMonitorCache.Clear(); + + (_oauthOptions as SiteOptionsManager).Reset(); + (_oauthOptionsSnapshot as SiteOptionsManager).Reset(); + _oauthOptionsMonitorCache.Clear(); + + (_identityOptions as SiteOptionsManager).Reset(); + (_identityOptionsSnapshot as SiteOptionsManager).Reset(); + _identityOptionsMonitorCache.Clear(); _logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared"); } diff --git a/Oqtane.Server/Controllers/TimeZoneController.cs b/Oqtane.Server/Controllers/TimeZoneController.cs new file mode 100644 index 00000000..158d9f72 --- /dev/null +++ b/Oqtane.Server/Controllers/TimeZoneController.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Oqtane.Models; +using Oqtane.Shared; + +namespace Oqtane.Controllers +{ + [Route(ControllerRoutes.ApiRoute)] + public class TimeZoneController : Controller + { + public TimeZoneController() {} + + // GET: api/ + [HttpGet] + public IEnumerable Get() + { + return TimeZoneInfo.GetSystemTimeZones() + .Select(item => new Models.TimeZone + { + Id = item.Id, + DisplayName = item.DisplayName + }) + .OrderBy(item => item.DisplayName); + } + } +} diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 92aef5c5..859bd50c 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -131,14 +131,16 @@ namespace Oqtane.Controllers filtered.TwoFactorCode = ""; filtered.SecurityStamp = ""; - // include private properties if authenticated user is accessing their own user account os is an administrator + // include private properties if authenticated user is accessing their own user account or is an administrator if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || _userPermissions.GetUser(User).UserId == user.UserId) { filtered.Email = user.Email; + filtered.TimeZoneId = user.TimeZoneId; filtered.PhotoFileId = user.PhotoFileId; filtered.LastLoginOn = user.LastLoginOn; filtered.LastIPAddress = user.LastIPAddress; filtered.TwoFactorRequired = user.TwoFactorRequired; + filtered.EmailConfirmed = user.EmailConfirmed; filtered.Roles = user.Roles; filtered.CreatedBy = user.CreatedBy; filtered.CreatedOn = user.CreatedOn; @@ -199,10 +201,15 @@ namespace Oqtane.Controllers [Authorize] public async Task Put(int id, [FromBody] User user) { - if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null + var existing = _userManager.GetUser(user.UserId, user.SiteId); + if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null && (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username)) { - user.EmailConfirmed = User.IsInRole(RoleNames.Admin); + // only authorized users can update the email confirmation + if (!_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin)) + { + user.EmailConfirmed = existing.EmailConfirmed; + } user = await _userManager.UpdateUser(user); } else diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs index 9b4b09e9..98a392cc 100644 --- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs @@ -104,6 +104,7 @@ namespace Microsoft.Extensions.DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // providers services.AddScoped(); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 0e58e4b6..e98a0afa 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -532,8 +532,9 @@ namespace Oqtane.Extensions // external roles if (claimsPrincipal.Claims.Any(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) { - var _roles = httpContext.RequestServices.GetRequiredService(); - var roles = _roles.GetRoles(user.SiteId).ToList(); // global roles excluded ie. host users cannot be added/deleted + var _roles = httpContext.RequestServices.GetRequiredService(); + var allowhostrole = bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:AllowHostRole", "false")); + var roles = _roles.GetRoles(user.SiteId, allowhostrole).ToList(); var mappings = httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimMappings", "").Split(','); foreach (var claim in claimsPrincipal.Claims.Where(item => item.Type == httpContext.GetSiteSettings().GetValue("ExternalLogin:RoleClaimType", ""))) @@ -583,8 +584,9 @@ namespace Oqtane.Extensions } } - var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered); - if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate)) + var host = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Host); + var registered = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered); + if (!user.IsDeleted && (host != null || registered != null && Utilities.IsEffectiveAndNotExpired(registered.EffectiveDate, registered.ExpiryDate))) { // update user user.LastLoginOn = DateTime.UtcNow; diff --git a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs index 6234d2fb..d00c01ae 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteIdentityBuilderExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Oqtane.Models; using Microsoft.AspNetCore.Identity; using System; diff --git a/Oqtane.Server/Infrastructure/DatabaseManager.cs b/Oqtane.Server/Infrastructure/DatabaseManager.cs index 2273cded..1ee2a138 100644 --- a/Oqtane.Server/Infrastructure/DatabaseManager.cs +++ b/Oqtane.Server/Infrastructure/DatabaseManager.cs @@ -731,6 +731,7 @@ namespace Oqtane.Infrastructure { _configManager.AddOrUpdateSetting($"{SettingKeys.DatabaseSection}:{SettingKeys.DatabaseTypeKey}", Constants.DefaultDBType, true); } + if (!_configManager.GetSection(SettingKeys.AvailableDatabasesSection).Exists()) { string databases = "["; @@ -742,6 +743,19 @@ namespace Oqtane.Infrastructure databases += "]"; _configManager.AddOrUpdateSetting(SettingKeys.AvailableDatabasesSection, databases, true); } + var availabledatabases = _configManager.GetSection(SettingKeys.AvailableDatabasesSection).GetChildren(); + if (!availabledatabases.Any(item => item.GetSection("Name").Value == "Azure SQL")) + { + // Azure SQL added in 6.1.2 + string databases = "["; + foreach (var database in availabledatabases) + { + databases += "{ " + $"\"Name\": \"{database["Name"]}\", \"ControlType\": \"{database["ControlType"]}\", \"DBTYpe\": \"{database["DBType"]}\"" + " },"; + } + databases += "{ \"Name\": \"Azure SQL\", \"ControlType\": \"Oqtane.Installer.Controls.AzureSqlConfig, Oqtane.Client\", \"DBTYpe\": \"Oqtane.Database.SqlServer.SqlServerDatabase, Oqtane.Database.SqlServer\" }"; + databases += "]"; + _configManager.AddOrUpdateSetting(SettingKeys.AvailableDatabasesSection, databases, true); + } } } } diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs index fdfca028..1591c150 100644 --- a/Oqtane.Server/Infrastructure/InstallationManager.cs +++ b/Oqtane.Server/Infrastructure/InstallationManager.cs @@ -380,7 +380,7 @@ namespace Oqtane.Infrastructure File.WriteAllText(assemblyLogPath, JsonSerializer.Serialize(assemblies, new JsonSerializerOptions { WriteIndented = true })); } - public async Task UpgradeFramework() + public async Task UpgradeFramework(bool backup) { string folder = Path.Combine(_environment.ContentRootPath, Constants.PackagesFolder); if (Directory.Exists(folder)) @@ -448,14 +448,14 @@ namespace Oqtane.Infrastructure // install Oqtane.Upgrade zip package if (File.Exists(upgradepackage)) { - FinishUpgrade(); + FinishUpgrade(backup); } } } } } - private void FinishUpgrade() + private void FinishUpgrade(bool backup) { // check if updater application exists string Updater = Constants.UpdaterPackageId + ".dll"; @@ -469,7 +469,7 @@ namespace Oqtane.Infrastructure { WorkingDirectory = folder, FileName = "dotnet", - Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\"", + Arguments = Path.Combine(folder, Updater) + " \"" + _environment.ContentRootPath + "\" \"" + _environment.WebRootPath + "\" \"" + backup.ToString() + "\"", UseShellExecute = false, ErrorDialog = false, CreateNoWindow = true, diff --git a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs index 0e5bcc6d..1d1648c7 100644 --- a/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs +++ b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs @@ -7,7 +7,7 @@ namespace Oqtane.Infrastructure void InstallPackages(); bool UninstallPackage(string PackageName); int RegisterAssemblies(); - Task UpgradeFramework(); + Task UpgradeFramework(bool backup); void RestartApplication(); } } diff --git a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs index 5c8fc04f..655c7b20 100644 --- a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs +++ b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs @@ -46,6 +46,8 @@ namespace Oqtane.Infrastructure protected async Task ExecuteAsync(CancellationToken stoppingToken) { + await Task.Yield(); // required so that this method does not block startup + while (!stoppingToken.IsCancellationRequested) { using (var scope = _serviceScopeFactory.CreateScope()) @@ -170,8 +172,7 @@ namespace Oqtane.Infrastructure jobs.UpdateJob(job); // trim the job log - List logs = jobLogs.GetJobLogs().Where(item => item.JobId == job.JobId) - .OrderByDescending(item => item.JobLogId).ToList(); + List logs = jobLogs.GetJobLogs(job.JobId).ToList(); for (int i = logs.Count; i > job.RetentionHistory; i--) { jobLogs.DeleteJobLog(logs[i - 1].JobLogId); diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs index ab9cc058..4147455b 100644 --- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs @@ -40,27 +40,26 @@ namespace Oqtane.Infrastructure log += "Processing Notifications For Site: " + site.Name + "
"; // get site settings - List sitesettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList(); - Dictionary settings = GetSettings(sitesettings); - if (!site.IsDeleted && (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True")) + var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1); + + if (!site.IsDeleted && settingRepository.GetSettingValue(settings, "SMTPEnabled", "True") == "True") { - if (settings.ContainsKey("SMTPHost") && settings["SMTPHost"] != "" && - settings.ContainsKey("SMTPPort") && settings["SMTPPort"] != "" && - settings.ContainsKey("SMTPSSL") && settings["SMTPSSL"] != "" && - settings.ContainsKey("SMTPSender") && settings["SMTPSender"] != "") + if (settingRepository.GetSettingValue(settings, "SMTPHost", "") != "" && + settingRepository.GetSettingValue(settings, "SMTPPort", "") != "" && + settingRepository.GetSettingValue(settings, "SMTPSender", "") != "") { // construct SMTP Client var client = new SmtpClient() { DeliveryMethod = SmtpDeliveryMethod.Network, UseDefaultCredentials = false, - Host = settings["SMTPHost"], - Port = int.Parse(settings["SMTPPort"]), - EnableSsl = bool.Parse(settings["SMTPSSL"]) + Host = settingRepository.GetSettingValue(settings, "SMTPHost", ""), + Port = int.Parse(settingRepository.GetSettingValue(settings, "SMTPPort", "")), + EnableSsl = bool.Parse(settingRepository.GetSettingValue(settings, "SMTPSSL", "False")) }; - if (settings["SMTPUsername"] != "" && settings["SMTPPassword"] != "") + if (settingRepository.GetSettingValue(settings, "SMTPUsername", "") != "" && settingRepository.GetSettingValue(settings, "SMTPPassword", "") != "") { - client.Credentials = new NetworkCredential(settings["SMTPUsername"], settings["SMTPPassword"]); + client.Credentials = new NetworkCredential(settingRepository.GetSettingValue(settings, "SMTPUsername", ""), settingRepository.GetSettingValue(settings, "SMTPPassword", "")); } // iterate through undelivered notifications @@ -100,7 +99,7 @@ namespace Oqtane.Infrastructure MailMessage mailMessage = new MailMessage(); // sender - if (settings.ContainsKey("SMTPRelay") && settings["SMTPRelay"] == "True" && !string.IsNullOrEmpty(notification.FromEmail)) + if (settingRepository.GetSettingValue(settings, "SMTPRelay", "False") == "True" && !string.IsNullOrEmpty(notification.FromEmail)) { if (!string.IsNullOrEmpty(notification.FromDisplayName)) { @@ -113,7 +112,7 @@ namespace Oqtane.Infrastructure } else { - mailMessage.From = new MailAddress(settings["SMTPSender"], (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name); + mailMessage.From = new MailAddress(settingRepository.GetSettingValue(settings, "SMTPSender", ""), (!string.IsNullOrEmpty(notification.FromDisplayName)) ? notification.FromDisplayName : site.Name); } // recipient @@ -130,7 +129,12 @@ namespace Oqtane.Infrastructure mailMessage.Subject = notification.Subject; //body - mailMessage.Body = notification.Body.Replace("\n", "
"); + mailMessage.Body = notification.Body; + if (!mailMessage.Body.Contains("<") || !mailMessage.Body.Contains(">")) + { + // plain text messages should convert line breaks to HTML tags to preserve formatting + mailMessage.Body = mailMessage.Body.Replace("\n", "
"); + } // encoding mailMessage.SubjectEncoding = System.Text.Encoding.UTF8; @@ -157,7 +161,7 @@ namespace Oqtane.Infrastructure } else { - log += "SMTP Not Configured Properly In Site Settings - Host, Port, SSL, And Sender Are All Required" + "
"; + log += "SMTP Not Configured Properly In Site Settings - Host, Port, And Sender Are All Required" + "
"; } } else @@ -168,15 +172,5 @@ namespace Oqtane.Infrastructure return log; } - - private Dictionary GetSettings(List settings) - { - Dictionary dictionary = new Dictionary(); - foreach (Setting setting in settings.OrderBy(item => item.SettingName).ToList()) - { - dictionary.Add(setting.SettingName, setting.SettingValue); - } - return dictionary; - } } } diff --git a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs index 5cf4c59c..89a2238d 100644 --- a/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs +++ b/Oqtane.Server/Infrastructure/Jobs/PurgeJob.cs @@ -40,18 +40,13 @@ namespace Oqtane.Infrastructure foreach (Site site in sites) { log += "
Processing Site: " + site.Name + "
"; - int retention; int count; // get site settings - Dictionary settings = GetSettings(settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList()); + var settings = settingRepository.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1); // purge event log - retention = 30; // 30 days - if (settings.ContainsKey("LogRetention") && !string.IsNullOrEmpty(settings["LogRetention"])) - { - retention = int.Parse(settings["LogRetention"]); - } + var retention = int.Parse(settingRepository.GetSettingValue(settings, "LogRetention", "30")); // 30 day default try { count = logRepository.DeleteLogs(site.SiteId, retention); @@ -65,11 +60,7 @@ namespace Oqtane.Infrastructure // purge visitors if (site.VisitorTracking) { - retention = 30; // 30 days - if (settings.ContainsKey("VisitorRetention") && !string.IsNullOrEmpty(settings["VisitorRetention"])) - { - retention = int.Parse(settings["VisitorRetention"]); - } + retention = int.Parse(settingRepository.GetSettingValue(settings, "VisitorRetention", "30")); // 30 day default try { count = visitorRepository.DeleteVisitors(site.SiteId, retention); @@ -82,11 +73,7 @@ namespace Oqtane.Infrastructure } // purge notifications - retention = 30; // 30 days - if (settings.ContainsKey("NotificationRetention") && !string.IsNullOrEmpty(settings["NotificationRetention"])) - { - retention = int.Parse(settings["NotificationRetention"]); - } + retention = int.Parse(settingRepository.GetSettingValue(settings, "NotificationRetention", "30")); // 30 day default try { count = notificationRepository.DeleteNotifications(site.SiteId, retention); @@ -98,11 +85,7 @@ namespace Oqtane.Infrastructure } // purge broken urls - retention = 30; // 30 days - if (settings.ContainsKey("UrlMappingRetention") && !string.IsNullOrEmpty(settings["UrlMappingRetention"])) - { - retention = int.Parse(settings["UrlMappingRetention"]); - } + retention = int.Parse(settingRepository.GetSettingValue(settings, "UrlMappingRetention", "30")); // 30 day default try { count = urlMappingRepository.DeleteUrlMappings(site.SiteId, retention); @@ -127,15 +110,5 @@ namespace Oqtane.Infrastructure return log; } - - private Dictionary GetSettings(List settings) - { - Dictionary dictionary = new Dictionary(); - foreach (Setting setting in settings.OrderBy(item => item.SettingName).ToList()) - { - dictionary.Add(setting.SettingName, setting.SettingValue); - } - return dictionary; - } } } diff --git a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs index 4af3d6c2..13b74176 100644 --- a/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs +++ b/Oqtane.Server/Infrastructure/Middleware/TenantMiddleware.cs @@ -23,10 +23,6 @@ namespace Oqtane.Infrastructure var config = context.RequestServices.GetService(typeof(IConfigManager)) as IConfigManager; string path = context.Request.Path.ToString(); - // note that in order to support Alias subfolders we used to ignore Blazor framework requests... - // but this does not work in static rendering as the web UI request originates from /_blazor - //if (config.IsInstalled() && !path.StartsWith("/_")) - if (config.IsInstalled()) { // get alias (note that this also sets SiteState.Alias) @@ -43,7 +39,7 @@ namespace Oqtane.Infrastructure var sitesettings = cache.GetOrCreate(Constants.HttpContextSiteSettingsKey + alias.SiteKey, entry => { var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository; - return settingRepository.GetSettings(EntityNames.Site, alias.SiteId) + return settingRepository.GetSettings(EntityNames.Site, alias.SiteId, EntityNames.Host, -1) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); }); context.Items.Add(Constants.HttpContextSiteSettingsKey, sitesettings); @@ -74,8 +70,21 @@ namespace Oqtane.Infrastructure // handle robots.txt root request (does not support subfolder aliases) if (context.Request.Path.StartsWithSegments("/robots.txt") && string.IsNullOrEmpty(alias.Path)) { - // allow all user agents and specify site map - var robots = $"User-agent: *\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml"; + string robots = ""; + if (sitesettings.ContainsKey("Robots") && !string.IsNullOrEmpty(sitesettings["Robots"])) + { + robots = sitesettings["Robots"]; + } + else + { + // allow all user agents by default + robots = $"User-agent: *"; + } + if (!robots.ToLower().Contains("Sitemap:")) + { + // add sitemap if not specified + robots += $"\n\nSitemap: {context.Request.Scheme}://{alias.Name}/sitemap.xml"; + } context.Response.ContentType = "text/plain"; await context.Response.WriteAsync(robots); return; diff --git a/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs index e4737d9a..5c802fa9 100644 --- a/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs +++ b/Oqtane.Server/Infrastructure/Options/SiteOptionsCache.cs @@ -19,7 +19,6 @@ namespace Oqtane.Infrastructure { var cache = map.GetOrAdd(GetKey(), new OptionsCache()); cache.Clear(); - } public TOptions GetOrAdd(string name, Func createOptions) diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs index 7727e9aa..ddd1e784 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/AdminSiteTemplate.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Microsoft.Extensions.Localization; using Oqtane.Documentation; -using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Shared; @@ -182,6 +181,7 @@ namespace Oqtane.Infrastructure.SiteTemplates Name = "Privacy", Parent = "", Path = "privacy", + Order = seed + 11, Icon = Icons.Eye, IsNavigation = false, IsPersonalizable = false, @@ -212,6 +212,7 @@ namespace Oqtane.Infrastructure.SiteTemplates Name = "Terms", Parent = "", Path = "terms", + Order = seed + 13, Icon = Icons.List, IsNavigation = false, IsPersonalizable = false, @@ -242,7 +243,7 @@ namespace Oqtane.Infrastructure.SiteTemplates Name = "Not Found", Parent = "", Path = "404", - Order = seed + 11, + Order = seed + 15, Icon = Icons.X, IsNavigation = false, IsPersonalizable = false, diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index 00498def..9521469c 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using Microsoft.AspNetCore.Hosting; using Oqtane.Documentation; -using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Repository; using Oqtane.Shared; diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs index 7cc0eb07..f35f1ab2 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/EmptySiteTemplate.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using Oqtane.Documentation; -using Oqtane.Infrastructure; using Oqtane.Models; using Oqtane.Shared; diff --git a/Oqtane.Server/Infrastructure/UpgradeManager.cs b/Oqtane.Server/Infrastructure/UpgradeManager.cs index 409ab59c..421be775 100644 --- a/Oqtane.Server/Infrastructure/UpgradeManager.cs +++ b/Oqtane.Server/Infrastructure/UpgradeManager.cs @@ -473,6 +473,7 @@ namespace Oqtane.Infrastructure Name = "Privacy", Parent = "", Path = "privacy", + Order = 1011, Icon = Icons.Eye, IsNavigation = false, IsPersonalizable = false, @@ -502,6 +503,7 @@ namespace Oqtane.Infrastructure Name = "Terms", Parent = "", Path = "terms", + Order = 1013, Icon = Icons.List, IsNavigation = false, IsPersonalizable = false, diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index 84679d23..5e1e6e64 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -65,7 +65,12 @@ namespace Oqtane.Managers { user.SiteId = siteid; user.Roles = GetUserRoles(user.UserId, user.SiteId); - user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp; + var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult(); + if (identityuser != null) + { + user.SecurityStamp = identityuser.SecurityStamp; + user.EmailConfirmed = identityuser.EmailConfirmed; + } user.Settings = _settings.GetSettings(EntityNames.User, user.UserId) .ToDictionary(setting => setting.SettingName, setting => setting.SettingValue); } @@ -245,22 +250,30 @@ namespace Oqtane.Managers { identityuser.Email = user.Email; await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated - - // if email address changed and it is not confirmed, verification is required for new email address - if (!user.EmailConfirmed) - { - string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); - string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; - var notification = new Notification(user.SiteId, user, "User Account Verification", body); - _notifications.AddNotification(notification); - } } if (user.EmailConfirmed) { - var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); - await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + if (!identityuser.EmailConfirmed) + { + var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken); + + string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password."; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); + } + } + else + { + identityuser.EmailConfirmed = false; + await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated + + string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser); + string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token); + string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!"; + var notification = new Notification(user.SiteId, user, "User Account Verification", body); + _notifications.AddNotification(notification); } user = _users.UpdateUser(user); diff --git a/Oqtane.Server/Migrations/Tenant/06010301_AddTimeZone.cs b/Oqtane.Server/Migrations/Tenant/06010301_AddTimeZone.cs new file mode 100644 index 00000000..0e7d40c0 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/06010301_AddTimeZone.cs @@ -0,0 +1,31 @@ +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.03.01")] + public class AddTimeZone : MultiDatabaseMigration + { + public AddTimeZone(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var siteEntityBuilder = new SiteEntityBuilder(migrationBuilder, ActiveDatabase); + siteEntityBuilder.AddStringColumn("TimeZoneId", 50, true); + + var userEntityBuilder = new UserEntityBuilder(migrationBuilder, ActiveDatabase); + userEntityBuilder.AddStringColumn("TimeZoneId", 50, true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Migrations/Tenant/06010302_AddModuleHeaderFooter.cs b/Oqtane.Server/Migrations/Tenant/06010302_AddModuleHeaderFooter.cs new file mode 100644 index 00000000..39e01287 --- /dev/null +++ b/Oqtane.Server/Migrations/Tenant/06010302_AddModuleHeaderFooter.cs @@ -0,0 +1,29 @@ +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.03.02")] + public class AddModuleHeaderFooter : MultiDatabaseMigration + { + public AddModuleHeaderFooter(IDatabase database) : base(database) + { + } + + protected override void Up(MigrationBuilder migrationBuilder) + { + var pageModuleEntityBuilder = new PageModuleEntityBuilder(migrationBuilder, ActiveDatabase); + pageModuleEntityBuilder.AddMaxStringColumn("Header", true); + pageModuleEntityBuilder.AddMaxStringColumn("Footer", true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + // not implemented + } + } +} diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index a7205ca8..2068c0ea 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.1 + 6.1.3 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -34,21 +34,21 @@
- - - - - + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + - - - + + + diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index b241388e..f01cd0b7 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -112,7 +112,7 @@ namespace Oqtane.Pages url += Request.QueryString.Value.Substring(1); } - + return RedirectPermanent(url); } @@ -133,10 +133,29 @@ namespace Oqtane.Pages } } - string etag; + string etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16); string downloadName = file.Name; string filepath = _files.GetFilePath(file); + var header = ""; + if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) + { + header = ifNoneMatch.ToString(); + } + + if (header.Equals(etag)) + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified; + return Content(String.Empty); + } + + if (!System.IO.File.Exists(filepath)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); + HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; + return BrokenFile(); + } + // evaluate any querystring parameters bool isRequestingImageManipulation = false; @@ -165,34 +184,6 @@ namespace Oqtane.Pages isRequestingImageManipulation = true; } - if (isRequestingImageManipulation) - { - etag = Utilities.GenerateSimpleHash(Request.QueryString.Value); - } - else - { - etag = Convert.ToString(file.ModifiedOn.Ticks ^ file.Size, 16); - } - - var header = ""; - if (HttpContext.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var ifNoneMatch)) - { - header = ifNoneMatch.ToString(); - } - - if (header.Equals(etag)) - { - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified; - return Content(String.Empty); - } - - if (!System.IO.File.Exists(filepath)) - { - _logger.Log(LogLevel.Error, this, LogFunction.Read, "File Does Not Exist {FilePath}", filepath); - HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound; - return BrokenFile(); - } - if (isRequestingImageManipulation) { var _ImageFiles = _settingRepository.GetSetting(EntityNames.Site, _alias.SiteId, "ImageFiles")?.SettingValue; diff --git a/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs b/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs index d5858da5..2c092e7a 100644 --- a/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Oqtane.Models; namespace Oqtane.Repository @@ -6,6 +6,7 @@ namespace Oqtane.Repository public interface IJobLogRepository { IEnumerable GetJobLogs(); + IEnumerable GetJobLogs(int jobId); JobLog AddJobLog(JobLog jobLog); JobLog UpdateJobLog(JobLog jobLog); JobLog GetJobLog(int jobLogId); diff --git a/Oqtane.Server/Repository/Interfaces/ISettingRepository.cs b/Oqtane.Server/Repository/Interfaces/ISettingRepository.cs index 5c231806..e74e8eda 100644 --- a/Oqtane.Server/Repository/Interfaces/ISettingRepository.cs +++ b/Oqtane.Server/Repository/Interfaces/ISettingRepository.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Oqtane.Models; +using Oqtane.Shared; namespace Oqtane.Repository { @@ -7,11 +8,13 @@ namespace Oqtane.Repository { IEnumerable GetSettings(string entityName); IEnumerable GetSettings(string entityName, int entityId); + IEnumerable GetSettings(string entityName1, int entityId1, string entityName2, int entityId2); Setting AddSetting(Setting setting); Setting UpdateSetting(Setting setting); Setting GetSetting(string entityName, int settingId); Setting GetSetting(string entityName, int entityId, string settingName); void DeleteSetting(string entityName, int settingId); void DeleteSettings(string entityName, int entityId); + string GetSettingValue(IEnumerable settings, string settingName, string defaultValue); } } diff --git a/Oqtane.Server/Repository/JobLogRepository.cs b/Oqtane.Server/Repository/JobLogRepository.cs index 440a483d..ee234c27 100644 --- a/Oqtane.Server/Repository/JobLogRepository.cs +++ b/Oqtane.Server/Repository/JobLogRepository.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using Oqtane.Models; @@ -15,9 +15,17 @@ namespace Oqtane.Repository } public IEnumerable GetJobLogs() + { + return GetJobLogs(-1); + } + + public IEnumerable GetJobLogs(int jobId) { return _db.JobLog + .AsNoTracking() + .Where(item => item.JobId == jobId || jobId == -1) .Include(item => item.Job) // eager load jobs + .OrderByDescending(item => item.JobLogId) .ToList(); } diff --git a/Oqtane.Server/Repository/SettingRepository.cs b/Oqtane.Server/Repository/SettingRepository.cs index 0ca28d50..735ec981 100644 --- a/Oqtane.Server/Repository/SettingRepository.cs +++ b/Oqtane.Server/Repository/SettingRepository.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Oqtane.Infrastructure; @@ -39,24 +38,27 @@ namespace Oqtane.Repository public IEnumerable GetSettings(string entityName, int entityId) { - var settings = GetSettings(entityName).ToList(); - if (entityName == EntityNames.Site) + return GetSettings(entityName).Where(item => item.EntityId == entityId); + } + + public IEnumerable GetSettings(string entityName1, int entityId1, string entityName2, int entityId2) + { + // merge settings from entity2 into entity1 + var settings1 = GetSettings(entityName1, entityId1).ToList(); + foreach (var setting2 in GetSettings(entityName2, entityId2)) { - // site settings can be overridden by host settings - var hostsettings = GetSettings(EntityNames.Host); - foreach (var hostsetting in hostsettings) + var setting1 = settings1.FirstOrDefault(item => item.SettingName == setting2.SettingName); + if (setting1 == null) { - if (settings.Any(item => item.SettingName == hostsetting.SettingName)) - { - settings.First(item => item.SettingName == hostsetting.SettingName).SettingValue = hostsetting.SettingValue; - } - else - { - settings.Add(new Setting { SettingId = -1, EntityName = entityName, EntityId = entityId, SettingName = hostsetting.SettingName, SettingValue = hostsetting.SettingValue, IsPrivate = hostsetting.IsPrivate }); - } + settings1.Add(new Setting { EntityName = entityName1, EntityId = entityId1, SettingName = setting2.SettingName, SettingValue = setting2.SettingValue, IsPrivate = setting2.IsPrivate }); + } + else + { + setting1.SettingValue = setting2.SettingValue; + setting1.IsPrivate = setting2.IsPrivate; } } - return settings.Where(item => item.EntityId == entityId); + return settings1; } public Setting AddSetting(Setting setting) @@ -165,6 +167,19 @@ namespace Oqtane.Repository ManageCache(entityName); } + public string GetSettingValue(IEnumerable settings, string settingName, string defaultValue) + { + var setting = settings.FirstOrDefault(item => item.SettingName == settingName); + if (setting != null) + { + return setting.SettingValue; + } + else + { + return defaultValue; + } + } + private bool IsMaster(string EntityName) { return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host); diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index 10de3576..48620ead 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -442,6 +442,8 @@ namespace Oqtane.Repository pageModule.Pane = (string.IsNullOrEmpty(pageTemplateModule.Pane)) ? PaneNames.Default : pageTemplateModule.Pane; pageModule.Order = (pageTemplateModule.Order == 0) ? 1 : pageTemplateModule.Order; pageModule.ContainerType = pageTemplateModule.ContainerType; + pageModule.Header = pageTemplateModule.Header; + pageModule.Footer = pageTemplateModule.Footer; pageModule.IsDeleted = pageTemplateModule.IsDeleted; pageModule.Module.PermissionList = new List(); foreach (var permission in pageTemplateModule.PermissionList) diff --git a/Oqtane.Server/Repository/UserRoleRepository.cs b/Oqtane.Server/Repository/UserRoleRepository.cs index 8af62274..6956a20d 100644 --- a/Oqtane.Server/Repository/UserRoleRepository.cs +++ b/Oqtane.Server/Repository/UserRoleRepository.cs @@ -61,6 +61,9 @@ namespace Oqtane.Repository public UserRole AddUserRole(UserRole userRole) { + userRole.EffectiveDate = userRole.EffectiveDate.HasValue ? DateTime.SpecifyKind(userRole.EffectiveDate.Value, DateTimeKind.Utc) : userRole.EffectiveDate; + userRole.ExpiryDate = userRole.ExpiryDate.HasValue ? DateTime.SpecifyKind(userRole.ExpiryDate.Value, DateTimeKind.Utc) : userRole.ExpiryDate; + using var db = _dbContextFactory.CreateDbContext(); db.UserRole.Add(userRole); db.SaveChanges(); @@ -84,6 +87,9 @@ namespace Oqtane.Repository public UserRole UpdateUserRole(UserRole userRole) { + userRole.EffectiveDate = userRole.EffectiveDate.HasValue ? DateTime.SpecifyKind(userRole.EffectiveDate.Value, DateTimeKind.Utc) : userRole.EffectiveDate; + userRole.ExpiryDate = userRole.ExpiryDate.HasValue ? DateTime.SpecifyKind(userRole.ExpiryDate.Value, DateTimeKind.Utc) : userRole.ExpiryDate; + using var db = _dbContextFactory.CreateDbContext(); db.Entry(userRole).State = EntityState.Modified; db.SaveChanges(); diff --git a/Oqtane.Server/Services/SiteService.cs b/Oqtane.Server/Services/SiteService.cs index 11c624c3..f316b766 100644 --- a/Oqtane.Server/Services/SiteService.cs +++ b/Oqtane.Server/Services/SiteService.cs @@ -111,7 +111,7 @@ namespace Oqtane.Services if (site != null && site.SiteId == alias.SiteId) { // site settings - site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId) + site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId, EntityNames.Host, -1) .ToDictionary(setting => setting.SettingName, setting => (setting.IsPrivate ? _private : "") + setting.SettingValue); // populate file extensions @@ -285,6 +285,8 @@ namespace Oqtane.Services ContainerType = pagemodule.ContainerType, EffectiveDate = pagemodule.EffectiveDate, ExpiryDate = pagemodule.ExpiryDate, + Header = pagemodule.Header, + Footer = pagemodule.Footer, ModuleDefinition = _moduleDefinitions.FilterModuleDefinition(moduledefinitions.Find(item => item.ModuleDefinitionName == pagemodule.Module.ModuleDefinitionName)), diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 319d1471..2101a861 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -70,7 +70,6 @@ namespace Oqtane services.AddLocalization(options => options.ResourcesPath = "Resources"); services.AddOptions>().Bind(Configuration.GetSection(SettingKeys.AvailableDatabasesSection)); - services.Configure(opts => opts.ShutdownTimeout = TimeSpan.FromSeconds(10)); // increase from default of 5 seconds // register scoped core services services.AddScoped() @@ -137,7 +136,7 @@ namespace Oqtane policy => { // allow .NET MAUI client cross origin calls - policy.WithOrigins("https://0.0.0.0", "http://0.0.0.0", "app://0.0.0.0") + policy.WithOrigins("https://0.0.0.1", "http://0.0.0.1", "app://0.0.0.1") .AllowAnyHeader().AllowAnyMethod().AllowCredentials(); }); }); @@ -196,9 +195,6 @@ namespace Oqtane app.UseHsts(); } - // execute any IServerStartup logic - app.ConfigureOqtaneAssemblies(env); - // allow oqtane localization middleware app.UseOqtaneLocalization(); @@ -229,6 +225,9 @@ namespace Oqtane app.UseAuthorization(); app.UseAntiforgery(); + // execute any IServerStartup logic + app.ConfigureOqtaneAssemblies(env); + if (_useSwagger) { app.UseSwagger(); diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj index 5c41d71c..c40f7c41 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/[Owner].Module.[Module].Client.csproj @@ -13,11 +13,11 @@
- - - - - + + + + + diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/AssemblyInfo.cs b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/AssemblyInfo.cs new file mode 100644 index 00000000..7c4c9cbe --- /dev/null +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Resources; +using Microsoft.Extensions.Localization; + +[assembly: RootNamespace("[Owner].Module.[Module].Server")] diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj index 41a11759..75da4858 100644 --- a/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj +++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Server/[Owner].Module.[Module].Server.csproj @@ -19,10 +19,10 @@ - - - - + + + + diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg deleted file mode 100644 index 63028fb6..00000000 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg and /dev/null differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg deleted file mode 100644 index 2b6babc0..00000000 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg and /dev/null differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg deleted file mode 100644 index dad4f9b6..00000000 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg and /dev/null differ diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg deleted file mode 100644 index 77495a18..00000000 Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg and /dev/null differ diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj index facf7f31..77ae8f52 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/[Owner].Theme.[Theme].Client.csproj @@ -13,9 +13,9 @@ - - - + + + diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd index 08f5286c..1785fa66 100644 --- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd +++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.cmd @@ -4,4 +4,4 @@ set ProjectName=%2 del "*.nupkg" "..\..\[RootFolder]\oqtane.package\nuget.exe" pack %ProjectName%.nuspec -Properties targetframework=%TargetFramework%;projectname=%ProjectName% -XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\wwwroot\Packages\" /Y \ No newline at end of file +XCOPY "*.nupkg" "..\..\[RootFolder]\Oqtane.Server\Packages\" /Y \ No newline at end of file diff --git a/Oqtane.Shared/Models/Folder.cs b/Oqtane.Shared/Models/Folder.cs index da875219..47177cdb 100644 --- a/Oqtane.Shared/Models/Folder.cs +++ b/Oqtane.Shared/Models/Folder.cs @@ -43,7 +43,7 @@ namespace Oqtane.Models public string Path { get; set; } /// - /// Sorting order of the folder + /// Sorting order of the folder ** not used as folders are sorted in alphabetical order ** /// public int Order { get; set; } diff --git a/Oqtane.Shared/Models/Module.cs b/Oqtane.Shared/Models/Module.cs index 55f31357..7775fcf5 100644 --- a/Oqtane.Shared/Models/Module.cs +++ b/Oqtane.Shared/Models/Module.cs @@ -113,6 +113,18 @@ namespace Oqtane.Models [NotMapped] public DateTime? ExpiryDate { get; set; } + /// + /// Header content to include at the top of a module instance in the UI + /// + [NotMapped] + public string Header { get; set; } + + /// + /// Footer content to include below a module instance in the UI + /// + [NotMapped] + public string Footer { get; set; } + #endregion #region SiteRouter properties @@ -218,6 +230,8 @@ namespace Oqtane.Models ContainerType = ContainerType, EffectiveDate = EffectiveDate, ExpiryDate = ExpiryDate, + Header = Header, + Footer = Footer, CreatedBy = CreatedBy, CreatedOn = CreatedOn, ModifiedBy = ModifiedBy, diff --git a/Oqtane.Shared/Models/Page.cs b/Oqtane.Shared/Models/Page.cs index bec0347e..15d640fe 100644 --- a/Oqtane.Shared/Models/Page.cs +++ b/Oqtane.Shared/Models/Page.cs @@ -122,6 +122,12 @@ namespace Oqtane.Models [NotMapped] public bool HasChildren { get; set; } + /// + /// Indicates if module permissions should be updated to be consistent with page permissions + /// + [NotMapped] + public bool UpdateModulePermissions { get; set; } + /// /// List of permissions for this page /// diff --git a/Oqtane.Shared/Models/PageModule.cs b/Oqtane.Shared/Models/PageModule.cs index 0e7c812f..20985e93 100644 --- a/Oqtane.Shared/Models/PageModule.cs +++ b/Oqtane.Shared/Models/PageModule.cs @@ -41,14 +41,27 @@ namespace Oqtane.Models /// Reference to a Razor Container which wraps this module instance. /// public string ContainerType { get; set; } + /// /// Start of when this assignment is valid. See also /// public DateTime? EffectiveDate { get; set; } + /// /// End of when this assignment is valid. See also /// public DateTime? ExpiryDate { get; set; } + + /// + /// Header content to include above the module instance in the UI + /// + public string Header { get; set; } + + /// + /// Footer content to include below the module instance in the UI + /// + public string Footer { get; set; } + #region IDeletable Properties public string DeletedBy { get; set; } diff --git a/Oqtane.Shared/Models/Result.cs b/Oqtane.Shared/Models/Result.cs index d550f2c4..8500d3ad 100644 --- a/Oqtane.Shared/Models/Result.cs +++ b/Oqtane.Shared/Models/Result.cs @@ -6,6 +6,8 @@ namespace Oqtane.Models public string Message { get; set; } + public Result() {} + public Result(bool success) { Success = success; diff --git a/Oqtane.Shared/Models/Site.cs b/Oqtane.Shared/Models/Site.cs index aeb6e37b..e674be2e 100644 --- a/Oqtane.Shared/Models/Site.cs +++ b/Oqtane.Shared/Models/Site.cs @@ -26,6 +26,11 @@ namespace Oqtane.Models /// public string Name { get; set; } + /// + /// The default time zone for the site + /// + public string TimeZoneId { get; set; } + /// /// Reference to a which has the Logo for this site. /// Should be an image. @@ -200,6 +205,7 @@ namespace Oqtane.Models SiteId = SiteId, TenantId = TenantId, Name = Name, + TimeZoneId = TimeZoneId, LogoFileId = LogoFileId, FaviconFileId = FaviconFileId, DefaultThemeType = DefaultThemeType, diff --git a/Oqtane.Shared/Models/SiteTemplate.cs b/Oqtane.Shared/Models/SiteTemplate.cs index 348e1834..42cbffee 100644 --- a/Oqtane.Shared/Models/SiteTemplate.cs +++ b/Oqtane.Shared/Models/SiteTemplate.cs @@ -95,6 +95,8 @@ namespace Oqtane.Models Pane = PaneNames.Default; Order = 1; ContainerType = ""; + Header = ""; + Footer = ""; IsDeleted = false; PermissionList = new List() { @@ -110,6 +112,8 @@ namespace Oqtane.Models public string Pane { get; set; } public int Order { get; set; } public string ContainerType { get; set; } + public string Header { get; set; } + public string Footer { get; set; } public bool IsDeleted { get; set; } public List PermissionList { get; set; } public List Settings { get; set; } diff --git a/Oqtane.Shared/Models/TimeZone.cs b/Oqtane.Shared/Models/TimeZone.cs new file mode 100644 index 00000000..a2ff00f0 --- /dev/null +++ b/Oqtane.Shared/Models/TimeZone.cs @@ -0,0 +1,10 @@ +namespace Oqtane.Models +{ + public class TimeZone + { + public string Id { get; set; } + + public string DisplayName { get; set; } + + } +} diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index d5009fb5..7b6b398b 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -29,6 +29,11 @@ namespace Oqtane.Models /// public string Email { get; set; } + /// + /// User time zone + /// + public string TimeZoneId { get; set; } + /// /// Reference to a containing the users photo. /// diff --git a/Oqtane.Shared/Modules/HtmlText/Models/HtmlText.cs b/Oqtane.Shared/Modules/HtmlText/Models/HtmlText.cs index d4f49d37..024c266a 100644 --- a/Oqtane.Shared/Modules/HtmlText/Models/HtmlText.cs +++ b/Oqtane.Shared/Modules/HtmlText/Models/HtmlText.cs @@ -1,7 +1,5 @@ -using System; using Oqtane.Models; using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; using Oqtane.Documentation; namespace Oqtane.Modules.HtmlText.Models diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj index a3a0fe81..fbffe080 100644 --- a/Oqtane.Shared/Oqtane.Shared.csproj +++ b/Oqtane.Shared/Oqtane.Shared.csproj @@ -3,7 +3,7 @@ net9.0 Debug;Release - 6.1.1 + 6.1.3 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 https://github.com/oqtane/oqtane.framework Git Oqtane @@ -19,11 +19,11 @@ - - - + + + - + diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs index 0519de73..a11a80cc 100644 --- a/Oqtane.Shared/Shared/Constants.cs +++ b/Oqtane.Shared/Shared/Constants.cs @@ -4,8 +4,8 @@ namespace Oqtane.Shared { public class Constants { - public static readonly string Version = "6.1.1"; - public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1"; + public static readonly string Version = "6.1.3"; + public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1,4.0.2,4.0.3,4.0.4,4.0.5,4.0.6,5.0.0,5.0.1,5.0.2,5.0.3,5.1.0,5.1.1,5.1.2,5.2.0,5.2.1,5.2.2,5.2.3,5.2.4,6.0.0,6.0.1,6.1.0,6.1.1,6.1.2,6.1.3"; public const string PackageId = "Oqtane.Framework"; public const string ClientId = "Oqtane.Client"; public const string UpdaterPackageId = "Oqtane.Updater"; @@ -46,7 +46,7 @@ namespace Oqtane.Shared public const string DefaultSite = "Default Site"; public const string ImageFiles = "jpg,jpeg,jpe,gif,bmp,png,ico,webp"; - public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,rss,css"; + public const string UploadableFiles = ImageFiles + ",mov,wmv,avi,mp4,mp3,doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,nupkg,csv,json,rss,css,md"; public const string ReservedDevices = "CON,NUL,PRN,COM0,COM1,COM2,COM3,COM4,COM5,COM6,COM7,COM8,COM9,LPT0,LPT1,LPT2,LPT3,LPT4,LPT5,LPT6,LPT7,LPT8,LPT9,CONIN$,CONOUT$"; public static readonly char[] InvalidFileNameChars = diff --git a/Oqtane.Shared/Shared/ExternalLoginProviders.cs b/Oqtane.Shared/Shared/ExternalLoginProviders.cs index 8b62bad9..57cf9322 100644 --- a/Oqtane.Shared/Shared/ExternalLoginProviders.cs +++ b/Oqtane.Shared/Shared/ExternalLoginProviders.cs @@ -68,7 +68,7 @@ namespace Oqtane.Shared Name = "Facebook", Settings = new Dictionary() { - { "ExternalLogin:ProviderUrl", "https://developers.facebook.com/apps/" }, + { "ExternalLogin:ProviderUrl", "https://developers.facebook.com" }, { "ExternalLogin:ProviderType", "oauth2" }, { "ExternalLogin:ProviderName", "Facebook" }, { "ExternalLogin:AuthorizationUrl", "https://www.facebook.com/v18.0/dialog/oauth" }, diff --git a/Oqtane.Shared/Shared/Utilities.cs b/Oqtane.Shared/Shared/Utilities.cs index 826fac70..95ae2cd1 100644 --- a/Oqtane.Shared/Shared/Utilities.cs +++ b/Oqtane.Shared/Shared/Utilities.cs @@ -121,6 +121,17 @@ namespace Oqtane.Shared return $"{alias?.BaseUrl}{url}{Constants.ImageUrl}{fileId}/{width}/{height}/{mode}/{position}/{background}/{rotate}/{recreate}"; } + public static string ImageUrl(Alias alias, string folderpath, string filename, int width, int height, string mode, string position, string background, int rotate, string format, bool recreate) + { + var aliasUrl = (alias != null && !string.IsNullOrEmpty(alias.Path)) ? "/" + alias.Path : ""; + mode = string.IsNullOrEmpty(mode) ? "crop" : mode; + position = string.IsNullOrEmpty(position) ? "center" : position; + background = string.IsNullOrEmpty(background) ? "transparent" : background; + format = string.IsNullOrEmpty(format) ? "png" : format; + var querystring = $"?width={width}&height={height}&mode={mode}&position={position}&background={background}&rotate={rotate}&format={format}&recreate={recreate}"; + return $"{alias?.BaseUrl}{aliasUrl}{Constants.FileUrl}{folderpath.Replace("\\", "/")}{filename}{querystring}"; + } + public static string TenantUrl(Alias alias, string url) { url = (!url.StartsWith("/")) ? "/" + url : url; @@ -480,6 +491,15 @@ namespace Oqtane.Shared return querystring; } + public static string GetUrlPath(string url) + { + if (url.Contains("?")) + { + url = url.Substring(0, url.IndexOf("?")); + } + return url; + } + public static string LogMessage(object @class, string message) { return $"[{@class.GetType()}] {message}"; diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj index 4343cf9b..7474506c 100644 --- a/Oqtane.Updater/Oqtane.Updater.csproj +++ b/Oqtane.Updater/Oqtane.Updater.csproj @@ -3,7 +3,7 @@ net9.0 Exe - 6.1.1 + 6.1.3 Oqtane Shaun Walker .NET Foundation @@ -11,7 +11,7 @@ .NET Foundation https://www.oqtane.org https://github.com/oqtane/oqtane.framework/blob/dev/LICENSE - https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1 + https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3 https://github.com/oqtane/oqtane.framework Git Oqtane diff --git a/Oqtane.Updater/Program.cs b/Oqtane.Updater/Program.cs index cd8f8359..e99ac13f 100644 --- a/Oqtane.Updater/Program.cs +++ b/Oqtane.Updater/Program.cs @@ -15,18 +15,26 @@ namespace Oqtane.Updater /// static void Main(string[] args) { + // note additional arguments must be added in a backward compatible manner as older versions will not pass them // requires 2 arguments - the ContentRootPath and the WebRootPath of the site // for testing purposes you can uncomment and modify the logic below - //Array.Resize(ref args, 2); + //Array.Resize(ref args, 3); //args[0] = @"C:\yourpath\oqtane.framework\Oqtane.Server"; //args[1] = @"C:\yourpath\oqtane.framework\Oqtane.Server\wwwroot"; + //args[2] = @"true"; // parameter added in 6.1.2 - if (args.Length == 2) + if (args.Length >= 2) { string contentrootfolder = args[0]; string webrootfolder = args[1]; + bool backup = true; + if (args.Length >= 3) + { + backup = bool.Parse(args[2]); + } + string deployfolder = Path.Combine(contentrootfolder, "Packages"); string backupfolder = Path.Combine(contentrootfolder, "Backup"); @@ -49,6 +57,7 @@ namespace Oqtane.Updater WriteLog(logFilePath, "Upgrade Process Started: " + DateTime.UtcNow.ToString() + Environment.NewLine); WriteLog(logFilePath, "ContentRootPath: " + contentrootfolder + Environment.NewLine); WriteLog(logFilePath, "WebRootPath: " + webrootfolder + Environment.NewLine); + if (packagename != "" && File.Exists(Path.Combine(webrootfolder, "app_offline.bak"))) { WriteLog(logFilePath, "Located Upgrade Package: " + packagename + Environment.NewLine); @@ -74,12 +83,12 @@ namespace Oqtane.Updater } bool success = true; - // ensure files are not locked - if (CanAccessFiles(files)) + + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 10, "Preparing Backup Folder"); WriteLog(logFilePath, "Preparing Backup Folder: " + backupfolder + Environment.NewLine); - + try { // clear out backup folder @@ -112,8 +121,28 @@ namespace Oqtane.Updater { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } - File.Copy(file, filename); - WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + + try + { + // try optimistically to backup the file + File.Copy(file, filename); + WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + } + catch + { + // if the file is locked, wait until it is unlocked + if (CanAccessFile(file)) + { + File.Copy(file, filename); + WriteLog(logFilePath, "Copy File: " + filename + Environment.NewLine); + } + else + { + // file could not be backed up, upgrade unsuccessful + success = false; + WriteLog(logFilePath, "Error Backing Up Files" + Environment.NewLine); + } + } } } catch (Exception ex) @@ -125,17 +154,20 @@ namespace Oqtane.Updater } } } + } - // extract files - if (success) + // extract files + if (success) + { + UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package"); + WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine); + try { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Extracting Files From Upgrade Package"); - WriteLog(logFilePath, "Extracting Files From Upgrade Package..." + Environment.NewLine); - try + using (ZipArchive archive = ZipFile.OpenRead(packagename)) { - using (ZipArchive archive = ZipFile.OpenRead(packagename)) + foreach (ZipArchiveEntry entry in archive.Entries) { - foreach (ZipArchiveEntry entry in archive.Entries) + if (success) { if (!string.IsNullOrEmpty(entry.Name)) { @@ -144,20 +176,42 @@ namespace Oqtane.Updater { Directory.CreateDirectory(Path.GetDirectoryName(filename)); } - entry.ExtractToFile(filename, true); - WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + + try + { + // try optimistically to extract the file + entry.ExtractToFile(filename, true); + WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + } + catch + { + // if the file is locked, wait until it is unlocked + if (CanAccessFile(filename)) + { + entry.ExtractToFile(filename, true); + WriteLog(logFilePath, "Exact File: " + filename + Environment.NewLine); + } + else + { + // file could not be extracted, upgrade unsuccessful + success = false; + WriteLog(logFilePath, "Error Extracting Files From Upgrade Package" + Environment.NewLine); + } + } } } } } - catch (Exception ex) - { - success = false; - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger"); - WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine); - } + } + catch (Exception ex) + { + success = false; + WriteLog(logFilePath, "Error Extracting Files From Upgrade Package: " + ex.Message + Environment.NewLine); + } - if (success) + if (success) + { + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 90, "Removing Backup Folder"); WriteLog(logFilePath, "Removing Backup Folder..." + Environment.NewLine); @@ -174,7 +228,12 @@ namespace Oqtane.Updater WriteLog(logFilePath, "Error Removing Backup Folder: " + ex.Message + Environment.NewLine); } } - else + } + else + { + UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Error Extracting Files From Upgrade Package", "bg-danger"); + + if (backup) { UpdateOfflineContent(offlineFilePath, offlineTemplate, 50, "Upgrade Failed, Restoring Files From Backup Folder", "bg-warning"); WriteLog(logFilePath, "Restoring Files From Backup Folder..." + Environment.NewLine); @@ -201,18 +260,14 @@ namespace Oqtane.Updater } } } - else - { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger"); - WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine); - } } else { - UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Some Files Are Locked By The Hosting Environment", "bg-danger"); - WriteLog(logFilePath, "Upgrade Failed: Some Files Are Locked By The Hosting Environment" + Environment.NewLine); + UpdateOfflineContent(offlineFilePath, offlineTemplate, 95, "Upgrade Failed: Could Not Backup Files", "bg-danger"); + WriteLog(logFilePath, "Upgrade Failed: Could Not Backup Files" + Environment.NewLine); } + UpdateOfflineContent(offlineFilePath, offlineTemplate, 100, "Upgrade Process Finished, Reloading", success ? "" : "bg-danger"); Thread.Sleep(3000); //wait for 3 seconds to complete the upgrade process. // bring the app back online @@ -240,55 +295,45 @@ namespace Oqtane.Updater } } - private static bool CanAccessFiles(List files) + private static bool CanAccessFile(string filepath) { - // ensure files are not locked by another process - // the IIS ShutdownTimeLimit defines the duration for app shutdown (default is 90 seconds) - // websockets can delay application shutdown (ie. Blazor Server) + // ensure file is not locked by another process int retries = 60; int sleep = 2; - - bool canAccess = true; + int attempts = 0; FileStream stream = null; - int i = 0; - while (i < (files.Count - 1) && canAccess) - { - string filepath = files[i]; - int attempts = 0; - bool locked = true; - while (attempts < retries && locked) + bool locked = true; + while (attempts < retries && locked) + { + try { - try + if (File.Exists(filepath)) { - if (File.Exists(filepath)) - { - stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None); - locked = false; - } - else - { - locked = false; - } + stream = File.Open(filepath, FileMode.Open, FileAccess.Read, FileShare.None); + locked = false; } - catch // file is locked by another process + else { - Thread.Sleep(sleep * 1000); // wait + locked = false; } - finally - { - stream?.Close(); - } - attempts += 1; } - if (locked) + catch // file is locked by another process { - canAccess = false; - Console.WriteLine("File Locked: " + filepath); + Thread.Sleep(sleep * 1000); // wait } - i += 1; + finally + { + stream?.Close(); + } + attempts += 1; } - return canAccess; + if (locked) + { + Console.WriteLine("File Locked: " + filepath); + } + + return !locked; } private static void UpdateOfflineContent(string filePath, string contentTemplate, int progress, string status, string progressClass = "") diff --git a/README.md b/README.md index 8a0e6b17..d05b1815 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,21 @@ Oqtane is being developed based on some fundamental principles which are outline # Latest Release -[6.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1) was released on March 12, 2025 and is a maintenance release including 46 pull requests by 4 different contributors, pushing the total number of project commits all-time to over 6400. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. +[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) was released on May 29, 2025 and is a maintenance release including 59 pull requests by 5 different contributors, pushing the total number of project commits all-time to over 6600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers. -# Getting Started (Version 6.1.1) +# Try It Now! + +Microsoft's Public Cloud (requires an Azure account) +[![Deploy to Azure](https://aka.ms/deploytoazurebutton)](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fdev%2Fazuredeploy.json) + +A free ASP.NET hosting account. No hidden fees. No credit card required. +[![Deploy to MonsterASP.NET](https://www.oqtane.org/files/Public/MonsterASPNET.png)](https://www.monsterasp.net/) + +# Getting Started (Version 6) **Installing using source code from the Dev/Master branch:** -- Install **[.NET 9.0.3 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. +- Install **[.NET 9.0.5 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)**. - Install the latest edition (v17.12 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/downloads) with the **ASP.NET and web development** workload enabled. Oqtane works with ALL editions of Visual Studio from Community to Enterprise. If you wish to use LocalDB for development ( not a requirement as Oqtane supports SQLite, mySQL, and PostgreSQL ) you must also install the **Data storage and processing**. @@ -84,6 +92,12 @@ Connect with other developers, get support, and share ideas by joining the Oqtan # Roadmap This project is open source, and therefore is a work in progress... +[6.1.3](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.3) (May 29, 2025) +- [x] Stabilization improvements + +[6.1.2](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.2) (Apr 10, 2025) +- [x] Stabilization improvements + [6.1.1](https://github.com/oqtane/oqtane.framework/releases/tag/v6.1.1) (Mar 12, 2025) - [x] Stabilization improvements - [x] Cookie Consent Banner & Privacy/Terms @@ -157,7 +171,7 @@ The following diagram visualizes the client and server components in the Oqtane # Databases -As of version 2.1 (June 2021) Oqtane supports multiple relational database providers - SQL Server, SQLite, MySQL, PostgreSQL +Oqtane supports multiple relational database providers - SQL Server, SQLite, MySQL, PostgreSQL ![Databases](https://github.com/oqtane/framework/blob/dev/screenshots/databases.png?raw=true "Oqtane Databases") diff --git a/azuredeploy.json b/azuredeploy.json index 123ee8e2..d7970211 100644 --- a/azuredeploy.json +++ b/azuredeploy.json @@ -2,6 +2,24 @@ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", "contentVersion": "1.0.0.2", "parameters": { + "sqlServerName": { + "type": "string", + "metadata": { + "description": "The SQL Server name (must be unique). If you have an existing SQL Server in your resource group, you can specify its name and include its login credentials below." + } + }, + "sqlAdministratorLogin": { + "type": "string", + "metadata": { + "description": "The Administrator login username for the SQL Server specified above" + } + }, + "sqlAdministratorLoginPassword": { + "type": "securestring", + "metadata": { + "description": "The Administrator login password for the SQL Server specified above" + } + }, "sqlDatabaseEditionTierDtuCapacity": { "type": "string", "defaultValue": "Basic-Basic-5-2", @@ -26,37 +44,19 @@ "GeneralPurpose-GP_S_Gen5_2-2-250" ], "metadata": { - "description": "Describes the database Edition, Tier, Dtu, Gigabytes (Edition-Tier-Dtu-Gigabytes)" - } - }, - "sqlServerName": { - "type": "string", - "metadata": { - "description": "The name of the sql server. It has to be unique." + "description": "The SQL Database Configuration (Edition-Tier-DTU-Capacity)" } }, "sqlDatabaseName": { "type": "string", "metadata": { - "description": "The name of the sql database. It has to be unique." - } - }, - "sqlAdministratorLogin": { - "type": "string", - "metadata": { - "description": "The admin user of the SQL Server." - } - }, - "sqlAdministratorLoginPassword": { - "type": "securestring", - "metadata": { - "description": "The password of the admin user of the SQL Server." + "description": "The SQL Database name (must be unique)" } }, "BlazorWebsiteName": { "type": "string", "metadata": { - "description": "The name of the website. It has to be unique." + "description": "The App Service name for the Blazor Website (must be unique). If you use the same name as the SQL Database specified above, it will make it easier to associate the resources later." } }, "BlazorSKU": { @@ -77,28 +77,12 @@ ], "defaultValue": "B1", "metadata": { - "description": "The SKU for the App Service Plan" - } - }, - "BlazorSKUCapacity": { - "type": "int", - "defaultValue": 1, - "maxValue": 3, - "minValue": 1, - "metadata": { - "description": "Describes plan's instance count" - } - }, - "location": { - "type": "string", - "defaultValue": "[resourceGroup().location]", - "metadata": { - "description": "Location for all resources." + "description": "The App Service SKU for the Blazor Website" } } }, "variables": { - "hostingPlanName": "[concat('Oqtane-hostingplan-', uniqueString(resourceGroup().id))]", + "hostingPlanName": "[concat('Oqtane-HostingPlan-', uniqueString(resourceGroup().id))]", "databaseCollation": "SQL_Latin1_General_CP1_CI_AS", "databaseEditionTierDtuCapacity": "[split(parameters('sqlDatabaseEditionTierDtuCapacity'),'-')]", "databaseEdition": "[variables('databaseEditionTierDtuCapacity')[0]]", @@ -115,9 +99,9 @@ // ------------------------------------------------------ { "type": "Microsoft.Sql/servers", - "apiVersion": "2022-05-01-preview", // Updated API version + "apiVersion": "2022-05-01-preview", "name": "[parameters('sqlServerName')]", - "location": "[parameters('location')]", + "location": "[resourceGroup().location]", "tags": { "displayName": "SQL Server" }, @@ -132,9 +116,9 @@ // ------------------------------------------------------ { "type": "Microsoft.Sql/servers/databases", - "apiVersion": "2022-05-01-preview", // Updated API version + "apiVersion": "2022-05-01-preview", "name": "[format('{0}/{1}', parameters('sqlServerName'), parameters('sqlDatabaseName'))]", - "location": "[parameters('location')]", + "location": "[resourceGroup().location]", "tags": { "displayName": "Database" }, @@ -169,11 +153,11 @@ ] }, // ------------------------------------------------------ - // Firewall Rule (renamed to 'AllowAllMicrosoftAzureIps') + // Firewall Rule // ------------------------------------------------------ { "type": "Microsoft.Sql/servers/firewallRules", - "apiVersion": "2022-05-01-preview", // Updated API version + "apiVersion": "2022-05-01-preview", "name": "[format('{0}/{1}', parameters('sqlServerName'), 'AllowAllMicrosoftAzureIps')]", "properties": { "endIpAddress": "0.0.0.0", @@ -188,16 +172,15 @@ // ------------------------------------------------------ { "type": "Microsoft.Web/serverfarms", - "apiVersion": "2022-03-01", // Updated API version + "apiVersion": "2022-03-01", "name": "[variables('hostingPlanName')]", - "location": "[parameters('location')]", + "location": "[resourceGroup().location]", "tags": { "displayName": "Blazor" }, "sku": { "name": "[parameters('BlazorSKU')]", - // If you want to auto-map to certain "tier" strings, you can do so. Here we just set the capacity: - "capacity": "[parameters('BlazorSKUCapacity')]" + "capacity": "1" }, "properties": { "name": "[variables('hostingPlanName')]", @@ -209,13 +192,11 @@ // Web App // ------------------------------------------------------ { - "apiVersion": "2022-03-01", // Updated API version - "name": "[parameters('BlazorWebsiteName')]", "type": "Microsoft.Web/sites", - "location": "[parameters('location')]", - "dependsOn": [ - "[variables('hostingPlanName')]" - ], + "apiVersion": "2022-03-01", + "name": "[parameters('BlazorWebsiteName')]", + "kind": "app", + "location": "[resourceGroup().location]", "tags": { "[concat('hidden-related:', resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName')))]": "empty", "displayName": "Website" @@ -224,44 +205,26 @@ "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", "siteConfig": { "webSocketsEnabled": true, - // Updated .NET version "v9.0" from second snippet "netFrameworkVersion": "v9.0" } }, + "dependsOn": [ + "[variables('hostingPlanName')]" + ], "resources": [ // -------------------------------------------------- - // Source Control for your Web App + // ZIP Deploy // -------------------------------------------------- { - "type": "sourcecontrols", - "apiVersion": "2022-03-01", - "name": "web", - "location": "[parameters('location')]", + "type": "Microsoft.Web/sites/extensions", + "apiVersion": "2024-04-01", + "name": "[concat(parameters('BlazorWebsiteName'), '/ZipDeploy')]", + "properties": { + "packageUri": "https://github.com/oqtane/oqtane.framework/releases/download/v6.1.3/Oqtane.Framework.6.1.3.Install.zip" + }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" - ], - "properties": { - "repoUrl": "https://github.com/oqtane/oqtane.framework.git", - "branch": "master", - "isManualIntegration": true - } - }, - // -------------------------------------------------- - // Connection Strings (to use FQDN) - // -------------------------------------------------- - { - "type": "config", - "apiVersion": "2022-03-01", - "name": "connectionstrings", - "dependsOn": [ - "[resourceId('Microsoft.Web/sites', parameters('BlazorWebsiteName'))]" - ], - "properties": { - "DefaultConnection": { - "value": "[concat('Data Source=tcp:', reference(resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))).fullyQualifiedDomainName, ',1433;Initial Catalog=', parameters('sqlDatabaseName'), ';User Id=', parameters('sqlAdministratorLogin'), '@', reference(resourceId('Microsoft.Sql/servers', parameters('sqlServerName'))).fullyQualifiedDomainName, ';Password=', parameters('sqlAdministratorLoginPassword'), ';')]", - "type": "SQLAzure" - } - } + ] } ] }