@@ -413,6 +413,9 @@ else
private string _lifetime;
private string _token;
+ private bool isSortedAscending;
+ private string activeSortColumn;
+
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.View;
protected override async Task OnInitializedAsync()
@@ -654,4 +657,43 @@ else
_togglesecret = SharedLocalizer["ShowPassword"];
}
}
+
+ private void SortTable(string columnName)
+ {
+ if (columnName != activeSortColumn)
+ {
+ users = users.OrderBy(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
+ isSortedAscending = true;
+ activeSortColumn = columnName;
+ }
+ else
+ {
+ if (isSortedAscending)
+ {
+ users = users.OrderByDescending(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
+ }
+ else
+ {
+ users = users.OrderBy(x => x.User.GetType().GetProperty(columnName)?.GetValue(x.User)).ToList();
+ }
+
+ isSortedAscending = !isSortedAscending;
+ }
+ }
+
+ private string SetSortIcon(string columnName)
+ {
+ if (activeSortColumn != columnName)
+ {
+ return "app-fas pe-3 ";
+ }
+ if (isSortedAscending)
+ {
+ return "app-fas oi oi-sort-ascending";
+ }
+ else
+ {
+ return "app-fas oi oi-sort-descending";
+ }
+ }
}
diff --git a/Oqtane.Client/Modules/Admin/Users/Roles.razor b/Oqtane.Client/Modules/Admin/Users/Roles.razor
index 9135a9f6..8766c46a 100644
--- a/Oqtane.Client/Modules/Admin/Users/Roles.razor
+++ b/Oqtane.Client/Modules/Admin/Users/Roles.razor
@@ -34,13 +34,13 @@ else
@@ -75,8 +75,8 @@ else
private string name = string.Empty;
private List roles;
private int roleid = -1;
- private string effectivedate = string.Empty;
- private string expirydate = string.Empty;
+ private DateTime? effectivedate = null;
+ private DateTime? expirydate = null;
private List userroles;
public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Edit;
@@ -130,23 +130,8 @@ else
var userrole = userroles.Where(item => item.UserId == userid && item.RoleId == roleid).FirstOrDefault();
if (userrole != null)
{
- if (string.IsNullOrEmpty(effectivedate))
- {
- userrole.EffectiveDate = null;
- }
- else
- {
- userrole.EffectiveDate = DateTime.Parse(effectivedate);
- }
-
- if (string.IsNullOrEmpty(expirydate))
- {
- userrole.ExpiryDate = null;
- }
- else
- {
- userrole.ExpiryDate = DateTime.Parse(expirydate);
- }
+ userrole.EffectiveDate = effectivedate;
+ userrole.ExpiryDate = expirydate;
await UserRoleService.UpdateUserRoleAsync(userrole);
}
else
@@ -154,25 +139,8 @@ else
userrole = new UserRole();
userrole.UserId = userid;
userrole.RoleId = roleid;
-
- if (string.IsNullOrEmpty(effectivedate))
- {
- userrole.EffectiveDate = null;
- }
- else
- {
- userrole.EffectiveDate = DateTime.Parse(effectivedate);
- }
-
- if (string.IsNullOrEmpty(expirydate))
- {
- userrole.ExpiryDate = null;
- }
- else
- {
- userrole.ExpiryDate = DateTime.Parse(expirydate);
- }
-
+ userrole.EffectiveDate = effectivedate;
+ userrole.ExpiryDate = expirydate;
await UserRoleService.AddUserRoleAsync(userrole);
}
diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor
index cd5ad6e3..c37cef54 100644
--- a/Oqtane.Client/Modules/Controls/FileManager.razor
+++ b/Oqtane.Client/Modules/Controls/FileManager.razor
@@ -55,16 +55,26 @@
- @if (ShowFiles && GetFileId() != -1)
+ @if (GetFileId() != -1)
{
}
-
+ @if (ShowProgress)
+ {
+
+ }
+ else
+ {
+ if (_uploading)
+ {
+
+ }
+ }
}
@@ -100,6 +110,7 @@
private string _guid;
private string _message = string.Empty;
private MessageType _messagetype;
+ private bool _uploading = false;
[Parameter]
public string Id { get; set; } // optional - for setting the id of the FileManager component for accessibility
@@ -128,6 +139,9 @@
[Parameter]
public bool ShowImage { get; set; } = true; // optional - for indicating whether an image thumbnail should be displayed - default is true
+ [Parameter]
+ public bool ShowProgress { get; set; } = true; // optional - for indicating whether progress info should be displayed during upload - default is true
+
[Parameter]
public bool ShowSuccess { get; set; } = false; // optional - for indicating whether a success message should be displayed upon successful upload - default is false
@@ -143,7 +157,7 @@
[Parameter]
public EventCallback OnDelete { get; set; } // optional - executes a method in the calling component when a file is deleted
- protected override async Task OnInitializedAsync()
+ protected override async Task OnParametersSetAsync()
{
// packages folder is a framework folder for uploading installable nuget packages
if (Folder == Constants.PackagesFolder)
@@ -154,11 +168,6 @@
ShowSuccess = true;
}
- if (!ShowFiles)
- {
- ShowImage = false;
- }
-
_folders = await FolderService.GetFoldersAsync(ModuleState.SiteId);
if (!string.IsNullOrEmpty(Folder) && Folder != Constants.PackagesFolder)
@@ -182,7 +191,6 @@
if (file != null)
{
FolderId = file.FolderId;
- await OnSelect.InvokeAsync(FileId);
}
else
{
@@ -224,7 +232,14 @@
if (folder != null)
{
_haseditpermission = UserSecurity.IsAuthorized(PageState.User, PermissionNames.Edit, folder.PermissionList);
- _files = await FileService.GetFilesAsync(FolderId);
+ if (UserSecurity.IsAuthorized(PageState.User, PermissionNames.Browse, folder.PermissionList))
+ {
+ _files = await FileService.GetFilesAsync(FolderId);
+ }
+ else
+ {
+ _files = new List();
+ }
}
else
{
@@ -270,13 +285,10 @@
{
_message = string.Empty;
FileId = int.Parse((string)e.Value);
- if (FileId != -1)
- {
- await OnSelect.InvokeAsync(FileId);
- }
-
await SetImage();
+ await OnSelect.InvokeAsync(FileId);
StateHasChanged();
+
}
private async Task SetImage()
@@ -320,6 +332,12 @@
}
if (restricted == "")
{
+ if (!ShowProgress)
+ {
+ _uploading = true;
+ StateHasChanged();
+ }
+
try
{
// upload the files
@@ -327,32 +345,42 @@
var folder = (Folder == Constants.PackagesFolder) ? Folder : FolderId.ToString();
await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken);
- // uploading is asynchronous so we need to wait for the uploads to complete
- // note that this will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
- bool success = false;
- int attempts = 0;
- while (attempts < 5 && !success)
+ // uploading is asynchronous so we need to poll to determine if uploads are completed
+ var success = true;
+ int upload = 0;
+ while (upload < uploads.Length && success)
{
- attempts += 1;
- Thread.Sleep(1000 * attempts); // progressive retry
-
- success = true;
- List files = await FileService.GetFilesAsync(folder);
- if (files.Count > 0)
+ success = false;
+ // note that progressive retry will only wait a maximum of 15 seconds which may not be long enough for very large file uploads
+ int attempts = 0;
+ while (attempts < 5 && !success)
{
- foreach (string upload in uploads)
+ attempts += 1;
+ Thread.Sleep(1000 * attempts); // progressive retry
+
+ var file = await FileService.GetFileAsync(int.Parse(folder), uploads[upload]);
+ if (file != null)
{
- if (!files.Exists(item => item.Name == upload))
- {
- success = false;
- }
+ success = true;
}
}
+ if (success)
+ {
+ upload++;
+ }
}
// reset progress indicators
- await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
- await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
+ if (ShowProgress)
+ {
+ await interop.SetElementAttribute(_guid + "ProgressInfo", "style", "display: none;");
+ await interop.SetElementAttribute(_guid + "ProgressBar", "style", "display: none;");
+ }
+ else
+ {
+ _uploading = false;
+ StateHasChanged();
+ }
if (success)
{
@@ -377,48 +405,53 @@
else
{
// set FileId to first file in upload collection
- await GetFiles();
- var file = _files.Where(item => item.Name == uploads[0]).FirstOrDefault();
+ var file = await FileService.GetFileAsync(int.Parse(folder), uploads[0]);
if (file != null)
{
FileId = file.FileId;
await SetImage();
await OnUpload.InvokeAsync(FileId);
}
+ await GetFiles();
StateHasChanged();
}
- }
- catch (Exception ex)
- {
- await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
- _message = Localizer["Error.File.Upload"];
- _messagetype = MessageType.Error;
- }
- }
- else
- {
- _message = string.Format(Localizer["Message.File.Restricted"], restricted);
- _messagetype = MessageType.Warning;
- }
- }
- else
- {
- _message = Localizer["Message.File.NotSelected"];
- _messagetype = MessageType.Warning;
- }
- }
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "File Upload Failed {Error}", ex.Message);
+ _message = Localizer["Error.File.Upload"];
+ _messagetype = MessageType.Error;
+ _uploading = false;
+ }
- private async Task DeleteFile()
- {
- _message = string.Empty;
- try
- {
- await FileService.DeleteFileAsync(FileId);
- await logger.LogInformation("File Deleted {File}", FileId);
- await OnDelete.InvokeAsync(FileId);
+ }
+ else
+ {
+ _message = string.Format(Localizer["Message.File.Restricted"], restricted);
+ _messagetype = MessageType.Warning;
+ }
+ }
+ else
+ {
+ _message = Localizer["Message.File.NotSelected"];
+ _messagetype = MessageType.Warning;
+ }
+ }
- _message = Localizer["Success.File.Delete"];
- _messagetype = MessageType.Success;
+ private async Task DeleteFile()
+ {
+ _message = string.Empty;
+ try
+ {
+ await FileService.DeleteFileAsync(FileId);
+ await logger.LogInformation("File Deleted {File}", FileId);
+ await OnDelete.InvokeAsync(FileId);
+
+ if (ShowSuccess)
+ {
+ _message = Localizer["Success.File.Delete"];
+ _messagetype = MessageType.Success;
+ }
await GetFiles();
FileId = -1;
diff --git a/Oqtane.Client/Modules/Controls/Section.razor b/Oqtane.Client/Modules/Controls/Section.razor
index 3620d893..22737d6c 100644
--- a/Oqtane.Client/Modules/Controls/Section.razor
+++ b/Oqtane.Client/Modules/Controls/Section.razor
@@ -17,7 +17,10 @@
- @ChildContent
+ @if (ChildContent != null)
+ {
+ @ChildContent
+ }
@code {
@@ -26,7 +29,7 @@
private string _show = string.Empty;
[Parameter]
- public RenderFragment ChildContent { get; set; }
+ public RenderFragment ChildContent { get; set; } = null;
[Parameter]
public string Name { get; set; } // required - the name of the section
@@ -37,19 +40,10 @@
[Parameter]
public string Expanded { get; set; } // optional - will default to false if not provided
- protected override void OnInitialized()
- {
- _heading = (!string.IsNullOrEmpty(Heading)) ? Heading : Name;
- _expanded = (!string.IsNullOrEmpty(Expanded)) ? Expanded : "false";
- if (_expanded == "true") { _show = "show"; }
- }
-
protected override void OnParametersSet()
{
- base.OnParametersSet();
-
- _heading = !string.IsNullOrEmpty(Heading)
- ? Localize(nameof(Heading), Heading)
- : Localize(nameof(Name), Name);
+ _heading = !string.IsNullOrEmpty(Heading) ? Localize(nameof(Heading), Heading) : Localize(nameof(Name), Name);
+ _expanded = (!string.IsNullOrEmpty(Expanded)) ? Expanded.ToLower() : "false";
+ if (_expanded == "true") { _show = "show"; }
}
}
diff --git a/Oqtane.Client/Modules/Controls/TabStrip.razor b/Oqtane.Client/Modules/Controls/TabStrip.razor
index 01f5aded..90b3948a 100644
--- a/Oqtane.Client/Modules/Controls/TabStrip.razor
+++ b/Oqtane.Client/Modules/Controls/TabStrip.razor
@@ -8,7 +8,7 @@
@foreach (TabPanel tabPanel in _tabPanels)
{
- @if (tabPanel.Name == ActiveTab)
+ @if (tabPanel.Name.ToLower() == ActiveTab.ToLower())
{
@tabPanel.DisplayHeading()
diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs
index 8023da67..f1a086e5 100644
--- a/Oqtane.Client/Modules/ModuleBase.cs
+++ b/Oqtane.Client/Modules/ModuleBase.cs
@@ -9,7 +9,6 @@ using Oqtane.UI;
using System.Collections.Generic;
using Microsoft.JSInterop;
using System.Linq;
-using Oqtane.Themes;
namespace Oqtane.Modules
{
diff --git a/Oqtane.Client/Oqtane.Client.csproj b/Oqtane.Client/Oqtane.Client.csproj
index 9ea4853c..ae3e2589 100644
--- a/Oqtane.Client/Oqtane.Client.csproj
+++ b/Oqtane.Client/Oqtane.Client.csproj
@@ -3,9 +3,8 @@
net7.0
Exe
- 3.0
Debug;Release
- 4.0.0
+ 4.0.1
Oqtane
Shaun Walker
.NET Foundation
@@ -13,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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
https://github.com/oqtane/oqtane.framework
Git
Oqtane
diff --git a/Oqtane.Client/Resources/Modules/Admin/Logs/Detail.resx b/Oqtane.Client/Resources/Modules/Admin/Logs/Detail.resx
index 062d8a27..e7734cb0 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Logs/Detail.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Logs/Detail.resx
@@ -1,4 +1,4 @@
-
+
Exe
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
https://github.com/oqtane/oqtane.framework
Git
Oqtane.Maui
@@ -31,7 +31,7 @@
0E29FC31-1B83-48ED-B6E0-9F3C67B775D4
- 4.0.0
+ 4.0.1
1
14.2
diff --git a/Oqtane.Package/Oqtane.Client.nuspec b/Oqtane.Package/Oqtane.Client.nuspec
index b2b4def4..752a2449 100644
--- a/Oqtane.Package/Oqtane.Client.nuspec
+++ b/Oqtane.Package/Oqtane.Client.nuspec
@@ -2,7 +2,7 @@
Oqtane.Client
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Framework.nuspec b/Oqtane.Package/Oqtane.Framework.nuspec
index 662219e6..5c5ad632 100644
--- a/Oqtane.Package/Oqtane.Framework.nuspec
+++ b/Oqtane.Package/Oqtane.Framework.nuspec
@@ -2,7 +2,7 @@
Oqtane.Framework
- 4.0.0
+ 4.0.1
Shaun Walker
.NET Foundation
Oqtane Framework
@@ -11,8 +11,8 @@
.NET Foundation
false
MIT
- https://github.com/oqtane/oqtane.framework/releases/download/v4.0.0/Oqtane.Framework.4.0.0.Upgrade.zip
- https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/download/v4.0.1/Oqtane.Framework.4.0.1.Upgrade.zip
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
icon.png
oqtane framework
diff --git a/Oqtane.Package/Oqtane.Server.nuspec b/Oqtane.Package/Oqtane.Server.nuspec
index 70a25469..94fe9ab5 100644
--- a/Oqtane.Package/Oqtane.Server.nuspec
+++ b/Oqtane.Package/Oqtane.Server.nuspec
@@ -2,7 +2,7 @@
Oqtane.Server
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Shared.nuspec b/Oqtane.Package/Oqtane.Shared.nuspec
index 6b073a4a..f4170745 100644
--- a/Oqtane.Package/Oqtane.Shared.nuspec
+++ b/Oqtane.Package/Oqtane.Shared.nuspec
@@ -2,7 +2,7 @@
Oqtane.Shared
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
icon.png
oqtane
diff --git a/Oqtane.Package/Oqtane.Updater.nuspec b/Oqtane.Package/Oqtane.Updater.nuspec
index 9e667cc9..622e8109 100644
--- a/Oqtane.Package/Oqtane.Updater.nuspec
+++ b/Oqtane.Package/Oqtane.Updater.nuspec
@@ -2,7 +2,7 @@
Oqtane.Updater
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
icon.png
oqtane
diff --git a/Oqtane.Package/install.ps1 b/Oqtane.Package/install.ps1
index 9d1c4f9e..8f8c07ad 100644
--- a/Oqtane.Package/install.ps1
+++ b/Oqtane.Package/install.ps1
@@ -1 +1 @@
-Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.0.Install.zip" -Force
\ No newline at end of file
+Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Install.zip" -Force
\ No newline at end of file
diff --git a/Oqtane.Package/upgrade.ps1 b/Oqtane.Package/upgrade.ps1
index 17d5c251..0c20267b 100644
--- a/Oqtane.Package/upgrade.ps1
+++ b/Oqtane.Package/upgrade.ps1
@@ -1 +1 @@
-Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.0.Upgrade.zip" -Force
\ No newline at end of file
+Compress-Archive -Path "..\Oqtane.Server\bin\Release\net7.0\publish\*" -DestinationPath "Oqtane.Framework.4.0.1.Upgrade.zip" -Force
\ No newline at end of file
diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs
index 30cbab26..31ed3c80 100644
--- a/Oqtane.Server/Controllers/FileController.cs
+++ b/Oqtane.Server/Controllers/FileController.cs
@@ -123,8 +123,38 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {FileId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (file != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {FileId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
+ return null;
+ }
+ }
+
+ [HttpGet("name/{name}/{folderId}")]
+ public Models.File Get(string name, int folderId)
+ {
+ Models.File file = _files.GetFile(folderId, name);
+ if (file != null && file.Folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, file.Folder.PermissionList))
+ {
+ return file;
+ }
+ else
+ {
+ if (file != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized File Get Attempt {Name} For Folder {FolderId}", name, folderId);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/FolderController.cs b/Oqtane.Server/Controllers/FolderController.cs
index 8c805e5e..68feab68 100644
--- a/Oqtane.Server/Controllers/FolderController.cs
+++ b/Oqtane.Server/Controllers/FolderController.cs
@@ -43,7 +43,7 @@ namespace Oqtane.Controllers
{
foreach (Folder folder in _folders.GetFolders(SiteId))
{
- if (_userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
+ if (_userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
folders.Add(folder);
}
@@ -64,14 +64,21 @@ namespace Oqtane.Controllers
public Folder Get(int id)
{
Folder folder = _folders.GetFolder(id);
- if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
+ if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
return folder;
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {FolderId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (folder != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {FolderId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -80,19 +87,26 @@ namespace Oqtane.Controllers
public Folder GetByPath(int siteId, string path)
{
var folderPath = WebUtility.UrlDecode(path).Replace("\\", "/");
- if (!folderPath.EndsWith("/"))
+ if (!folderPath.EndsWith("/") && folderPath != "")
{
folderPath += "/";
}
Folder folder = _folders.GetFolder(siteId, folderPath);
- if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.Browse, folder.PermissionList))
+ if (folder != null && folder.SiteId == _alias.SiteId && _userPermissions.IsAuthorized(User, PermissionNames.View, folder.PermissionList))
{
return folder;
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {Path} For Site {SiteId}", path, siteId);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (folder != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Folder Get Attempt {Path} For Site {SiteId}", path, siteId);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -124,7 +138,10 @@ namespace Oqtane.Controllers
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.UrlCombine(parent.Path, folder.Name);
}
- folder.Path = folder.Path + "/";
+ if (!folder.Path.EndsWith("/"))
+ {
+ folder.Path = folder.Path + "/";
+ }
folder = _folders.AddFolder(folder);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Folder, folder.FolderId, SyncEventActions.Create);
_logger.Log(LogLevel.Information, this, LogFunction.Create, "Folder Added {Folder}", folder);
@@ -166,7 +183,10 @@ namespace Oqtane.Controllers
Folder parent = _folders.GetFolder(folder.ParentId.Value);
folder.Path = Utilities.UrlCombine(parent.Path, folder.Name);
}
- folder.Path = folder.Path + "/";
+ if (!folder.Path.EndsWith("/"))
+ {
+ folder.Path = folder.Path + "/";
+ }
Folder _folder = _folders.GetFolder(id, false);
if (_folder.Path != folder.Path && Directory.Exists(_folders.GetFolderPath(_folder)))
diff --git a/Oqtane.Server/Controllers/LanguageController.cs b/Oqtane.Server/Controllers/LanguageController.cs
index e4c6dbc7..e1ac9406 100644
--- a/Oqtane.Server/Controllers/LanguageController.cs
+++ b/Oqtane.Server/Controllers/LanguageController.cs
@@ -89,8 +89,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Get Attempt {LanguageId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (language != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Language Get Attempt {LanguageId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/ModuleController.cs b/Oqtane.Server/Controllers/ModuleController.cs
index 7176057f..9422d019 100644
--- a/Oqtane.Server/Controllers/ModuleController.cs
+++ b/Oqtane.Server/Controllers/ModuleController.cs
@@ -113,8 +113,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Module Get Attempt {ModuleId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (module != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Module Get Attempt {ModuleId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/ModuleDefinitionController.cs b/Oqtane.Server/Controllers/ModuleDefinitionController.cs
index c8114821..21379582 100644
--- a/Oqtane.Server/Controllers/ModuleDefinitionController.cs
+++ b/Oqtane.Server/Controllers/ModuleDefinitionController.cs
@@ -89,15 +89,22 @@ namespace Oqtane.Controllers
if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId)
{
ModuleDefinition moduledefinition = _moduleDefinitions.GetModuleDefinition(id, SiteId);
- if (_userPermissions.IsAuthorized(User, PermissionNames.Utilize, moduledefinition.PermissionList))
+ if (moduledefinition != null && _userPermissions.IsAuthorized(User, PermissionNames.Utilize, moduledefinition.PermissionList))
{
- if (string.IsNullOrEmpty(moduledefinition.Version)) moduledefinition.Version = new Version(1, 0, 0).ToString();
+ moduledefinition.Version = (string.IsNullOrEmpty(moduledefinition.Version)) ? new Version(1, 0, 0).ToString() : moduledefinition.Version;
return moduledefinition;
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Get Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (moduledefinition != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized ModuleDefinition Get Attempt {ModuleDefinitionId} {SiteId}", id, siteid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/NotificationController.cs b/Oqtane.Server/Controllers/NotificationController.cs
index dfdba5f1..e2bfd14e 100644
--- a/Oqtane.Server/Controllers/NotificationController.cs
+++ b/Oqtane.Server/Controllers/NotificationController.cs
@@ -9,6 +9,9 @@ using Oqtane.Repository;
using Oqtane.Security;
using System.Net;
using System.Reflection.Metadata;
+using Microsoft.Extensions.Localization;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using System.Linq;
namespace Oqtane.Controllers
{
@@ -30,6 +33,72 @@ namespace Oqtane.Controllers
_alias = tenantManager.GetAlias();
}
+ // GET: api//read?siteid=x&direction=to&userid=1&count=5&isread=false
+ [HttpGet("read")]
+ [Authorize(Roles = RoleNames.Registered)]
+ public IEnumerable Get(string siteid, string direction, string userid, string count, string isread)
+ {
+ IEnumerable notifications = null;
+
+ int SiteId;
+ int UserId;
+ int Count;
+ bool IsRead;
+ if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && int.TryParse(count, out Count) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
+ {
+ if (direction == "to")
+ {
+ notifications = _notifications.GetNotifications(SiteId, -1, UserId, Count, IsRead);
+ }
+ else
+ {
+ notifications = _notifications.GetNotifications(SiteId, UserId, -1, Count, IsRead);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {Count} {isRead}", siteid, direction, userid, count, isread);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ notifications = null;
+ }
+
+
+ return notifications;
+ }
+
+ // GET: api//read?siteid=x&direction=to&userid=1&count=5&isread=false
+ [HttpGet("read-count")]
+ [Authorize(Roles = RoleNames.Registered)]
+ public int Get(string siteid, string direction, string userid, string isread)
+ {
+ int notificationsCount = 0;
+
+ int SiteId;
+ int UserId;
+ bool IsRead;
+ if (int.TryParse(siteid, out SiteId) && SiteId == _alias.SiteId && int.TryParse(userid, out UserId) && bool.TryParse(isread, out IsRead) && IsAuthorized(UserId))
+ {
+ if (direction == "to")
+ {
+ notificationsCount = _notifications.GetNotificationCount(SiteId, -1, UserId, IsRead);
+ }
+ else
+ {
+ notificationsCount = _notifications.GetNotificationCount(SiteId, UserId, -1, IsRead);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {SiteId} {Direction} {UserId} {isRead}", siteid, direction, userid, isread);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ notificationsCount = 0;
+ }
+
+
+ return notificationsCount;
+ }
+
+
// GET: api/?siteid=x&type=y&userid=z
[HttpGet]
[Authorize(Roles = RoleNames.Registered)]
@@ -72,8 +141,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {NotificationId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (notification != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Notification Get Attempt {NotificationId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/PackageController.cs b/Oqtane.Server/Controllers/PackageController.cs
index d4ba5c21..4af636f4 100644
--- a/Oqtane.Server/Controllers/PackageController.cs
+++ b/Oqtane.Server/Controllers/PackageController.cs
@@ -34,7 +34,7 @@ namespace Oqtane.Controllers
// GET: api/?type=x&search=y&price=z&package=a
[HttpGet]
- public async Task> Get(string type, string search, string price, string package)
+ public async Task> Get(string type, string search, string price, string package, string sort)
{
// get packages
List packages = new List();
@@ -44,8 +44,8 @@ namespace Oqtane.Controllers
{
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
- packages = await GetJson>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}");
- }
+ packages = await GetJson>(client, Constants.PackageRegistryUrl + $"/api/registry/packages/?id={_configManager.GetInstallationId()}&type={type.ToLower()}&version={Constants.Version}&search={search}&price={price}&package={package}&sort={sort}");
+ }
}
return packages;
}
diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs
index ce81eec2..5c7c5e8a 100644
--- a/Oqtane.Server/Controllers/PageController.cs
+++ b/Oqtane.Server/Controllers/PageController.cs
@@ -7,10 +7,8 @@ using System.Linq;
using Oqtane.Security;
using System.Net;
using Oqtane.Enums;
-using Oqtane.Extensions;
using Oqtane.Infrastructure;
using Oqtane.Repository;
-using Oqtane.Modules.Admin.Users;
using System.IO;
namespace Oqtane.Controllers
@@ -89,8 +87,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Page Get Attempt {PageId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (page != null)
+ {
+ _logger.Log(LogLevel.Warning, this, LogFunction.Security, "Unauthorized Page Get Attempt {PageId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -109,8 +114,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Page Get Attempt {SiteId} {Path}", siteid, path);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (page != null)
+ {
+ _logger.Log(LogLevel.Warning, this, LogFunction.Security, "Unauthorized Page Get Attempt {SiteId} {Path}", siteid, path);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -177,64 +189,68 @@ namespace Oqtane.Controllers
User user = _userPermissions.GetUser(User);
if (parent != null && parent.SiteId == _alias.SiteId && parent.IsPersonalizable && user.UserId == int.Parse(userid))
{
- page = new Page();
- page.SiteId = parent.SiteId;
- page.ParentId = parent.PageId;
- page.Name = user.Username;
- page.Path = parent.Path + "/" + page.Name;
- page.Title = parent.Name + " - " + page.Name;
- page.Order = 0;
- page.IsNavigation = false;
- page.Url = "";
- page.ThemeType = parent.ThemeType;
- page.DefaultContainerType = parent.DefaultContainerType;
- page.Icon = parent.Icon;
- page.PermissionList = new List()
+ page = _pages.GetPage(parent.Path + "/" + user.Username, parent.SiteId);
+ if (page == null)
{
- new Permission(PermissionNames.View, int.Parse(userid), true),
- new Permission(PermissionNames.View, RoleNames.Everyone, true),
- new Permission(PermissionNames.Edit, int.Parse(userid), true)
- };
- page.IsPersonalizable = false;
- page.UserId = int.Parse(userid);
- page = _pages.AddPage(page);
-
- // copy modules
- List pagemodules = _pageModules.GetPageModules(page.SiteId).ToList();
- foreach (PageModule pm in pagemodules.Where(item => item.PageId == parent.PageId && !item.IsDeleted))
- {
- Module module = new Module();
- module.SiteId = page.SiteId;
- module.PageId = page.PageId;
- module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
- module.AllPages = false;
- module.PermissionList = new List()
+ page = new Page();
+ page.SiteId = parent.SiteId;
+ page.ParentId = parent.PageId;
+ page.Name = (!string.IsNullOrEmpty(user.DisplayName)) ? user.DisplayName : user.Username;
+ page.Path = parent.Path + "/" + user.Username;
+ page.Title = page.Name + " - " + parent.Name;
+ page.Order = 0;
+ page.IsNavigation = false;
+ page.Url = "";
+ page.ThemeType = parent.ThemeType;
+ page.DefaultContainerType = parent.DefaultContainerType;
+ page.Icon = parent.Icon;
+ page.PermissionList = new List()
{
new Permission(PermissionNames.View, int.Parse(userid), true),
new Permission(PermissionNames.View, RoleNames.Everyone, true),
new Permission(PermissionNames.Edit, int.Parse(userid), true)
};
- module = _modules.AddModule(module);
+ page.IsPersonalizable = false;
+ page.UserId = int.Parse(userid);
+ page = _pages.AddPage(page);
- string content = _modules.ExportModule(pm.ModuleId);
- if (content != "")
+ // copy modules
+ List pagemodules = _pageModules.GetPageModules(page.SiteId).ToList();
+ foreach (PageModule pm in pagemodules.Where(item => item.PageId == parent.PageId && !item.IsDeleted))
{
- _modules.ImportModule(module.ModuleId, content);
+ Module module = new Module();
+ module.SiteId = page.SiteId;
+ module.PageId = page.PageId;
+ module.ModuleDefinitionName = pm.Module.ModuleDefinitionName;
+ module.AllPages = false;
+ module.PermissionList = new List()
+ {
+ new Permission(PermissionNames.View, int.Parse(userid), true),
+ new Permission(PermissionNames.View, RoleNames.Everyone, true),
+ new Permission(PermissionNames.Edit, int.Parse(userid), true)
+ };
+ module = _modules.AddModule(module);
+
+ string content = _modules.ExportModule(pm.ModuleId);
+ if (content != "")
+ {
+ _modules.ImportModule(module.ModuleId, content);
+ }
+
+ PageModule pagemodule = new PageModule();
+ pagemodule.PageId = page.PageId;
+ pagemodule.ModuleId = module.ModuleId;
+ pagemodule.Title = pm.Title;
+ pagemodule.Pane = pm.Pane;
+ pagemodule.Order = pm.Order;
+ pagemodule.ContainerType = pm.ContainerType;
+
+ _pageModules.AddPageModule(pagemodule);
}
- PageModule pagemodule = new PageModule();
- pagemodule.PageId = page.PageId;
- pagemodule.ModuleId = module.ModuleId;
- pagemodule.Title = pm.Title;
- pagemodule.Pane = pm.Pane;
- pagemodule.Order = pm.Order;
- pagemodule.ContainerType = pm.ContainerType;
-
- _pageModules.AddPageModule(pagemodule);
+ _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Create);
+ _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
}
-
- _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Create);
- _syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
}
else
{
@@ -263,14 +279,18 @@ namespace Oqtane.Controllers
// save url mapping if page path changed
if (currentPage.Path != page.Path)
{
- var urlMapping = new UrlMapping();
- urlMapping.SiteId = page.SiteId;
- urlMapping.Url = currentPage.Path;
- urlMapping.MappedUrl = page.Path;
- urlMapping.Requests = 0;
- urlMapping.CreatedOn = System.DateTime.UtcNow;
- urlMapping.RequestedOn = System.DateTime.UtcNow;
- _urlMappings.AddUrlMapping(urlMapping);
+ var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
+ if (urlMapping == null)
+ {
+ urlMapping = new UrlMapping();
+ urlMapping.SiteId = page.SiteId;
+ urlMapping.Url = currentPage.Path;
+ urlMapping.MappedUrl = page.Path;
+ urlMapping.Requests = 0;
+ urlMapping.CreatedOn = System.DateTime.UtcNow;
+ urlMapping.RequestedOn = System.DateTime.UtcNow;
+ _urlMappings.AddUrlMapping(urlMapping);
+ }
}
// get differences between current and new page permissions
@@ -314,6 +334,16 @@ namespace Oqtane.Controllers
}
}
+ // update child paths
+ if (page.ParentId != currentPage.ParentId)
+ {
+ foreach (Page _page in _pages.GetPages(page.SiteId).Where(item => item.Path.StartsWith(currentPage.Path)).ToList())
+ {
+ _page.Path = _page.Path.Replace(currentPage.Path, page.Path);
+ _pages.UpdatePage(_page);
+ }
+ }
+
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Page, page.PageId, SyncEventActions.Update);
_syncManager.AddSyncEvent(_alias.TenantId, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
_logger.Log(LogLevel.Information, this, LogFunction.Update, "Page Updated {Page}", page);
diff --git a/Oqtane.Server/Controllers/PageModuleController.cs b/Oqtane.Server/Controllers/PageModuleController.cs
index c5b94e6d..3c445772 100644
--- a/Oqtane.Server/Controllers/PageModuleController.cs
+++ b/Oqtane.Server/Controllers/PageModuleController.cs
@@ -44,8 +44,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageModuleId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (pagemodule != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageModuleId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -61,8 +68,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageId} {ModuleId}", pageid, moduleid);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (pagemodule != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized PageModule Get Attempt {PageId} {ModuleId}", pageid, moduleid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/ProfileController.cs b/Oqtane.Server/Controllers/ProfileController.cs
index 04f2c1bd..13e4e859 100644
--- a/Oqtane.Server/Controllers/ProfileController.cs
+++ b/Oqtane.Server/Controllers/ProfileController.cs
@@ -56,8 +56,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Profile Get Attempt {ProfileId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (profile != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Profile Get Attempt {ProfileId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/RoleController.cs b/Oqtane.Server/Controllers/RoleController.cs
index 7a48bdc0..15a880ef 100644
--- a/Oqtane.Server/Controllers/RoleController.cs
+++ b/Oqtane.Server/Controllers/RoleController.cs
@@ -59,9 +59,16 @@ namespace Oqtane.Controllers
return role;
}
else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Role Get Attempt {RoleId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ {
+ if (role != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Role Get Attempt {RoleId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/SettingController.cs b/Oqtane.Server/Controllers/SettingController.cs
index c341155a..9bb9fb8e 100644
--- a/Oqtane.Server/Controllers/SettingController.cs
+++ b/Oqtane.Server/Controllers/SettingController.cs
@@ -89,11 +89,15 @@ namespace Oqtane.Controllers
}
else
{
- if (entityName != EntityNames.Visitor)
+ if (setting != null && entityName != EntityNames.Visitor)
{
_logger.Log(LogLevel.Error, this, LogFunction.Read, "User Not Authorized To Access Setting {EntityName} {SettingId}", entityName, id);
HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
}
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/SiteController.cs b/Oqtane.Server/Controllers/SiteController.cs
index 95de3a92..f239eb14 100644
--- a/Oqtane.Server/Controllers/SiteController.cs
+++ b/Oqtane.Server/Controllers/SiteController.cs
@@ -79,7 +79,7 @@ namespace Oqtane.Controllers
private Site GetSite(int siteid)
{
var site = _sites.GetSite(siteid);
- if (site.SiteId == _alias.SiteId)
+ if (site != null && site.SiteId == _alias.SiteId)
{
// site settings
site.Settings = _settings.GetSettings(EntityNames.Site, site.SiteId)
@@ -153,8 +153,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", siteid);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (site != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Site Get Attempt {SiteId}", siteid);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -246,7 +253,7 @@ namespace Oqtane.Controllers
foreach (Page child in children)
{
child.Level = level + 1;
- child.HasChildren = pages.Any(item => item.ParentId == child.PageId);
+ child.HasChildren = pages.Any(item => item.ParentId == child.PageId && !item.IsDeleted && item.IsNavigation);
hierarchy.Add(child);
getPath(pageList, child);
}
diff --git a/Oqtane.Server/Controllers/UrlMappingController.cs b/Oqtane.Server/Controllers/UrlMappingController.cs
index ddfd2ddb..a81e2c35 100644
--- a/Oqtane.Server/Controllers/UrlMappingController.cs
+++ b/Oqtane.Server/Controllers/UrlMappingController.cs
@@ -55,9 +55,16 @@ namespace Oqtane.Controllers
return urlMapping;
}
else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {UrlMappingId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ {
+ if (urlMapping != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {UrlMappingId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
@@ -73,8 +80,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {SiteId} {Url}", siteid, url);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (urlMapping != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized UrlMapping Get Attempt {SiteId} {Url}", siteid, url);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs
index 0090dd43..d6ef858b 100644
--- a/Oqtane.Server/Controllers/UserController.cs
+++ b/Oqtane.Server/Controllers/UserController.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -9,13 +8,13 @@ using System.Linq;
using System.Security.Claims;
using Oqtane.Shared;
using System;
-using System.IO;
using System.Net;
using Oqtane.Enums;
using Oqtane.Infrastructure;
using Oqtane.Repository;
using Oqtane.Security;
using Oqtane.Extensions;
+using Oqtane.Managers;
namespace Oqtane.Controllers
{
@@ -23,31 +22,27 @@ namespace Oqtane.Controllers
public class UserController : Controller
{
private readonly IUserRepository _users;
- private readonly IUserRoleRepository _userRoles;
private readonly UserManager _identityUserManager;
private readonly SignInManager _identitySignInManager;
private readonly ITenantManager _tenantManager;
private readonly INotificationRepository _notifications;
- private readonly IFolderRepository _folders;
+ private readonly IUserManager _userManager;
private readonly ISiteRepository _sites;
private readonly IUserPermissions _userPermissions;
private readonly IJwtManager _jwtManager;
- private readonly ISyncManager _syncManager;
private readonly ILogManager _logger;
- public UserController(IUserRepository users, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ISyncManager syncManager, ILogManager logger)
+ public UserController(IUserRepository users, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IUserManager userManager, ISiteRepository sites, IUserPermissions userPermissions, IJwtManager jwtManager, ILogManager logger)
{
_users = users;
- _userRoles = userRoles;
_identityUserManager = identityUserManager;
_identitySignInManager = identitySignInManager;
_tenantManager = tenantManager;
_notifications = notifications;
- _folders = folders;
+ _userManager = userManager;
_sites = sites;
_userPermissions = userPermissions;
_jwtManager = jwtManager;
- _syncManager = syncManager;
_logger = logger;
}
@@ -56,14 +51,12 @@ namespace Oqtane.Controllers
[Authorize]
public User Get(int id, string siteid)
{
- int SiteId;
- if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
+ if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
- User user = _users.GetUser(id);
- if (user != null)
+ User user = _userManager.GetUser(id, SiteId);
+ if (user == null)
{
- user.SiteId = int.Parse(siteid);
- user.Roles = GetUserRoles(user.UserId, user.SiteId);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return Filter(user);
}
@@ -79,14 +72,12 @@ namespace Oqtane.Controllers
[HttpGet("name/{name}")]
public User Get(string name, string siteid)
{
- int SiteId;
- if (int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
+ if (int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
- User user = _users.GetUser(name);
- if (user != null)
+ User user = _userManager.GetUser(name, SiteId);
+ if (user == null)
{
- user.SiteId = int.Parse(siteid);
- user.Roles = GetUserRoles(user.UserId, user.SiteId);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
}
return Filter(user);
}
@@ -133,8 +124,28 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId)
{
- var User = await CreateUser(user);
- return User;
+ bool allowregistration;
+ if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
+ {
+ user.EmailConfirmed = true;
+ allowregistration = true;
+ }
+ else
+ {
+ user.EmailConfirmed = false;
+ allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
+ }
+
+ if (allowregistration)
+ {
+ user = await _userManager.AddUser(user);
+ }
+ else
+ {
+ _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "User Registration Is Not Enabled For Site. User Was Not Added {User}", user);
+ }
+
+ return user;
}
else
{
@@ -145,99 +156,6 @@ namespace Oqtane.Controllers
}
}
- private async Task CreateUser(User user)
- {
- User newUser = null;
-
- bool verified;
- bool allowregistration;
- if (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin))
- {
- verified = true;
- allowregistration = true;
- }
- else
- {
- verified = false;
- allowregistration = _sites.GetSite(user.SiteId).AllowRegistration;
- }
-
- if (allowregistration)
- {
- bool succeeded;
- string errors = "";
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser == null)
- {
- identityuser = new IdentityUser();
- identityuser.UserName = user.Username;
- identityuser.Email = user.Email;
- identityuser.EmailConfirmed = verified;
- var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
- succeeded = result.Succeeded;
- if (!succeeded)
- {
- errors = string.Join(", ", result.Errors.Select(e => e.Description));
- }
- }
- else
- {
- var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
- succeeded = result.Succeeded;
- if (!succeeded)
- {
- errors = "Password Not Valid For User";
- }
- verified = succeeded;
- }
-
- if (succeeded)
- {
- user.LastLoginOn = null;
- user.LastIPAddress = "";
- newUser = _users.AddUser(user);
- _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, newUser.UserId, SyncEventActions.Create);
- }
- else
- {
- _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {Username} - {Errors}", user.Username, errors);
- }
-
- if (newUser != null)
- {
- if (!verified)
- {
- string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
- string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
- string body = "Dear " + user.DisplayName + ",\n\nIn Order To Complete The Registration Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
- var notification = new Notification(user.SiteId, newUser, "User Account Verification", body);
- _notifications.AddNotification(notification);
- }
- else
- {
- string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name;
- string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
- var notification = new Notification(user.SiteId, newUser, "User Account Notification", body);
- _notifications.AddNotification(notification);
- }
-
- newUser.Password = ""; // remove sensitive information
- _logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", newUser);
- }
- else
- {
- user.Password = ""; // remove sensitive information
- _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {User}", user);
- }
- }
- else
- {
- _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "User Registration Is Not Enabled For Site. User Was Not Added {User}", user);
- }
-
- return newUser;
- }
-
// PUT api//5
[HttpPut("{id}")]
[Authorize]
@@ -246,37 +164,7 @@ namespace Oqtane.Controllers
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && _users.GetUser(user.UserId, false) != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null)
- {
- identityuser.Email = user.Email;
- var valid = true;
- if (user.Password != "")
- {
- var validator = new PasswordValidator();
- var result = await validator.ValidateAsync(_identityUserManager, null, user.Password);
- valid = result.Succeeded;
- if (valid)
- {
- identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
- }
- }
- if (valid)
- {
- await _identityUserManager.UpdateAsync(identityuser);
-
- user = _users.UpdateUser(user);
- _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
- _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Refresh);
- user.Password = ""; // remove sensitive information
- _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
- }
- else
- {
- _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username);
- user = null;
- }
- }
+ user = await _userManager.UpdateUser(user);
}
else
{
@@ -293,51 +181,10 @@ namespace Oqtane.Controllers
[Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")]
public async Task Delete(int id, string siteid)
{
- int SiteId;
- User user = _users.GetUser(id);
- if (user != null && int.TryParse(siteid, out SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
+ User user = _users.GetUser(id, false);
+ if (user != null && int.TryParse(siteid, out int SiteId) && SiteId == _tenantManager.GetAlias().SiteId)
{
- // remove user roles for site
- foreach (UserRole userrole in _userRoles.GetUserRoles(user.UserId, SiteId).ToList())
- {
- _userRoles.DeleteUserRole(userrole.UserRoleId);
- _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole);
- }
-
- // remove user folder for site
- var folder = _folders.GetFolder(SiteId, $"Users{user.UserId}/");
- if (folder != null)
- {
- if (Directory.Exists(_folders.GetFolderPath(folder)))
- {
- Directory.Delete(_folders.GetFolderPath(folder), true);
- }
- _folders.DeleteFolder(folder.FolderId);
- _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Folder Deleted {Folder}", folder);
- }
-
- // delete user if they are not a member of any other sites
- if (!_userRoles.GetUserRoles(user.UserId, -1).Any())
- {
- // get identity user
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null)
- {
- // delete identity user
- var result = await _identityUserManager.DeleteAsync(identityuser);
- if (result != null)
- {
- // delete user
- _users.DeleteUser(user.UserId);
- _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Delete);
- _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", user.UserId, result.ToString());
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Deleting User {UserId}", user.UserId);
- }
- }
- }
+ await _userManager.DeleteUser(id, SiteId);
}
else
{
@@ -350,83 +197,15 @@ namespace Oqtane.Controllers
[HttpPost("login")]
public async Task Login([FromBody] User user, bool setCookie, bool isPersistent)
{
- User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
-
if (ModelState.IsValid)
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null)
- {
- var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
- if (result.Succeeded)
- {
- var LastIPAddress = user.LastIPAddress ?? "";
-
- user = _users.GetUser(user.Username);
- if (user.TwoFactorRequired)
- {
- var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
- user.TwoFactorCode = token;
- user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
- _users.UpdateUser(user);
-
- string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
- "\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
- "\n\nThank You!";
- var notification = new Notification(loginUser.SiteId, user, "User Verification Code", body);
- _notifications.AddNotification(notification);
-
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
- loginUser.TwoFactorRequired = true;
- }
- else
- {
- loginUser = _users.GetUser(identityuser.UserName);
- if (loginUser != null)
- {
- if (identityuser.EmailConfirmed)
- {
- loginUser.IsAuthenticated = true;
- loginUser.LastLoginOn = DateTime.UtcNow;
- loginUser.LastIPAddress = LastIPAddress;
- _users.UpdateUser(loginUser);
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
-
- if (setCookie)
- {
- await _identitySignInManager.SignInAsync(identityuser, isPersistent);
- }
- }
- else
- {
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
- }
- }
- }
- }
- else
- {
- if (result.IsLockedOut)
- {
- user = _users.GetUser(user.Username);
- string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
- string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
- string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
- "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
- "\n\nThank You!";
- var notification = new Notification(loginUser.SiteId, user, "User Lockout", body);
- _notifications.AddNotification(notification);
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
- }
- else
- {
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
- }
- }
- }
+ user = await _userManager.LoginUser(user, setCookie, isPersistent);
}
-
- return loginUser;
+ else
+ {
+ user = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
+ }
+ return user;
}
// POST api//logout
@@ -444,25 +223,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null && !string.IsNullOrEmpty(token))
- {
- var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
- if (result.Succeeded)
- {
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "Email Verified For {Username}", user.Username);
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
- user = null;
- }
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}And Token {Token}", user.Username, token);
- user = null;
- }
+ user = await _userManager.VerifyEmail(user, token);
}
return user;
}
@@ -473,25 +234,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null)
- {
- user = _users.GetUser(user.Username);
- string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
- string url = HttpContext.Request.Scheme + "://" + _tenantManager.GetAlias().Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
- string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
- "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
- "\n\nIf you did not request to reset your password you can safely ignore this message." +
- "\n\nThank You!";
-
- var notification = new Notification(_tenantManager.GetAlias().SiteId, user, "User Password Reset", body);
- _notifications.AddNotification(notification);
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
- }
+ await _userManager.ForgotPassword(user);
}
}
@@ -501,26 +244,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null && !string.IsNullOrEmpty(token))
- {
- var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
- if (result.Succeeded)
- {
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username);
- user.Password = "";
- }
- else
- {
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
- user = null;
- }
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} And Token {Token}", user.Username, token);
- user = null;
- }
+ user = await _userManager.ResetPassword(user, token);
}
return user;
}
@@ -529,21 +253,16 @@ namespace Oqtane.Controllers
[HttpPost("twofactor")]
public User TwoFactor([FromBody] User user, string token)
{
- User loginUser = new User { SiteId = user.SiteId, Username = user.Username, IsAuthenticated = false };
-
if (ModelState.IsValid && !string.IsNullOrEmpty(token))
{
- user = _users.GetUser(user.Username);
- if (user != null)
- {
- if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
- {
- loginUser.IsAuthenticated = true;
- }
- }
+ user = _userManager.VerifyTwoFactor(user, token);
+ }
+ else
+ {
+ user.IsAuthenticated = false;
}
- return loginUser;
+ return user;
}
// POST api//link
@@ -552,23 +271,7 @@ namespace Oqtane.Controllers
{
if (ModelState.IsValid)
{
- IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
- if (identityuser != null && !string.IsNullOrEmpty(token))
- {
- var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
- if (result.Succeeded)
- {
- // make LoginProvider multi-tenant aware
- type += ":" + user.SiteId.ToString();
- await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
- _logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
- }
- else
- {
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
- user = null;
- }
- }
+ user = await _userManager.LinkExternalAccount(user, token, type, key, name);
}
else
{
@@ -582,9 +285,7 @@ namespace Oqtane.Controllers
[HttpGet("validate/{password}")]
public async Task Validate(string password)
{
- var validator = new PasswordValidator();
- var result = await validator.ValidateAsync(_identityUserManager, null, password);
- return result.Succeeded;
+ return await _userManager.ValidatePassword(password);
}
// GET api//token
@@ -640,21 +341,5 @@ namespace Oqtane.Controllers
}
return user;
}
-
- private string GetUserRoles(int userId, int siteId)
- {
- string roles = "";
- List userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
- foreach (UserRole userrole in userroles)
- {
- roles += userrole.Role.Name + ";";
- if (userrole.Role.Name == RoleNames.Host && userroles.Where(item => item.Role.Name == RoleNames.Admin).FirstOrDefault() == null)
- {
- roles += RoleNames.Admin + ";";
- }
- }
- if (roles != "") roles = ";" + roles;
- return roles;
- }
}
}
diff --git a/Oqtane.Server/Controllers/UserRoleController.cs b/Oqtane.Server/Controllers/UserRoleController.cs
index ab083144..a33872f3 100644
--- a/Oqtane.Server/Controllers/UserRoleController.cs
+++ b/Oqtane.Server/Controllers/UserRoleController.cs
@@ -79,8 +79,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Role Get Attempt {UserRoleId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (userrole != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized User Role Get Attempt {UserRoleId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Controllers/VisitorController.cs b/Oqtane.Server/Controllers/VisitorController.cs
index c4a8de73..46bf8935 100644
--- a/Oqtane.Server/Controllers/VisitorController.cs
+++ b/Oqtane.Server/Controllers/VisitorController.cs
@@ -64,8 +64,15 @@ namespace Oqtane.Controllers
}
else
{
- _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Visitor Get Attempt {VisitorId}", id);
- HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ if (visitor != null)
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Unauthorized Visitor Get Attempt {VisitorId}", id);
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
+ }
+ else
+ {
+ HttpContext.Response.StatusCode = (int)HttpStatusCode.NotFound;
+ }
return null;
}
}
diff --git a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
index a7239af2..73c5d9dd 100644
--- a/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneServiceCollectionExtensions.cs
@@ -19,6 +19,7 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using Oqtane.Infrastructure;
+using Oqtane.Managers;
using Oqtane.Models;
using Oqtane.Modules;
using Oqtane.Repository;
@@ -73,13 +74,7 @@ namespace Microsoft.Extensions.DependencyInjection
internal static IServiceCollection AddOqtaneTransientServices(this IServiceCollection services)
{
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
- services.AddTransient();
-
+ // repositories
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -95,7 +90,6 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
@@ -104,11 +98,21 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddTransient();
services.AddTransient();
services.AddTransient();
- services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
+ // managers
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
// obsolete - replaced by ITenantManager
services.AddTransient();
diff --git a/Oqtane.Server/Infrastructure/InstallationManager.cs b/Oqtane.Server/Infrastructure/InstallationManager.cs
index fa856456..835350b6 100644
--- a/Oqtane.Server/Infrastructure/InstallationManager.cs
+++ b/Oqtane.Server/Infrastructure/InstallationManager.cs
@@ -201,7 +201,24 @@ namespace Oqtane.Infrastructure
{
Directory.CreateDirectory(Path.GetDirectoryName(filename));
}
- entry.ExtractToFile(filename, true);
+ if (Path.Exists(filename) && Path.GetExtension(filename).ToLower() == ".dll")
+ {
+ // ensure assembly version is equal to or greater than existing assembly
+ var assembly = filename.Replace(Path.GetFileName(filename), "temp.dll");
+ entry.ExtractToFile(assembly, true);
+ if (Version.Parse(FileVersionInfo.GetVersionInfo(assembly).FileVersion).CompareTo(Version.Parse(FileVersionInfo.GetVersionInfo(filename).FileVersion)) >= 0)
+ {
+ File.Move(assembly, filename, true);
+ }
+ else
+ {
+ File.Delete(assembly);
+ }
+ }
+ else
+ {
+ entry.ExtractToFile(filename, true);
+ }
}
catch
{
diff --git a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
index a761bd0f..04ebe920 100644
--- a/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
+++ b/Oqtane.Server/Infrastructure/Jobs/NotificationJob.cs
@@ -42,7 +42,7 @@ namespace Oqtane.Infrastructure
// get site settings
List sitesettings = settingRepository.GetSettings(EntityNames.Site, site.SiteId).ToList();
Dictionary settings = GetSettings(sitesettings);
- if (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True")
+ if (!site.IsDeleted && (!settings.ContainsKey("SMTPEnabled") || settings["SMTPEnabled"] == "True"))
{
if (settings.ContainsKey("SMTPHost") && settings["SMTPHost"] != "" &&
settings.ContainsKey("SMTPPort") && settings["SMTPPort"] != "" &&
@@ -162,7 +162,7 @@ namespace Oqtane.Infrastructure
}
else
{
- log += "SMTP Disabled In Site Settings" + " ";
+ log += "Site Deleted Or SMTP Disabled In Site Settings" + " ";
}
}
diff --git a/Oqtane.Server/Infrastructure/LogManager.cs b/Oqtane.Server/Infrastructure/LogManager.cs
index 900f5e91..27d4df50 100644
--- a/Oqtane.Server/Infrastructure/LogManager.cs
+++ b/Oqtane.Server/Infrastructure/LogManager.cs
@@ -210,7 +210,7 @@ namespace Oqtane.Infrastructure
{
var subject = $"{alias.Name} Site {log.Level} Notification";
var url = $"{_accessor.HttpContext.Request.Scheme}://{alias.Name}/admin/log?id={log.LogId}";
- string body = $"Log Message: {log.Message}\n\nPlease visit {url} for more information";
+ string body = $"Log Message: {log.Message}
Please visit {url} for more information";
var notification = new Notification(log.SiteId.Value, userrole.User, subject, body);
_notifications.AddNotification(notification);
}
diff --git a/Oqtane.Server/Managers/Interfaces/IUserManager.cs b/Oqtane.Server/Managers/Interfaces/IUserManager.cs
new file mode 100644
index 00000000..ec58c091
--- /dev/null
+++ b/Oqtane.Server/Managers/Interfaces/IUserManager.cs
@@ -0,0 +1,21 @@
+using System.Threading.Tasks;
+using Oqtane.Models;
+
+namespace Oqtane.Managers
+{
+ public interface IUserManager
+ {
+ User GetUser(int userid, int siteid);
+ User GetUser(string username, int siteid);
+ Task AddUser(User user);
+ Task UpdateUser(User user);
+ Task DeleteUser(int userid, int siteid);
+ Task LoginUser(User user, bool setCookie, bool isPersistent);
+ Task VerifyEmail(User user, string token);
+ Task ForgotPassword(User user);
+ Task ResetPassword(User user, string token);
+ User VerifyTwoFactor(User user, string token);
+ Task LinkExternalAccount(User user, string token, string type, string key, string name);
+ Task ValidatePassword(string password);
+ }
+}
diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs
new file mode 100644
index 00000000..92677c78
--- /dev/null
+++ b/Oqtane.Server/Managers/UserManager.cs
@@ -0,0 +1,434 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Identity;
+using Oqtane.Enums;
+using Oqtane.Infrastructure;
+using Oqtane.Models;
+using Oqtane.Repository;
+using Oqtane.Shared;
+
+namespace Oqtane.Managers
+{
+ public class UserManager : IUserManager
+ {
+ private readonly IUserRepository _users;
+ private readonly IUserRoleRepository _userRoles;
+ private readonly UserManager _identityUserManager;
+ private readonly SignInManager _identitySignInManager;
+ private readonly ITenantManager _tenantManager;
+ private readonly INotificationRepository _notifications;
+ private readonly IFolderRepository _folders;
+ private readonly ISyncManager _syncManager;
+ private readonly ILogManager _logger;
+
+ public UserManager(IUserRepository users, IUserRoleRepository userRoles, UserManager identityUserManager, SignInManager identitySignInManager, ITenantManager tenantManager, INotificationRepository notifications, IFolderRepository folders, ISyncManager syncManager, ILogManager logger)
+ {
+ _users = users;
+ _userRoles = userRoles;
+ _identityUserManager = identityUserManager;
+ _identitySignInManager = identitySignInManager;
+ _tenantManager = tenantManager;
+ _notifications = notifications;
+ _folders = folders;
+ _syncManager = syncManager;
+ _logger = logger;
+ }
+
+ public User GetUser(int userid, int siteid)
+ {
+ User user = _users.GetUser(userid);
+ if (user != null)
+ {
+ user.SiteId = siteid;
+ user.Roles = GetUserRoles(user.UserId, user.SiteId);
+ }
+ return user;
+ }
+
+ public User GetUser(string username, int siteid)
+ {
+ User user = _users.GetUser(username);
+ if (user != null)
+ {
+ user.SiteId = siteid;
+ user.Roles = GetUserRoles(user.UserId, user.SiteId);
+ }
+ return user;
+ }
+
+ private string GetUserRoles(int userId, int siteId)
+ {
+ string roles = "";
+ List userroles = _userRoles.GetUserRoles(userId, siteId).ToList();
+ foreach (UserRole userrole in userroles)
+ {
+ roles += userrole.Role.Name + ";";
+ if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Admin))
+ {
+ roles += RoleNames.Admin + ";";
+ }
+ if (userrole.Role.Name == RoleNames.Host && !userroles.Any(item => item.Role.Name == RoleNames.Registered))
+ {
+ roles += RoleNames.Registered + ";";
+ }
+ }
+ if (roles != "") roles = ";" + roles;
+ return roles;
+ }
+
+ public async Task AddUser(User user)
+ {
+ User User = null;
+ var alias = _tenantManager.GetAlias();
+ bool succeeded = false;
+ string errors = "";
+
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser == null)
+ {
+ identityuser = new IdentityUser();
+ identityuser.UserName = user.Username;
+ identityuser.Email = user.Email;
+ identityuser.EmailConfirmed = user.EmailConfirmed;
+ var result = await _identityUserManager.CreateAsync(identityuser, user.Password);
+ succeeded = result.Succeeded;
+ if (!succeeded)
+ {
+ errors = string.Join(", ", result.Errors.Select(e => e.Description));
+ }
+ }
+ else
+ {
+ var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
+ succeeded = result.Succeeded;
+ if (!succeeded)
+ {
+ errors = "Password Not Valid For User";
+ }
+ user.EmailConfirmed = succeeded;
+ }
+
+ if (succeeded)
+ {
+ user.DisplayName = (user.DisplayName == null) ? user.Username : user.DisplayName;
+ user.LastLoginOn = null;
+ user.LastIPAddress = "";
+ User = _users.AddUser(user);
+ _syncManager.AddSyncEvent(alias.TenantId, EntityNames.User, User.UserId, SyncEventActions.Create);
+ }
+ else
+ {
+ _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {Username} - {Errors}", user.Username, errors);
+ }
+
+ if (User != null)
+ {
+ 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 Complete The Registration Of 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);
+ }
+ else
+ {
+ string url = alias.Protocol + "://" + alias.Name;
+ string body = "Dear " + user.DisplayName + ",\n\nA User Account Has Been Successfully Created For You. Please Use The Following Link To Access The Site:\n\n" + url + "\n\nThank You!";
+ var notification = new Notification(user.SiteId, User, "User Account Notification", body);
+ _notifications.AddNotification(notification);
+ }
+
+ User.Password = ""; // remove sensitive information
+ _logger.Log(user.SiteId, LogLevel.Information, this, LogFunction.Create, "User Added {User}", User);
+ }
+ else
+ {
+ user.Password = ""; // remove sensitive information
+ _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Create, "Unable To Add User {User}", user);
+ }
+
+ return User;
+ }
+
+ public async Task UpdateUser(User user)
+ {
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ identityuser.Email = user.Email;
+ var valid = true;
+ if (user.Password != "")
+ {
+ var validator = new PasswordValidator();
+ var result = await validator.ValidateAsync(_identityUserManager, null, user.Password);
+ valid = result.Succeeded;
+ if (valid)
+ {
+ identityuser.PasswordHash = _identityUserManager.PasswordHasher.HashPassword(identityuser, user.Password);
+ }
+ }
+ if (valid)
+ {
+ await _identityUserManager.UpdateAsync(identityuser);
+
+ user = _users.UpdateUser(user);
+ _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Update);
+ _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, user.UserId, SyncEventActions.Refresh);
+ user.Password = ""; // remove sensitive information
+ _logger.Log(LogLevel.Information, this, LogFunction.Update, "User Updated {User}", user);
+ }
+ else
+ {
+ _logger.Log(user.SiteId, LogLevel.Error, this, LogFunction.Update, "Unable To Update User {Username}. Password Does Not Meet Complexity Requirements.", user.Username);
+ user = null;
+ }
+ }
+
+ return user;
+ }
+
+ public async Task DeleteUser(int userid, int siteid)
+ {
+ // remove user roles for site
+ foreach (UserRole userrole in _userRoles.GetUserRoles(userid, siteid).ToList())
+ {
+ _userRoles.DeleteUserRole(userrole.UserRoleId);
+ _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Role Deleted {UserRole}", userrole);
+ }
+
+ // remove user folder for site
+ var folder = _folders.GetFolder(siteid, $"Users/{userid}/");
+ if (folder != null)
+ {
+ if (Directory.Exists(_folders.GetFolderPath(folder)))
+ {
+ Directory.Delete(_folders.GetFolderPath(folder), true);
+ }
+ _folders.DeleteFolder(folder.FolderId);
+ _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Folder Deleted {Folder}", folder);
+ }
+
+ // delete user if they are not a member of any other sites
+ if (!_userRoles.GetUserRoles(userid, -1).Any())
+ {
+ // get identity user
+ var user = _users.GetUser(userid, false);
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ // delete identity user
+ var result = await _identityUserManager.DeleteAsync(identityuser);
+ if (result != null)
+ {
+ // delete user
+ _users.DeleteUser(userid);
+ _syncManager.AddSyncEvent(_tenantManager.GetAlias().TenantId, EntityNames.User, userid, SyncEventActions.Delete);
+ _logger.Log(LogLevel.Information, this, LogFunction.Delete, "User Deleted {UserId}", userid, result.ToString());
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Delete, "Error Deleting User {UserId}", userid);
+ }
+ }
+ }
+ }
+
+ public async Task LoginUser(User user, bool setCookie, bool isPersistent)
+ {
+ user.IsAuthenticated = false;
+
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ var result = await _identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, true);
+ if (result.Succeeded)
+ {
+ var LastIPAddress = user.LastIPAddress ?? "";
+
+ user = _users.GetUser(user.Username);
+ if (user.TwoFactorRequired)
+ {
+ var token = await _identityUserManager.GenerateTwoFactorTokenAsync(identityuser, "Email");
+ user.TwoFactorCode = token;
+ user.TwoFactorExpiry = DateTime.UtcNow.AddMinutes(10);
+ _users.UpdateUser(user);
+
+ string body = "Dear " + user.DisplayName + ",\n\nYou requested a secure verification code to log in to your account. Please enter the secure verification code on the site:\n\n" + token +
+ "\n\nPlease note that the code is only valid for 10 minutes so if you are unable to take action within that time period, you should initiate a new login on the site." +
+ "\n\nThank You!";
+ var notification = new Notification(user.SiteId, user, "User Verification Code", body);
+ _notifications.AddNotification(notification);
+
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Verification Notification Sent For {Username}", user.Username);
+ user.TwoFactorRequired = true;
+ }
+ else
+ {
+ user = _users.GetUser(identityuser.UserName);
+ if (user != null)
+ {
+ if (identityuser.EmailConfirmed)
+ {
+ user.IsAuthenticated = true;
+ user.LastLoginOn = DateTime.UtcNow;
+ user.LastIPAddress = LastIPAddress;
+ _users.UpdateUser(user);
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful {Username}", user.Username);
+
+ if (setCookie)
+ {
+ await _identitySignInManager.SignInAsync(identityuser, isPersistent);
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Not Verified {Username}", user.Username);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (result.IsLockedOut)
+ {
+ var alias = _tenantManager.GetAlias();
+ user = _users.GetUser(user.Username);
+ string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
+ string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
+ string body = "Dear " + user.DisplayName + ",\n\nYou attempted multiple times unsuccessfully to log in to your account and it is now locked out. Please wait a few minutes and then try again... or use the link below to reset your password:\n\n" + url +
+ "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
+ "\n\nThank You!";
+ var notification = new Notification(user.SiteId, user, "User Lockout", body);
+ _notifications.AddNotification(notification);
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Lockout Notification Sent For {Username}", user.Username);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Failed {Username}", user.Username);
+ }
+ }
+ }
+
+ return user;
+ }
+
+ public async Task VerifyEmail(User user, string token)
+ {
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null && !string.IsNullOrEmpty(token))
+ {
+ var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
+ if (result.Succeeded)
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Email Verified For {Username}", user.Username);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
+ user = null;
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Email Verification Failed For {Username}And Token {Token}", user.Username, token);
+ user = null;
+ }
+ return user;
+ }
+ public async Task ForgotPassword(User user)
+ {
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null)
+ {
+ var alias = _tenantManager.GetAlias();
+ user = _users.GetUser(user.Username);
+ string token = await _identityUserManager.GeneratePasswordResetTokenAsync(identityuser);
+ string url = alias.Protocol + "://" + alias.Name + "/reset?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
+ string body = "Dear " + user.DisplayName + ",\n\nYou recently requested to reset your password. Please use the link below to complete the process:\n\n" + url +
+ "\n\nPlease note that the link is only valid for 24 hours so if you are unable to take action within that time period, you should initiate another password reset on the site." +
+ "\n\nIf you did not request to reset your password you can safely ignore this message." +
+ "\n\nThank You!";
+
+ var notification = new Notification(_tenantManager.GetAlias().SiteId, user, "User Password Reset", body);
+ _notifications.AddNotification(notification);
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Notification Sent For {Username}", user.Username);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Notification Failed For {Username}", user.Username);
+ }
+ }
+
+ public async Task ResetPassword(User user, string token)
+ {
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null && !string.IsNullOrEmpty(token))
+ {
+ var result = await _identityUserManager.ResetPasswordAsync(identityuser, token, user.Password);
+ if (result.Succeeded)
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset For {Username}", user.Username);
+ user.Password = "";
+ }
+ else
+ {
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "Password Reset Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
+ user = null;
+ }
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "Password Reset Failed For {Username} And Token {Token}", user.Username, token);
+ user = null;
+ }
+ return user;
+ }
+
+ public User VerifyTwoFactor(User user, string token)
+ {
+ user = _users.GetUser(user.Username);
+ if (user != null)
+ {
+ if (user.TwoFactorRequired && user.TwoFactorCode == token && DateTime.UtcNow < user.TwoFactorExpiry)
+ {
+ user.IsAuthenticated = true;
+ }
+ }
+ return user;
+ }
+
+ public async Task LinkExternalAccount(User user, string token, string type, string key, string name)
+ {
+ IdentityUser identityuser = await _identityUserManager.FindByNameAsync(user.Username);
+ if (identityuser != null && !string.IsNullOrEmpty(token))
+ {
+ var result = await _identityUserManager.ConfirmEmailAsync(identityuser, token);
+ if (result.Succeeded)
+ {
+ // make LoginProvider multi-tenant aware
+ type += ":" + user.SiteId.ToString();
+ await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(type, key, name));
+ _logger.Log(LogLevel.Information, this, LogFunction.Security, "External Login Linkage Successful For {Username} And Provider {Provider}", user.Username, type);
+ }
+ else
+ {
+ _logger.Log(LogLevel.Error, this, LogFunction.Security, "External Login Linkage Failed For {Username} - Error {Error}", user.Username, string.Join(" ", result.Errors.ToList().Select(e => e.Description)));
+ user = null;
+ }
+ }
+ return user;
+ }
+
+ public async Task ValidatePassword(string password)
+ {
+ var validator = new PasswordValidator();
+ var result = await validator.ValidateAsync(_identityUserManager, null, password);
+ return result.Succeeded;
+ }
+ }
+}
diff --git a/Oqtane.Server/Migrations/Tenant/04000101_AddNotificationIsRead.cs b/Oqtane.Server/Migrations/Tenant/04000101_AddNotificationIsRead.cs
new file mode 100644
index 00000000..c0f610a1
--- /dev/null
+++ b/Oqtane.Server/Migrations/Tenant/04000101_AddNotificationIsRead.cs
@@ -0,0 +1,35 @@
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Oqtane.Databases.Interfaces;
+using Oqtane.Migrations.EntityBuilders;
+using Oqtane.Repository;
+using Oqtane.Shared;
+
+namespace Oqtane.Migrations.Tenant
+{
+ [DbContext(typeof(TenantDBContext))]
+ [Migration("Tenant.04.00.01.01")]
+ public class AddNotificationIsRead : MultiDatabaseMigration
+ {
+
+ public AddNotificationIsRead(IDatabase database) : base(database)
+ {
+ }
+
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
+ notificationEntityBuilder.AddBooleanColumn("IsRead", true);
+ notificationEntityBuilder.UpdateColumn("IsRead", "1", "bool", "");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ var notificationEntityBuilder = new NotificationEntityBuilder(migrationBuilder, ActiveDatabase);
+ notificationEntityBuilder.DropColumn("IsRead");
+ }
+
+ }
+
+
+}
diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj
index 780e0400..4b8cc4bf 100644
--- a/Oqtane.Server/Oqtane.Server.csproj
+++ b/Oqtane.Server/Oqtane.Server.csproj
@@ -3,7 +3,7 @@
net7.0
Debug;Release
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
https://github.com/oqtane/oqtane.framework
Git
Oqtane
diff --git a/Oqtane.Server/Pages/_Host.cshtml.cs b/Oqtane.Server/Pages/_Host.cshtml.cs
index 4b44e341..98b94e13 100644
--- a/Oqtane.Server/Pages/_Host.cshtml.cs
+++ b/Oqtane.Server/Pages/_Host.cshtml.cs
@@ -114,7 +114,7 @@ namespace Oqtane.Pages
}
var site = _sites.InitializeSite(alias);
- if (site != null && !site.IsDeleted && site.Runtime != "Hybrid")
+ if (site != null && (!site.IsDeleted || url.Contains("admin/site")) && site.Runtime != "Hybrid")
{
Route route = new Route(url, alias.Path);
diff --git a/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs b/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs
index 34fb58be..948d7b53 100644
--- a/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs
+++ b/Oqtane.Server/Repository/Interfaces/INotificationRepository.cs
@@ -6,6 +6,8 @@ namespace Oqtane.Repository
public interface INotificationRepository
{
IEnumerable GetNotifications(int siteId, int fromUserId, int toUserId);
+ IEnumerable GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead);
+ int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead);
Notification AddNotification(Notification notification);
Notification UpdateNotification(Notification notification);
Notification GetNotification(int notificationId);
diff --git a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs
index a82cce99..93d0f7f4 100644
--- a/Oqtane.Server/Repository/ModuleDefinitionRepository.cs
+++ b/Oqtane.Server/Repository/ModuleDefinitionRepository.cs
@@ -101,6 +101,7 @@ namespace Oqtane.Repository
ModuleDefinition.IsPortable = moduleDefinition.IsPortable;
ModuleDefinition.Resources = moduleDefinition.Resources;
ModuleDefinition.IsEnabled = moduleDefinition.IsEnabled;
+ ModuleDefinition.PackageName = moduleDefinition.PackageName;
}
return ModuleDefinition;
diff --git a/Oqtane.Server/Repository/NotificationRepository.cs b/Oqtane.Server/Repository/NotificationRepository.cs
index 7596ee94..43f9b386 100644
--- a/Oqtane.Server/Repository/NotificationRepository.cs
+++ b/Oqtane.Server/Repository/NotificationRepository.cs
@@ -33,6 +33,54 @@ namespace Oqtane.Repository
.ToList();
}
+ public IEnumerable GetNotifications(int siteId, int fromUserId, int toUserId, int count, bool isRead)
+ {
+ if (toUserId == -1 && fromUserId == -1)
+ {
+ return _db.Notification
+ .Where(item => item.SiteId == siteId)
+ .Where(item => item.IsDelivered == false && item.IsDeleted == false)
+ .Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
+ .Where(item => item.IsRead == isRead)
+ .OrderByDescending(item => item.CreatedOn)
+ .ToList()
+ .Take(count);
+ }
+
+ return _db.Notification
+ .Where(item => item.SiteId == siteId)
+ .Where(item => item.ToUserId == toUserId || toUserId == -1)
+ .Where(item => item.FromUserId == fromUserId || fromUserId == -1)
+ .Where(item => item.IsRead == isRead)
+ .OrderByDescending(item => item.CreatedOn)
+ .ToList()
+ .Take(count);
+ }
+
+ public int GetNotificationCount(int siteId, int fromUserId, int toUserId, bool isRead)
+ {
+ if (toUserId == -1 && fromUserId == -1)
+ {
+ return _db.Notification
+ .Where(item => item.SiteId == siteId)
+ .Where(item => item.IsDelivered == false && item.IsDeleted == false)
+ .Where(item => item.SendOn == null || item.SendOn < System.DateTime.UtcNow)
+ .Where(item => item.IsRead == isRead)
+ .ToList()
+ .Count();
+
+ }
+
+ return _db.Notification
+ .Where(item => item.SiteId == siteId)
+ .Where(item => item.ToUserId == toUserId || toUserId == -1)
+ .Where(item => item.FromUserId == fromUserId || fromUserId == -1)
+ .Where(item => item.IsRead == isRead)
+ .ToList()
+ .Count();
+ }
+
+
public Notification AddNotification(Notification notification)
{
_db.Notification.Add(notification);
diff --git a/Oqtane.Server/Repository/ThemeRepository.cs b/Oqtane.Server/Repository/ThemeRepository.cs
index 02e4a9c0..d34d2f31 100644
--- a/Oqtane.Server/Repository/ThemeRepository.cs
+++ b/Oqtane.Server/Repository/ThemeRepository.cs
@@ -89,6 +89,7 @@ namespace Oqtane.Repository
Theme.Containers = theme.Containers;
Theme.ThemeSettingsType = theme.ThemeSettingsType;
Theme.ContainerSettingsType = theme.ContainerSettingsType;
+ Theme.PackageName = theme.PackageName;
Themes.Add(Theme);
}
diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Settings.razor b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Settings.razor
index d8f9bfaf..a29842ff 100644
--- a/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Settings.razor
+++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Client/Modules/[Owner].Module.[Module]/Settings.razor
@@ -13,7 +13,7 @@
@code {
- private string resourceType = "[Owner].[Module].Settings, [Owner].[Module].Client.Oqtane"; // for localization
+ private string resourceType = "[Owner].Module.[Module].Settings, [Owner].Module.[Module].Client.Oqtane"; // for localization
public override string Title => "[Module] Settings";
string _value;
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 98abe0e7..68e4160c 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
@@ -2,7 +2,6 @@
net7.0
- 3.0
1.0.0
[Owner]
[Owner]
diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh
index bad41932..fbf7c393 100644
--- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh
+++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/debug.sh
@@ -1,7 +1,7 @@
-cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Client/bin/Debug/net7.0/[Owner].Module.[Module].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Server/bin/Debug/net7.0/[Owner].Module.[Module].Server.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Shared/bin/Debug/net7.0/[Owner].Module.[Module].Shared.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh
index 2e532412..ae5e55b6 100644
--- a/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh
+++ b/Oqtane.Server/wwwroot/Modules/Templates/External/Package/release.sh
@@ -1,2 +1,2 @@
-"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
+"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Module.[Module].nuspec
cp -f "*.nupkg" "..\..\oqtane.framework\Oqtane.Server\Packages\"
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].sln b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].sln
index a9cbde70..e8622beb 100644
--- a/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].sln
+++ b/Oqtane.Server/wwwroot/Modules/Templates/External/[Owner].Module.[Module].sln
@@ -20,9 +20,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.Build.0 = Release|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak
index 72e692cf..47ba984b 100644
Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.MySQL.nupkg.bak differ
diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak
index ab6529ff..ee3324c0 100644
Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.PostgreSQL.nupkg.bak differ
diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak
index e9cd97e6..603f3fd1 100644
Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.SqlServer.nupkg.bak differ
diff --git a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak
index 054330c8..697d02f4 100644
Binary files a/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak and b/Oqtane.Server/wwwroot/Packages/Oqtane.Database.Sqlite.nupkg.bak differ
diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css
index 1803da00..eac1145d 100644
--- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css
+++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.BlazorTheme/Theme.css
@@ -6,12 +6,14 @@
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
+ color: #000 !important;
}
.table .form-select {
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
+ color: #000 !important;
}
.table .btn-primary {
diff --git a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css
index 947184b6..de27f0cb 100644
--- a/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css
+++ b/Oqtane.Server/wwwroot/Themes/Oqtane.Themes.OqtaneTheme/Theme.css
@@ -12,12 +12,14 @@ body {
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
+ color: #000 !important;
}
.table .form-select {
background-color: #ffffff !important;
border-width: 0.5px !important;
border-bottom-color: #ccc !important;
+ color: #000 !important;
}
.table .btn-primary {
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor
index 90f5d7d7..928ce551 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/Container1.razor
@@ -1,20 +1,46 @@
@namespace [Owner].Theme.[Theme]
@inherits ContainerBase
+@inject ISettingService SettingService
-
+
+ @if (_title && ModuleState.Title != "-")
+ {
+ }
+ else
+ {
+
+ }
@code {
- public override string Name => "Container1";
+ public override string Name => "[Owner] [Theme] - Container1";
+
+ private bool _title = true;
+ private string _classes = "container-fluid";
+
+ protected override void OnParametersSet()
+ {
+ try
+ {
+
+ _title = bool.Parse(SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true"));
+
+ }
+ catch
+ {
+ // error loading container settings
+ }
+ }
+
}
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/ContainerSettings.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/ContainerSettings.razor
new file mode 100644
index 00000000..78cc0d50
--- /dev/null
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Containers/ContainerSettings.razor
@@ -0,0 +1,50 @@
+@namespace [Owner].Theme.[Theme]
+@inherits ModuleBase
+@implements Oqtane.Interfaces.ISettingsControl
+@inject ISettingService SettingService
+@attribute [OqtaneIgnore]
+
+
+
+
+
+
+
+
+
+
+@code {
+ private string resourceType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
+ private string _title = "true";
+
+ protected override void OnInitialized()
+ {
+ try
+ {
+
+ _title = SettingService.GetSetting(ModuleState.Settings, GetType().Namespace + ":Title", "true");
+
+ }
+ catch (Exception ex)
+ {
+ ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
+ }
+ }
+
+ public async Task UpdateSettings()
+ {
+ try
+ {
+ var settings = await SettingService.GetModuleSettingsAsync(ModuleState.ModuleId);
+ settings = SettingService.SetSetting(settings, GetType().Namespace + ":Title", _title);
+ await SettingService.UpdateModuleSettingsAsync(settings, ModuleState.ModuleId);
+ }
+ catch (Exception ex)
+ {
+ ModuleInstance.AddModuleMessage(ex.Message, MessageType.Error);
+ }
+ }
+}
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs
index c6e434ca..cac21823 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/ThemeInfo.cs
@@ -9,9 +9,11 @@ namespace [Owner].Theme.[Theme]
{
public Oqtane.Models.Theme Theme => new Oqtane.Models.Theme
{
- Name = "[Theme]",
+ Name = "[Owner] [Theme]",
Version = "1.0.0",
PackageName = "[Owner].Theme.[Theme]",
+ ThemeSettingsType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane",
+ ContainerSettingsType = "[Owner].Theme.[Theme].ContainerSettings, [Owner].Theme.[Theme].Client.Oqtane",
Resources = new List ()
{
// obtained from https://cdnjs.com/libraries
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor
index 277ba1c0..fdee32d5 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/Theme1.razor
@@ -1,95 +1,96 @@
@namespace [Owner].Theme.[Theme]
@inherits ThemeBase
+@inject ISettingService SettingService
-
-
-
-
-
-
@@ -97,4 +98,21 @@
public override string Name => "Theme1";
public override string Panes => PaneNames.Admin + ",Top Full Width,Top 100%,Left 50%,Right 50%,Left 33%,Center 33%,Right 33%,Left Outer 25%,Left Inner 25%,Right Inner 25%,Right Outer 25%,Left 25%,Center 50%,Right 25%,Left Sidebar 66%,Right Sidebar 33%,Left Sidebar 33%,Right Sidebar 66%,Bottom 100%,Bottom Full Width";
+
+ private bool _login = true;
+ private bool _register = true;
+
+ protected override void OnParametersSet()
+ {
+ try
+ {
+ var settings = SettingService.MergeSettings(PageState.Site.Settings, PageState.Page.Settings);
+ _login = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true"));
+ _register = bool.Parse(SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true"));
+ }
+ catch
+ {
+ // error loading theme settings
+ }
+ }
}
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/ThemeSettings.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/ThemeSettings.razor
new file mode 100644
index 00000000..8db32e1f
--- /dev/null
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/Themes/ThemeSettings.razor
@@ -0,0 +1,140 @@
+@namespace [Owner].Theme.[Theme]
+@inherits ModuleBase
+@implements Oqtane.Interfaces.ISettingsControl
+@inject ISettingService SettingService
+@inject IStringLocalizer Localizer
+@inject IStringLocalizer SharedLocalizer
+ @attribute [OqtaneIgnore]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @code {
+ private int pageId = -1;
+ private string resourceType = "[Owner].Theme.[Theme].ThemeSettings, [Owner].Theme.[Theme].Client.Oqtane"; // for localization
+ private string _scope = "page";
+ private string _login = "-";
+ private string _register = "-";
+
+ protected override async Task OnInitializedAsync()
+ {
+ if (PageState.QueryString.ContainsKey("id"))
+ {
+ pageId = int.Parse(PageState.QueryString["id"]);
+ }
+
+ try
+ {
+ await LoadSettings();
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
+ AddModuleMessage("Error Loading Settings", MessageType.Error);
+ }
+ }
+
+ private async Task LoadSettings()
+ {
+ if (_scope == "site")
+ {
+ var settings = PageState.Site.Settings;
+ _login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "true");
+ _register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "true");
+ }
+ else
+ {
+ var settings = await SettingService.GetPageSettingsAsync(pageId);
+ settings = SettingService.MergeSettings(PageState.Site.Settings, settings);
+ _login = SettingService.GetSetting(settings, GetType().Namespace + ":Login", "-");
+ _register = SettingService.GetSetting(settings, GetType().Namespace + ":Register", "-");
+ }
+ await Task.Yield();
+ }
+
+ private async Task ScopeChanged(ChangeEventArgs eventArgs)
+ {
+ try
+ {
+ _scope = (string)eventArgs.Value;
+ await LoadSettings();
+ StateHasChanged();
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "Error Loading Settings {Error}", ex.Message);
+ AddModuleMessage("Error Loading Settings", MessageType.Error);
+ }
+ }
+
+ public async Task UpdateSettings()
+ {
+ try
+ {
+ if (_scope == "site")
+ {
+ var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId);
+ if (_login != "-")
+ {
+ settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login, true);
+ }
+
+ if (_register != "-")
+ {
+ settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register, true);
+ }
+ await SettingService.UpdateSiteSettingsAsync(settings, PageState.Site.SiteId);
+ }
+ else
+ {
+ var settings = await SettingService.GetPageSettingsAsync(pageId);
+ if (_login != "-")
+ {
+ settings = SettingService.SetSetting(settings, GetType().Namespace + ":Login", _login);
+ }
+ if (_register != "-")
+ {
+ settings = SettingService.SetSetting(settings, GetType().Namespace + ":Register", _register);
+ }
+ await SettingService.UpdatePageSettingsAsync(settings, pageId);
+ }
+ }
+ catch (Exception ex)
+ {
+ await logger.LogError(ex, "Error Saving Settings {Error}", ex.Message);
+ AddModuleMessage("Error Saving Settings", MessageType.Error);
+ }
+ }
+ }
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 8655320d..0799ae41 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
@@ -2,7 +2,6 @@
net7.0
- 3.0
1.0.0
[Owner]
[Owner]
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor
index d206b36e..9e4a6152 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Client/_Imports.razor
@@ -1,13 +1,17 @@
-@using System
+@using System
@using System.Linq
@using System.Collections.Generic
@using System.Net.Http
@using System.Net.Http.Json
+@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
+@using Oqtane
+@using Oqtane.Client
@using Oqtane.Models
@using Oqtane.Modules
@using Oqtane.Modules.Controls
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh
index 3b7eacf0..29b5ef17 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/debug.sh
@@ -1,3 +1,3 @@
-cp -f "../Client/bin/Debug/net7.0/[Owner].Theme.[Theme].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
-cp -f "../Client/bin/Debug/net7.0/[Owner].Theme.[Theme].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Client/bin/Debug/net7.0/[Owner].Theme.[Theme].Client.Oqtane.dll" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
+cp -f "../Client/bin/Debug/net7.0/[Owner].Theme.[Theme].Client.Oqtane.pdb" "../../oqtane.framework/Oqtane.Server/bin/Debug/net7.0/"
cp -rf "../Server/wwwroot/"* "../../oqtane.framework/Oqtane.Server/wwwroot/"
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh
index 63e97155..444e142d 100644
--- a/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/Package/release.sh
@@ -1,2 +1,2 @@
-"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Theme.[Theme].nuspec
+"..\..\oqtane.framework\oqtane.package\nuget.exe" pack [Owner].Theme.[Theme].nuspec
cp -f "*.nupkg" "..\..\oqtane.framework\Oqtane.Server\Packages\"
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].sln b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].sln
new file mode 100644
index 00000000..cd9d50da
--- /dev/null
+++ b/Oqtane.Server/wwwroot/Themes/Templates/External/[Owner].Theme.[Theme].sln
@@ -0,0 +1,35 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.28621.142
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oqtane.Server", "..\[RootFolder]\Oqtane.Server\Oqtane.Server.csproj", "{3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Theme.[Theme].Client", "Client\[Owner].Theme.[Theme].Client.csproj", "{AA8E58A1-CD09-4208-BF66-A8BB341FD669}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "[Owner].Theme.[Theme].Package", "Package\[Owner].Theme.[Theme].Package.csproj", "{C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3AB6FCC9-EFEB-4C0E-A2CF-8103914C5196}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AA8E58A1-CD09-4208-BF66-A8BB341FD669}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C5CE512D-CBB7-4545-AF0F-9B6591A0C3A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {1D016F15-46FE-4726-8DFD-2E4FD4DC7668}
+ EndGlobalSection
+EndGlobal
diff --git a/Oqtane.Server/wwwroot/css/app.css b/Oqtane.Server/wwwroot/css/app.css
index 8a2a4e3a..35deeb07 100644
--- a/Oqtane.Server/wwwroot/css/app.css
+++ b/Oqtane.Server/wwwroot/css/app.css
@@ -219,4 +219,12 @@ app {
/* Pager */
.app-pager-pointer {
cursor: pointer;
+}
+
+.app-sort-th {
+ cursor: pointer;
+}
+
+.app-fas {
+ margin-left: 5px;
}
\ No newline at end of file
diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js
index 8b98c530..792eb464 100644
--- a/Oqtane.Server/wwwroot/js/interop.js
+++ b/Oqtane.Server/wwwroot/js/interop.js
@@ -290,8 +290,10 @@ Oqtane.Interop = {
var progressinfo = document.getElementById('ProgressInfo_' + id);
var progressbar = document.getElementById('ProgressBar_' + id);
- progressinfo.setAttribute("style", "display: inline;");
- progressbar.setAttribute("style", "width: 200px; display: inline;");
+ if (progressinfo !== null && progressbar !== null) {
+ progressinfo.setAttribute("style", "display: inline;");
+ progressbar.setAttribute("style", "width: 100%; display: inline;");
+ }
for (var i = 0; i < files.length; i++) {
var FileChunk = [];
@@ -322,21 +324,29 @@ Oqtane.Interop = {
var request = new XMLHttpRequest();
request.open('POST', posturl, true);
request.upload.onloadstart = function (e) {
- progressinfo.innerHTML = file.name + ' 0%';
- progressbar.value = 0;
+ if (progressinfo !== null && progressbar !== null) {
+ progressinfo.innerHTML = file.name + ' 0%';
+ progressbar.value = 0;
+ }
};
request.upload.onprogress = function (e) {
- var percent = Math.ceil((e.loaded / e.total) * 100);
- progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
- progressbar.value = (percent / 100);
+ if (progressinfo !== null && progressbar !== null) {
+ var percent = Math.ceil((e.loaded / e.total) * 100);
+ progressinfo.innerHTML = file.name + '[' + PartCount + '] ' + percent + '%';
+ progressbar.value = (percent / 100);
+ }
};
request.upload.onloadend = function (e) {
- progressinfo.innerHTML = file.name + ' 100%';
- progressbar.value = 1;
+ if (progressinfo !== null && progressbar !== null) {
+ progressinfo.innerHTML = file.name + ' 100%';
+ progressbar.value = 1;
+ }
};
- request.upload.onerror = function () {
- progressinfo.innerHTML = file.name + ' Error: ' + xhr.status;
- progressbar.value = 0;
+ request.upload.onerror = function() {
+ if (progressinfo !== null && progressbar !== null) {
+ progressinfo.innerHTML = file.name + ' Error: ' + xhr.status;
+ progressbar.value = 0;
+ }
};
request.send(data);
}
diff --git a/Oqtane.Shared/Models/Notification.cs b/Oqtane.Shared/Models/Notification.cs
index 975b28c8..d7e08c01 100644
--- a/Oqtane.Shared/Models/Notification.cs
+++ b/Oqtane.Shared/Models/Notification.cs
@@ -94,6 +94,10 @@ namespace Oqtane.Models
///
public DateTime? SendOn { get; set; }
+ ///
+ /// If it has been read. See also
+ ///
+ public bool IsRead { get; set; }
// constructors
public Notification() {}
@@ -174,6 +178,7 @@ namespace Oqtane.Models
}
IsDelivered = false;
DeliveredOn = null;
+ IsRead = false;
}
}
diff --git a/Oqtane.Shared/Models/Package.cs b/Oqtane.Shared/Models/Package.cs
index 1a57e7bc..1e424744 100644
--- a/Oqtane.Shared/Models/Package.cs
+++ b/Oqtane.Shared/Models/Package.cs
@@ -32,6 +32,11 @@ namespace Oqtane.Models
///
public string Description { get; set; }
+ ///
+ /// logo
+ ///
+ public int? LogoFileId { get; set; }
+
///
/// License for the Package.
///
@@ -62,6 +67,11 @@ namespace Oqtane.Models
///
public string PackageUrl { get; set; }
+ ///
+ /// The direct Url for getting support for the product
+ ///
+ public string SupportUrl { get; set; }
+
///
/// Indicates if any known security vulnerabilities exist
///
diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs
index 4fe450dc..751b6e01 100644
--- a/Oqtane.Shared/Models/User.cs
+++ b/Oqtane.Shared/Models/User.cs
@@ -99,5 +99,11 @@ namespace Oqtane.Models
{
get => "Users\\" + UserId.ToString() + "\\";
}
+
+ ///
+ /// Information if this user's email address is confirmed (set during user creation)
+ ///
+ [NotMapped]
+ public bool EmailConfirmed { get; set; }
}
}
diff --git a/Oqtane.Shared/Oqtane.Shared.csproj b/Oqtane.Shared/Oqtane.Shared.csproj
index 793a2db5..e6062c0e 100644
--- a/Oqtane.Shared/Oqtane.Shared.csproj
+++ b/Oqtane.Shared/Oqtane.Shared.csproj
@@ -3,7 +3,7 @@
net7.0
Debug;Release
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
https://github.com/oqtane/oqtane.framework
Git
Oqtane
diff --git a/Oqtane.Shared/Shared/Constants.cs b/Oqtane.Shared/Shared/Constants.cs
index 916fa310..79c7ada9 100644
--- a/Oqtane.Shared/Shared/Constants.cs
+++ b/Oqtane.Shared/Shared/Constants.cs
@@ -7,8 +7,8 @@ namespace Oqtane.Shared
{
public class Constants
{
- public static readonly string Version = "4.0.0";
- 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";
+ public static readonly string Version = "4.0.1";
+ public const string ReleaseVersions = "1.0.0,1.0.1,1.0.2,1.0.3,1.0.4,2.0.0,2.0.1,2.0.2,2.1.0,2.2.0,2.3.0,2.3.1,3.0.0,3.0.1,3.0.2,3.0.3,3.1.0,3.1.1,3.1.2,3.1.3,3.1.4,3.2.0,3.2.1,3.3.0,3.3.1,3.4.0,3.4.1,3.4.2,3.4.3,4.0.0,4.0.1";
public const string PackageId = "Oqtane.Framework";
public const string ClientId = "Oqtane.Client";
public const string UpdaterPackageId = "Oqtane.Updater";
diff --git a/Oqtane.Updater/Oqtane.Updater.csproj b/Oqtane.Updater/Oqtane.Updater.csproj
index eb79be2f..d7f544f2 100644
--- a/Oqtane.Updater/Oqtane.Updater.csproj
+++ b/Oqtane.Updater/Oqtane.Updater.csproj
@@ -3,7 +3,7 @@
net7.0
Exe
- 4.0.0
+ 4.0.1
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/v4.0.0
+ https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.1
https://github.com/oqtane/oqtane.framework
Git
Oqtane
diff --git a/README.md b/README.md
index cd8e5080..485645d4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Latest Release
-[3.4.3](https://github.com/oqtane/oqtane.framework/releases/tag/v3.4.3) was released on May 3, 2023 and is primarily focused on stablization. This release includes 27 pull requests by 6 different contributors, pushing the total number of project commits all-time over 3400. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
+[4.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0) was released on June 26, 2023 and is a major framework upgrade to .NET 7. This release includes 104 pull requests by 5 different contributors, pushing the total number of project commits all-time over 3600. The Oqtane framework continues to evolve at a rapid pace to meet the needs of .NET developers.
[](https://portal.azure.com/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2Foqtane%2Foqtane.framework%2Fmaster%2Fazuredeploy.json)
@@ -8,7 +8,7 @@

-Oqtane is a CMS and Application Framework. It leverages Blazor, an open source and cross-platform web UI framework for building modern apps using .NET and C# instead of JavaScript. Blazor apps are composed of reusable web UI components implemented using C#, HTML, and CSS. Both client and server code is written in C#, allowing you to share code and libraries.
+Oqtane is an open source CMS and Application Framework that provides advanced functionality for developing web, mobile, and desktop applications on .NET. It leverages Blazor to compose a fully dynamic digital experience which can be hosted on Blazor Server, Blazor WebAssembly, or Blazor Hybrid (via .NET MAUI).
Oqtane is being developed based on some fundamental principles which are outlined in the [Oqtane Philosophy](https://www.oqtane.org/blog/!/20/oqtane-philosophy).
@@ -16,9 +16,9 @@ Please note that this project is owned by the .NET Foundation and is governed by
# Getting Started
-**Using Version 3:**
+**Using Version 4:**
-- Install **[.NET 6 SDK](https://dotnet.microsoft.com/download/dotnet/6.0)**.
+- Install **[.NET 7 SDK](https://dotnet.microsoft.com/download/dotnet/7.0)**.
- Install the latest edition (v17.0 or higher) of [Visual Studio 2022](https://visualstudio.microsoft.com/vs/preview/#download-preview) 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**.
@@ -53,7 +53,7 @@ Backlog (TBD)
5.0.0 (Q4 2023)
- [ ] Migration to .NET 8
-4.0.0 (Q2 2023)
+[4.0.0](https://github.com/oqtane/oqtane.framework/releases/tag/v4.0.0) ( June 26, 2023 )
- [x] Migration to .NET 7
- [x] Improved JavaScript, CSS, and Meta support
- [x] Optimized Client Assembly Loading
@@ -204,6 +204,8 @@ Oqtane was created by [Shaun Walker](https://www.linkedin.com/in/shaunbrucewalke
# Release Announcements
+[Oqtane 4.0](https://www.oqtane.org/blog/!/63/announcing-oqtane-4-0-for-net-7)
+
[Oqtane 3.4](https://www.oqtane.org/blog/!/56/oqtane-3-4-0-released)
[Oqtane 3.3](https://www.oqtane.org/blog/!/54/oqtane-3-3-0-released)
|