diff --git a/LICENSE b/LICENSE index d2b14c41..a8125f20 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018-2024 .NET Foundation +Copyright (c) 2018-2025 .NET Foundation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Oqtane.Client/Installer/Installer.razor b/Oqtane.Client/Installer/Installer.razor index c0187971..c00a0d7e 100644 --- a/Oqtane.Client/Installer/Installer.razor +++ b/Oqtane.Client/Installer/Installer.razor @@ -71,14 +71,14 @@
- +
- +
@@ -87,7 +87,7 @@
- +
@@ -95,7 +95,13 @@
- + +
+
+
+ +
+
@@ -153,6 +159,7 @@ private string _toggleConfirmPassword = string.Empty; private string _confirmPassword = string.Empty; private string _hostEmail = string.Empty; + private string _hostName = string.Empty; private List _templates; private string _template = Constants.DefaultSiteTemplate; private bool _register = true; @@ -236,7 +243,7 @@ } } - if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@")) + if (connectionString != "" && !string.IsNullOrEmpty(_hostUsername) && !string.IsNullOrEmpty(_hostPassword) && _hostPassword == _confirmPassword && !string.IsNullOrEmpty(_hostEmail) && _hostEmail.Contains("@") && !string.IsNullOrEmpty(_hostName)) { var result = await UserService.ValidateUserAsync(_hostUsername, _hostEmail, _hostPassword); if (result.Succeeded) @@ -256,7 +263,7 @@ HostUsername = _hostUsername, HostPassword = _hostPassword, HostEmail = _hostEmail, - HostName = _hostUsername, + HostName = _hostName, TenantName = TenantNames.Master, IsNewTenant = true, SiteName = Constants.DefaultSite, diff --git a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor index 4e4e2607..4b672b61 100644 --- a/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor +++ b/Oqtane.Client/Modules/Admin/RecycleBin/Index.razor @@ -13,71 +13,71 @@ } else { - - - @if (!_pages.Where(item => item.IsDeleted).Any()) - { -
-

@Localizer["NoPage.Deleted"]

- } - else - { - -
-   -   - @SharedLocalizer["Name"] - @Localizer["DeletedBy"] - @Localizer["DeletedOn"] -
- + + + @if (!_pages.Where(item => item.IsDeleted).Any()) + { +
+

@Localizer["NoPage.Deleted"]

+ } + else + { + +
+   +   + @SharedLocalizer["Path"] + @Localizer["DeletedBy"] + @Localizer["DeletedOn"] +
+ - - @context.Name - @context.DeletedBy - @context.DeletedOn - -
-
- - } -
- - @if (!_modules.Where(item => item.IsDeleted).Any()) - { -
-

@Localizer["NoModule.Deleted"]

- } - else - { - -
-   -   - @Localizer["Page"] - @Localizer["Module"] - @Localizer["DeletedBy"] - @Localizer["DeletedOn"] -
- - - - @_pages.Find(item => item.PageId == context.PageId).Name - @context.Title - @context.DeletedBy - @context.DeletedOn - -
-
- - } -
-
+ + @context.Path + @context.DeletedBy + @context.DeletedOn +
+
+
+ + } +
+ + @if (!_modules.Where(item => item.IsDeleted).Any()) + { +
+

@Localizer["NoModule.Deleted"]

+ } + else + { + +
+   +   + @Localizer["Page"] + @Localizer["Module"] + @Localizer["DeletedBy"] + @Localizer["DeletedOn"] +
+ + + + @_pages.Find(item => item.PageId == context.PageId).Name + @context.Title + @context.DeletedBy + @context.DeletedOn + +
+
+ + } +
+
} @code { - private List _pages; - private List _modules; + private List _pages; + private List _modules; private int _pagePage = 1; private int _pageModule = 1; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -105,12 +105,25 @@ else { try { - page.IsDeleted = false; - await PageService.UpdatePageAsync(page); - await logger.LogInformation("Page Restored {Page}", page); - await Load(); - StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); + var validated = true; + if (page.ParentId != null) + { + var parent = _pages.Find(item => item.PageId == page.ParentId); + validated = !parent.IsDeleted; + } + if (validated) + { + page.IsDeleted = false; + await PageService.UpdatePageAsync(page); + await logger.LogInformation("Page Restored {Page}", page); + AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success); + await Load(); + StateHasChanged(); + } + else + { + AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning); + } } catch (Exception ex) { @@ -125,9 +138,9 @@ else { await PageService.DeletePageAsync(page.PageId); await logger.LogInformation("Page Permanently Deleted {Page}", page); + AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success); await Load(); StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); } catch (Exception ex) { @@ -148,10 +161,10 @@ else } await logger.LogInformation("Pages Permanently Deleted"); + AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success); await Load(); HideProgressIndicator(); StateHasChanged(); - NavigationManager.NavigateTo(NavigateUrl()); } catch (Exception ex) { @@ -169,6 +182,7 @@ else pagemodule.IsDeleted = false; await PageModuleService.UpdatePageModuleAsync(pagemodule); await logger.LogInformation("Module Restored {Module}", module); + AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success); await Load(); StateHasChanged(); } @@ -185,6 +199,7 @@ else { await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await logger.LogInformation("Module Permanently Deleted {Module}", module); + AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success); await Load(); StateHasChanged(); } @@ -205,6 +220,7 @@ else await PageModuleService.DeletePageModuleAsync(module.PageModuleId); } await logger.LogInformation("Modules Permanently Deleted"); + AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success); await Load(); HideProgressIndicator(); StateHasChanged(); diff --git a/Oqtane.Client/Modules/Admin/Roles/Users.razor b/Oqtane.Client/Modules/Admin/Roles/Users.razor index d0eb0aac..5f8a6e99 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Users.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Users.razor @@ -58,7 +58,7 @@ else @context.EffectiveDate @context.ExpiryDate - + @@ -180,27 +180,28 @@ else private async Task DeleteUserRole(int UserRoleId) { - validated = true; - var interop = new Interop(JSRuntime); - if (await interop.FormValid(form)) + try { - try + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else { await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); - AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); - await GetUserRoles(); - StateHasChanged(); - } - catch (Exception ex) - { - await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); - AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); } + AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); + await GetUserRoles(); + StateHasChanged(); } - else + catch (Exception ex) { - AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning); + await logger.LogError(ex, "Error Removing User From Role {UserRoleId} {Error}", UserRoleId, ex.Message); + AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); } } } diff --git a/Oqtane.Client/Modules/Admin/Users/Edit.razor b/Oqtane.Client/Modules/Admin/Users/Edit.razor index a58af948..e4aedaa0 100644 --- a/Oqtane.Client/Modules/Admin/Users/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Users/Edit.razor @@ -51,15 +51,18 @@
-
- -
- + @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { +
+ +
+ +
-
+ }
@@ -127,8 +130,11 @@ @SharedLocalizer["Cancel"] -
-
+ @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True") + { + + } +

} @@ -226,8 +232,10 @@ user.Password = _password; user.Email = email; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; - - user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) + { + user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); + } user = await UserService.UpdateUserAsync(user); if (user != null) @@ -259,6 +267,25 @@ } } + private async Task DeleteUser() + { + try + { + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && userid != PageState.User.UserId) + { + var user = await UserService.GetUserAsync(userid, PageState.Site.SiteId); + await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); + await logger.LogInformation("User Permanently Deleted {User}", user); + NavigationManager.NavigateTo(NavigateUrl()); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Permanently Deleting User {UserId} {Error}", userid, ex.Message); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); + } + } + private bool ValidateProfiles() { foreach (Profile profile in profiles) diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index c6114047..8af3d381 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -35,7 +35,7 @@ else - + @@ -611,19 +611,31 @@ else { try { - var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); - if (user != null) + if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host)) { - await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); - await logger.LogInformation("User Deleted {User}", UserRole.User); - await LoadUsersAsync(true); - StateHasChanged(); + var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); + if (user != null) + { + user.IsDeleted = true; + await UserService.UpdateUserAsync(user); + await logger.LogInformation("User Soft Deleted {User}", user); + } } + else + { + var userrole = await UserRoleService.GetUserRoleAsync(UserRole.UserRoleId); + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + AddModuleMessage(Localizer["Success.DeleteUser"], MessageType.Success); + await LoadUsersAsync(true); + StateHasChanged(); } catch (Exception ex) { await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); - AddModuleMessage(ex.Message, MessageType.Error); + AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Roles.razor b/Oqtane.Client/Modules/Admin/Users/Roles.razor index 8d65480a..5ce12b62 100644 --- a/Oqtane.Client/Modules/Admin/Users/Roles.razor +++ b/Oqtane.Client/Modules/Admin/Users/Roles.razor @@ -53,17 +53,17 @@ else

- @Localizer["Roles"] - @Localizer["Effective"] - @Localizer["Expiry"] -   + @Localizer["Roles"] + @Localizer["Effective"] + @Localizer["Expiry"] +  
@context.Role.Name @Utilities.UtcAsLocalDate(context.EffectiveDate) @Utilities.UtcAsLocalDate(context.ExpiryDate) - + @@ -171,8 +171,18 @@ else { try { - await UserRoleService.DeleteUserRoleAsync(UserRoleId); - await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); + var userrole = await UserRoleService.GetUserRoleAsync(UserRoleId); + if (userrole.Role.Name == RoleNames.Registered) + { + userrole.ExpiryDate = DateTime.UtcNow; + await UserRoleService.UpdateUserRoleAsync(userrole); + await logger.LogInformation("User {Username} Expired From Role {Role}", userrole.User.Username, userrole.Role.Name); + } + else + { + await UserRoleService.DeleteUserRoleAsync(UserRoleId); + await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name); + } AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success); await GetUserRoles(); StateHasChanged(); diff --git a/Oqtane.Client/Resources/Installer/Installer.resx b/Oqtane.Client/Resources/Installer/Installer.resx index a06752d9..cd1c2e6e 100644 --- a/Oqtane.Client/Resources/Installer/Installer.resx +++ b/Oqtane.Client/Resources/Installer/Installer.resx @@ -186,4 +186,10 @@ The Username Provided Does Not Meet The System Requirement, It Can Only Contains Letters Or Digits. + + Full Name: + + + Provide the full name of the host user + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx index c45ee304..0e212ab8 100644 --- a/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/RecycleBin/Index.resx @@ -195,4 +195,25 @@ Modules + + You Cannot Restore A Page If Its Parent Is Deleted + + + Page Restored Successfully + + + Page Deleted Successfully + + + All Pages Deleted Successfully + + + Module Restored Successfully + + + Module Deleted Successfully + + + All Modules Deleted Successfully + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx index f56d3798..29aed594 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Edit.resx @@ -195,4 +195,13 @@ Last Login: + + Delete User + + + Delete + + + Are You Sure You Wish To Permanently Delete This User? + \ No newline at end of file diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index 021788e8..34884fb5 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -501,4 +501,10 @@ Specify whether access and refresh tokens should be saved after a successful login. The default is false to reduce the size of the authentication cookie. + + User Deleted Successfully + + + Error Deleting User + \ No newline at end of file diff --git a/Oqtane.Client/Resources/SharedResources.resx b/Oqtane.Client/Resources/SharedResources.resx index 8c5ae010..a0078ea5 100644 --- a/Oqtane.Client/Resources/SharedResources.resx +++ b/Oqtane.Client/Resources/SharedResources.resx @@ -427,7 +427,7 @@ At Least One Uppercase Letter - Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Compexity Requirements For This Site. + Passwords Must Have A Minimum Length Of {0} Characters, Including At Least {1} Unique Character(s), {2}{3}{4}{5} To Satisfy Password Complexity Requirements For This Site. {0} Is Not Valid @@ -474,4 +474,7 @@ User + + Path + \ No newline at end of file diff --git a/Oqtane.Client/Services/InstallationService.cs b/Oqtane.Client/Services/InstallationService.cs index a9e4d3d6..f1fbd057 100644 --- a/Oqtane.Client/Services/InstallationService.cs +++ b/Oqtane.Client/Services/InstallationService.cs @@ -56,10 +56,5 @@ namespace Oqtane.Services { await PostAsync($"{ApiUrl}/restart"); } - - public async Task RegisterAsync(string email) - { - await PostJsonAsync($"{ApiUrl}/register?email={WebUtility.UrlEncode(email)}", true); - } } } diff --git a/Oqtane.Client/Services/Interfaces/IInstallationService.cs b/Oqtane.Client/Services/Interfaces/IInstallationService.cs index 84790c63..e8a433c7 100644 --- a/Oqtane.Client/Services/Interfaces/IInstallationService.cs +++ b/Oqtane.Client/Services/Interfaces/IInstallationService.cs @@ -34,13 +34,5 @@ namespace Oqtane.Services /// /// internal status/message object Task RestartAsync(); - - /// - /// Registers a new - /// - /// Email of the user to be registered - /// - Task RegisterAsync(string email); - } } diff --git a/Oqtane.Server/Components/App.razor b/Oqtane.Server/Components/App.razor index a8a9fefb..e487eecf 100644 --- a/Oqtane.Server/Components/App.razor +++ b/Oqtane.Server/Components/App.razor @@ -174,7 +174,7 @@ // get jwt token for downstream APIs if (Context.User.Identity.IsAuthenticated) { - CreateJwtToken(alias); + GetJwtToken(alias); } // includes resources @@ -441,13 +441,23 @@ } } - private void CreateJwtToken(Alias alias) + private void GetJwtToken(Alias alias) { - var sitesettings = Context.GetSiteSettings(); - var secret = sitesettings.GetValue("JwtOptions:Secret", ""); - if (!string.IsNullOrEmpty(secret)) + _authorizationToken = Context.Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(_authorizationToken)) { - _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); + // bearer token was provided by remote Identity Provider and was persisted using SaveTokens + _authorizationToken = _authorizationToken.Replace("Bearer ", ""); + } + else + { + // generate bearer token if a secret has been configured in User Settings + var sitesettings = Context.GetSiteSettings(); + var secret = sitesettings.GetValue("JwtOptions:Secret", ""); + if (!string.IsNullOrEmpty(secret)) + { + _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); + } } } diff --git a/Oqtane.Server/Controllers/InstallationController.cs b/Oqtane.Server/Controllers/InstallationController.cs index 92e5b289..48bbf8b8 100644 --- a/Oqtane.Server/Controllers/InstallationController.cs +++ b/Oqtane.Server/Controllers/InstallationController.cs @@ -60,9 +60,9 @@ namespace Oqtane.Controllers { installation = _databaseManager.Install(config); - if (installation.Success && config.Register) + if (installation.Success) { - await RegisterContact(config.HostEmail); + await RegisterContact(config.HostEmail, config.HostName, config.Register); } } else @@ -257,7 +257,7 @@ namespace Oqtane.Controllers } } - private async Task RegisterContact(string email) + private async Task RegisterContact(string email, string name, bool register) { try { @@ -268,7 +268,7 @@ namespace Oqtane.Controllers { client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); - var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}")).ConfigureAwait(false); + var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}®ister={register.ToString().ToLower()}")).ConfigureAwait(false); } } } @@ -278,14 +278,6 @@ namespace Oqtane.Controllers } } - // GET api//register?email=x - [HttpPost("register")] - [Authorize(Roles = RoleNames.Host)] - public async Task Register(string email) - { - await RegisterContact(email); - } - public struct ClientAssembly { public ClientAssembly(string filepath, bool hashfilename) diff --git a/Oqtane.Server/Controllers/PageController.cs b/Oqtane.Server/Controllers/PageController.cs index 97c303ac..e57091c7 100644 --- a/Oqtane.Server/Controllers/PageController.cs +++ b/Oqtane.Server/Controllers/PageController.cs @@ -9,6 +9,8 @@ using System.Net; using Oqtane.Enums; using Oqtane.Infrastructure; using Oqtane.Repository; +using System.Xml.Linq; +using Microsoft.AspNetCore.Diagnostics; namespace Oqtane.Controllers { @@ -279,18 +281,14 @@ namespace Oqtane.Controllers // get current page permissions var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList(); - page = _pages.UpdatePage(page); + // preserve new path and deleted status + var newPath = page.Path; + var deleted = page.IsDeleted; + page.Path = currentPage.Path; + page.IsDeleted = currentPage.IsDeleted; - // save url mapping if page path changed - if (currentPage.Path != page.Path) - { - var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path); - if (urlMapping != null) - { - urlMapping.MappedUrl = page.Path; - _urlMappings.UpdateUrlMapping(urlMapping); - } - } + // update page + UpdatePage(page, page.Path, newPath, deleted); // get differences between current and new page permissions var added = GetPermissionsDifferences(page.PermissionList, currentPermissions); @@ -320,6 +318,7 @@ namespace Oqtane.Controllers }); } } + // permissions removed foreach (Permission permission in removed) { @@ -343,7 +342,6 @@ namespace Oqtane.Controllers } } - _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh); // personalized page @@ -378,6 +376,39 @@ namespace Oqtane.Controllers return page; } + private void UpdatePage(Page page, string oldPath, string newPath, bool deleted) + { + var update = false; + if (oldPath != newPath) + { + var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, page.Path); + if (urlMapping != null) + { + urlMapping.MappedUrl = newPath + page.Path.Substring(oldPath.Length); + _urlMappings.UpdateUrlMapping(urlMapping); + } + + page.Path = newPath + page.Path.Substring(oldPath.Length); + update = true; + } + if (deleted != page.IsDeleted) + { + page.IsDeleted = deleted; + update = true; + } + if (update) + { + _pages.UpdatePage(page); + _syncManager.AddSyncEvent(_alias, EntityNames.Page, page.PageId, SyncEventActions.Update); + } + + // update any children + foreach (var _page in _pages.GetPages(page.SiteId).Where(item => item.ParentId == page.PageId)) + { + UpdatePage(_page, oldPath, newPath, deleted); + } + } + private List GetPermissionsDifferences(List permissions1, List permissions2) { var differences = new List(); diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index 6d146758..92aef5c5 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -217,7 +217,7 @@ namespace Oqtane.Controllers // DELETE api//5?siteid=x [HttpDelete("{id}")] - [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Admin}")] + [Authorize(Policy = $"{EntityNames.User}:{PermissionNames.Write}:{RoleNames.Host}")] public async Task Delete(int id, string siteid) { User user = _users.GetUser(id, false); diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index 0a7b1094..0e58e4b6 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -524,11 +524,6 @@ namespace Oqtane.Extensions // manage user if (user != null) { - // update user - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); - _users.UpdateUser(user); - // manage roles var _userRoles = httpContext.RequestServices.GetRequiredService(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); @@ -588,64 +583,78 @@ namespace Oqtane.Extensions } } - // create claims identity - identityuser = await _identityUserManager.FindByNameAsync(user.Username); - user.SecurityStamp = identityuser.SecurityStamp; - identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); - identity.Label = ExternalLoginStatus.Success; - - // user profile claims - if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) + var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered); + if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate)) { - var _settings = httpContext.RequestServices.GetRequiredService(); - var _profiles = httpContext.RequestServices.GetRequiredService(); - var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); - foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) + // update user + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString(); + _users.UpdateUser(user); + + // create claims identity + identityuser = await _identityUserManager.FindByNameAsync(user.Username); + user.SecurityStamp = identityuser.SecurityStamp; + identity = UserSecurity.CreateClaimsIdentity(alias, user, userRoles); + identity.Label = ExternalLoginStatus.Success; + + // user profile claims + if (!string.IsNullOrEmpty(httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", ""))) { - if (mapping.Contains(":")) + var _settings = httpContext.RequestServices.GetRequiredService(); + var _profiles = httpContext.RequestServices.GetRequiredService(); + var profiles = _profiles.GetProfiles(alias.SiteId).ToList(); + foreach (var mapping in httpContext.GetSiteSettings().GetValue("ExternalLogin:ProfileClaimTypes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) { - var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); - if (claim != null) + if (mapping.Contains(":")) { - var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); - if (profile != null) + var claim = claimsPrincipal.Claims.FirstOrDefault(item => item.Type == mapping.Split(":")[0]); + if (claim != null) { - if (!string.IsNullOrEmpty(claim.Value)) + var profile = profiles.FirstOrDefault(item => item.Name == mapping.Split(":")[1]); + if (profile != null) { - var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); - if (setting != null) + if (!string.IsNullOrEmpty(claim.Value)) { - setting.SettingValue = claim.Value; - _settings.UpdateSetting(setting); - } - else - { - setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; - _settings.AddSetting(setting); + var setting = _settings.GetSetting(EntityNames.User, user.UserId, profile.Name); + if (setting != null) + { + setting.SettingValue = claim.Value; + _settings.UpdateSetting(setting); + } + else + { + setting = new Setting { EntityName = EntityNames.User, EntityId = user.UserId, SettingName = profile.Name, SettingValue = claim.Value, IsPrivate = profile.IsPrivate }; + _settings.AddSetting(setting); + } } } + else + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile {ProfileName} Does Not Exist For The Site. Please Verify Your User Profile Definitions.", mapping.Split(":")[1]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); } } else { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim {ClaimType} Does Not Exist. Please Use The Review Claims Feature To View The Claims Returned By Your Provider.", mapping.Split(":")[0]); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); } } - else - { - _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The User Profile Claim Mapping {Mapping} Is Not Specified Correctly. It Should Be In The Format 'ClaimType:ProfileName'.", mapping); - } } + + var _syncManager = httpContext.RequestServices.GetRequiredService(); + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); + } + else + { + identity.Label = ExternalLoginStatus.AccessDenied; + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External User Login Denied For {Username}. User Account Is Deleted Or Not An Active Member Of Site {SiteId}.", user.Username, user.SiteId); } - - var _syncManager = httpContext.RequestServices.GetRequiredService(); - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} From IP Address {IPAddress} Using Provider {Provider}", user.Username, httpContext.Connection.RemoteIpAddress.ToString(), providerName); } } else // claims invalid diff --git a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs index d6f41e91..09b1f30a 100644 --- a/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs +++ b/Oqtane.Server/Infrastructure/SiteTemplates/DefaultSiteTemplate.cs @@ -67,7 +67,7 @@ namespace Oqtane.SiteTemplates new Permission(PermissionNames.View, RoleNames.Admin, true), new Permission(PermissionNames.Edit, RoleNames.Admin, true) }, - Content = "

Copyright (c) 2018-2024 .NET Foundation

" + + Content = "

Copyright (c) 2018-2025 .NET Foundation

" + "

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

" + "

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

" + "

THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

" diff --git a/Oqtane.Server/Managers/UserManager.cs b/Oqtane.Server/Managers/UserManager.cs index c1a91f5d..694bfd03 100644 --- a/Oqtane.Server/Managers/UserManager.cs +++ b/Oqtane.Server/Managers/UserManager.cs @@ -363,28 +363,36 @@ namespace Oqtane.Managers } else { - user = _users.GetUser(identityuser.UserName); - if (user != null) + if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) { - if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) + user = GetUser(identityuser.UserName, alias.SiteId); + if (user != null) { - user.IsAuthenticated = true; - user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = LastIPAddress; - _users.UpdateUser(user); - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); - - _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); - - if (setCookie) + // ensure user is registered for site + if (user.Roles.Contains(RoleNames.Registered)) { - await _identitySignInManager.SignInAsync(identityuser, isPersistent); + user.IsAuthenticated = true; + user.LastLoginOn = DateTime.UtcNow; + user.LastIPAddress = LastIPAddress; + _users.UpdateUser(user); + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Login Successful For {Username} From IP Address {IPAddress}", user.Username, LastIPAddress); + + _syncManager.AddSyncEvent(alias, EntityNames.User, user.UserId, "Login"); + + if (setCookie) + { + await _identitySignInManager.SignInAsync(identityuser, isPersistent); + } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {Username} Is Not An Active Member Of Site {SiteId}", user.Username, alias.SiteId); } } - else - { - _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); - } + } + else + { + _logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); } } } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index a9060e71..1ed427f7 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -57,7 +57,7 @@ - + diff --git a/Oqtane.Server/Pages/Files.cshtml.cs b/Oqtane.Server/Pages/Files.cshtml.cs index 463534fc..8af671ae 100644 --- a/Oqtane.Server/Pages/Files.cshtml.cs +++ b/Oqtane.Server/Pages/Files.cshtml.cs @@ -253,12 +253,12 @@ namespace Oqtane.Pages if (download) { _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); - return PhysicalFile(filepath, file.GetMimeType(), downloadName); + return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName), downloadName); } else { HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); - return PhysicalFile(filepath, file.GetMimeType()); + return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName)); } } diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index 4e04bda7..6766e74d 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -12,9 +12,9 @@ export function onUpdate() { let key = getKey(script); if (enhancedNavigation) { - // reload the script if data-reload is "always" or if the script has not been loaded previously and data-reload is "once" or "true" + // reload the script if data-reload is "always" or "true"... or if the script has not been loaded previously and data-reload is "once" let dataReload = script.getAttribute('data-reload'); - if (dataReload === 'always' || (!scriptKeys.has(key) && (dataReload == 'once' || dataReload === 'true'))) { + if ((dataReload === 'always' || dataReload === 'true') || (!scriptKeys.has(key) && dataReload == 'once')) { reloadScript(script); } } @@ -43,17 +43,13 @@ function reloadScript(script) { replaceScript(script); } } catch (error) { - if (script.src) { - console.error(`Script Reload failed to load external script: ${script.src}`, error); - } else { - console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error); - } + console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error); } } function isValid(script) { if (script.innerHTML.includes('document.write(')) { - console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`); + console.log(`Blazor Script Reload does not support scripts using document.write(): ${script.innerHTML}`); return false; } return true;