Merge remote-tracking branch 'upstream/dev' into ActionDialogSize

This commit is contained in:
Leigh Pointer 2025-01-22 07:36:01 +01:00
commit f72438996d
24 changed files with 387 additions and 236 deletions

View File

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

View File

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

View File

@ -26,14 +26,14 @@ else
<Header> <Header>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th style="width: 1px;">&nbsp;</th> <th style="width: 1px;">&nbsp;</th>
<th>@SharedLocalizer["Name"]</th> <th>@SharedLocalizer["Path"]</th>
<th>@Localizer["DeletedBy"]</th> <th>@Localizer["DeletedBy"]</th>
<th>@Localizer["DeletedOn"]</th> <th>@Localizer["DeletedOn"]</th>
</Header> </Header>
<Row> <Row>
<td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td> <td><button type="button" @onclick="@(() => RestorePage(context))" class="btn btn-success" title="Restore">@Localizer["Restore"]</button></td>
<td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td> <td><ActionDialog Header="Delete Page" Message="@string.Format(Localizer["Confirm.Page.Delete"], context.Name)" Action="Delete" Security="SecurityAccessLevel.Admin" Class="btn btn-danger" OnClick="@(async () => await DeletePage(context))" ResourceKey="DeletePage" /></td>
<td>@context.Name</td> <td>@context.Path</td>
<td>@context.DeletedBy</td> <td>@context.DeletedBy</td>
<td>@context.DeletedOn</td> <td>@context.DeletedOn</td>
</Row> </Row>
@ -104,13 +104,26 @@ else
private async Task RestorePage(Page page) private async Task RestorePage(Page page)
{ {
try try
{
var validated = true;
if (page.ParentId != null)
{
var parent = _pages.Find(item => item.PageId == page.ParentId);
validated = !parent.IsDeleted;
}
if (validated)
{ {
page.IsDeleted = false; page.IsDeleted = false;
await PageService.UpdatePageAsync(page); await PageService.UpdatePageAsync(page);
await logger.LogInformation("Page Restored {Page}", page); await logger.LogInformation("Page Restored {Page}", page);
AddModuleMessage(Localizer["Success.Page.Restore"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl()); }
else
{
AddModuleMessage(Localizer["Message.Page.Restore"], MessageType.Warning);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -125,9 +138,9 @@ else
{ {
await PageService.DeletePageAsync(page.PageId); await PageService.DeletePageAsync(page.PageId);
await logger.LogInformation("Page Permanently Deleted {Page}", page); await logger.LogInformation("Page Permanently Deleted {Page}", page);
AddModuleMessage(Localizer["Success.Page.Delete"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -148,10 +161,10 @@ else
} }
await logger.LogInformation("Pages Permanently Deleted"); await logger.LogInformation("Pages Permanently Deleted");
AddModuleMessage(Localizer["Success.Pages.Delete"], MessageType.Success);
await Load(); await Load();
HideProgressIndicator(); HideProgressIndicator();
StateHasChanged(); StateHasChanged();
NavigationManager.NavigateTo(NavigateUrl());
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -169,6 +182,7 @@ else
pagemodule.IsDeleted = false; pagemodule.IsDeleted = false;
await PageModuleService.UpdatePageModuleAsync(pagemodule); await PageModuleService.UpdatePageModuleAsync(pagemodule);
await logger.LogInformation("Module Restored {Module}", module); await logger.LogInformation("Module Restored {Module}", module);
AddModuleMessage(Localizer["Success.Module.Restore"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
} }
@ -185,6 +199,7 @@ else
{ {
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
await logger.LogInformation("Module Permanently Deleted {Module}", module); await logger.LogInformation("Module Permanently Deleted {Module}", module);
AddModuleMessage(Localizer["Success.Module.Delete"], MessageType.Success);
await Load(); await Load();
StateHasChanged(); StateHasChanged();
} }
@ -205,6 +220,7 @@ else
await PageModuleService.DeletePageModuleAsync(module.PageModuleId); await PageModuleService.DeletePageModuleAsync(module.PageModuleId);
} }
await logger.LogInformation("Modules Permanently Deleted"); await logger.LogInformation("Modules Permanently Deleted");
AddModuleMessage(Localizer["Success.Modules.Delete"], MessageType.Success);
await Load(); await Load();
HideProgressIndicator(); HideProgressIndicator();
StateHasChanged(); StateHasChanged();

View File

@ -58,7 +58,7 @@ else
<td>@context.EffectiveDate</td> <td>@context.EffectiveDate</td>
<td>@context.ExpiryDate</td> <td>@context.ExpiryDate</td>
<td> <td>
<ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove User" Message="@string.Format(Localizer["Confirm.User.DeleteRole"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.User.Username == UserNames.Host || context.User.UserId == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -179,15 +179,21 @@ else
} }
private async Task DeleteUserRole(int UserRoleId) 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 UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success); AddModuleMessage(Localizer["Confirm.User.RoleRemoved"], MessageType.Success);
await GetUserRoles(); await GetUserRoles();
StateHasChanged(); StateHasChanged();
@ -198,9 +204,4 @@ else
AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error); AddModuleMessage(Localizer["Error.User.RemoveRole"], MessageType.Error);
} }
} }
else
{
AddModuleMessage(SharedLocalizer["Message.InfoRequired"], MessageType.Warning);
}
}
} }

View File

@ -51,6 +51,8 @@
<input id="displayname" class="form-control" @bind="@displayname" /> <input id="displayname" class="form-control" @bind="@displayname" />
</div> </div>
</div> </div>
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label> <Label Class="col-sm-3" For="isdeleted" HelpText="Indicate if the user is active" ResourceKey="IsDeleted"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -60,6 +62,7 @@
</select> </select>
</div> </div>
</div> </div>
}
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label> <Label Class="col-sm-3" For="lastlogin" HelpText="The date and time when the user last signed in" ResourceKey="LastLogin"></Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -127,8 +130,11 @@
<button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button> <button type="button" class="btn btn-success" @onclick="SaveUser">@SharedLocalizer["Save"]</button>
<NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink> <NavLink class="btn btn-secondary" href="@NavigateUrl()">@SharedLocalizer["Cancel"]</NavLink>
<br /> @if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host) && isdeleted == "True")
<br /> {
<ActionDialog Header="Delete User" Message="Are You Sure You Wish To Permanently Delete This User?" Action="Delete" Security="SecurityAccessLevel.Host" Class="btn btn-danger" OnClick="@(async () => await DeleteUser())" ResourceKey="DeleteUser" />
}
<br /><br />
<AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo> <AuditInfo CreatedBy="@createdby" CreatedOn="@createdon" ModifiedBy="@modifiedby" ModifiedOn="@modifiedon" DeletedBy="@deletedby" DeletedOn="@deletedon"></AuditInfo>
} }
@ -226,8 +232,10 @@
user.Password = _password; user.Password = _password;
user.Email = email; user.Email = email;
user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname; user.DisplayName = string.IsNullOrWhiteSpace(displayname) ? username : displayname;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted)); user.IsDeleted = (isdeleted == null ? true : Boolean.Parse(isdeleted));
}
user = await UserService.UpdateUserAsync(user); user = await UserService.UpdateUserAsync(user);
if (user != null) if (user != null)
@ -259,6 +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() private bool ValidateProfiles()
{ {
foreach (Profile profile in profiles) foreach (Profile profile in profiles)

View File

@ -35,7 +35,7 @@ else
<ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" /> <ActionLink Action="Edit" Text="Edit" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="EditUser" />
</td> </td>
<td> <td>
<ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId)" ResourceKey="DeleteUser" /> <ActionDialog Header="Delete User" Message="@string.Format(Localizer["Confirm.User.Delete"], context.User.DisplayName)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUser(context))" Disabled="@(context.UserId == PageState.User.UserId || context.User.IsDeleted)" ResourceKey="DeleteUser" />
</td> </td>
<td> <td>
<ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" /> <ActionLink Action="Roles" Text="Roles" Parameters="@($"id=" + context.UserId.ToString())" Security="SecurityAccessLevel.Edit" ResourceKey="Roles" />
@ -610,20 +610,32 @@ else
private async Task DeleteUser(UserRole UserRole) private async Task DeleteUser(UserRole UserRole)
{ {
try try
{
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{ {
var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId); var user = await UserService.GetUserAsync(UserRole.UserId, PageState.Site.SiteId);
if (user != null) if (user != null)
{ {
await UserService.DeleteUserAsync(user.UserId, PageState.Site.SiteId); user.IsDeleted = true;
await logger.LogInformation("User Deleted {User}", UserRole.User); 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); await LoadUsersAsync(true);
StateHasChanged(); StateHasChanged();
} }
}
catch (Exception ex) catch (Exception ex)
{ {
await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message); await logger.LogError(ex, "Error Deleting User {User} {Error}", UserRole.User, ex.Message);
AddModuleMessage(ex.Message, MessageType.Error); AddModuleMessage(Localizer["Error.DeleteUser"], MessageType.Error);
} }
} }

View File

@ -63,7 +63,7 @@ else
<td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td> <td>@Utilities.UtcAsLocalDate(context.EffectiveDate)</td>
<td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td> <td>@Utilities.UtcAsLocalDate(context.ExpiryDate)</td>
<td> <td>
<ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.IsAutoAssigned || (context.Role.Name == RoleNames.Host && userid == PageState.User.UserId))" ResourceKey="DeleteUserRole" /> <ActionDialog Header="Remove Role" Message="@string.Format(Localizer["Confirm.User.RemoveRole"], context.Role.Name)" Action="Delete" Security="SecurityAccessLevel.Edit" Class="btn btn-danger" OnClick="@(async () => await DeleteUserRole(context.UserRoleId))" Disabled="@(context.Role.Name == RoleNames.Host && userid == PageState.User.UserId)" ResourceKey="DeleteUserRole" />
</td> </td>
</Row> </Row>
</Pager> </Pager>
@ -170,9 +170,19 @@ else
private async Task DeleteUserRole(int UserRoleId) private async Task DeleteUserRole(int UserRoleId)
{ {
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 UserRoleService.DeleteUserRoleAsync(UserRoleId);
await logger.LogInformation("User Removed From Role {UserRoleId}", UserRoleId); await logger.LogInformation("User {Username} Removed From Role {Role}", userrole.User.Username, userrole.Role.Name);
}
AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success); AddModuleMessage(Localizer["Success.User.Remove"], MessageType.Success);
await GetUserRoles(); await GetUserRoles();
StateHasChanged(); StateHasChanged();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -174,7 +174,7 @@
// get jwt token for downstream APIs // get jwt token for downstream APIs
if (Context.User.Identity.IsAuthenticated) if (Context.User.Identity.IsAuthenticated)
{ {
CreateJwtToken(alias); GetJwtToken(alias);
} }
// includes resources // includes resources
@ -441,8 +441,17 @@
} }
} }
private void CreateJwtToken(Alias alias) private void GetJwtToken(Alias alias)
{ {
_authorizationToken = Context.Request.Headers[HeaderNames.Authorization];
if (!string.IsNullOrEmpty(_authorizationToken))
{
// 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 sitesettings = Context.GetSiteSettings();
var secret = sitesettings.GetValue("JwtOptions:Secret", ""); var secret = sitesettings.GetValue("JwtOptions:Secret", "");
if (!string.IsNullOrEmpty(secret)) if (!string.IsNullOrEmpty(secret))
@ -450,6 +459,7 @@
_authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20"))); _authorizationToken = JwtManager.GenerateToken(alias, (ClaimsIdentity)Context.User.Identity, secret, sitesettings.GetValue("JwtOptions:Issuer", ""), sitesettings.GetValue("JwtOptions:Audience", ""), int.Parse(sitesettings.GetValue("JwtOptions:Lifetime", "20")));
} }
} }
}
private string CreatePWAScript(Alias alias, Site site, Route route) private string CreatePWAScript(Alias alias, Site site, Route route)
{ {

View File

@ -60,9 +60,9 @@ namespace Oqtane.Controllers
{ {
installation = _databaseManager.Install(config); installation = _databaseManager.Install(config);
if (installation.Success && config.Register) if (installation.Success)
{ {
await RegisterContact(config.HostEmail); await RegisterContact(config.HostEmail, config.HostName, config.Register);
} }
} }
else else
@ -257,7 +257,7 @@ namespace Oqtane.Controllers
} }
} }
private async Task RegisterContact(string email) private async Task RegisterContact(string email, string name, bool register)
{ {
try try
{ {
@ -268,7 +268,7 @@ namespace Oqtane.Controllers
{ {
client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value); client.DefaultRequestHeaders.Add("Referer", HttpContext.Request.Scheme + "://" + HttpContext.Request.Host.Value);
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version));
var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}")).ConfigureAwait(false); var response = await client.GetAsync(new Uri(url + $"/api/registry/contact/?id={_configManager.GetInstallationId()}&email={WebUtility.UrlEncode(email)}&name={WebUtility.UrlEncode(name)}&register={register.ToString().ToLower()}")).ConfigureAwait(false);
} }
} }
} }
@ -278,14 +278,6 @@ namespace Oqtane.Controllers
} }
} }
// GET api/<controller>/register?email=x
[HttpPost("register")]
[Authorize(Roles = RoleNames.Host)]
public async Task Register(string email)
{
await RegisterContact(email);
}
public struct ClientAssembly public struct ClientAssembly
{ {
public ClientAssembly(string filepath, bool hashfilename) public ClientAssembly(string filepath, bool hashfilename)

View File

@ -9,6 +9,8 @@ using System.Net;
using Oqtane.Enums; using Oqtane.Enums;
using Oqtane.Infrastructure; using Oqtane.Infrastructure;
using Oqtane.Repository; using Oqtane.Repository;
using System.Xml.Linq;
using Microsoft.AspNetCore.Diagnostics;
namespace Oqtane.Controllers namespace Oqtane.Controllers
{ {
@ -279,18 +281,14 @@ namespace Oqtane.Controllers
// get current page permissions // get current page permissions
var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList(); var currentPermissions = _permissionRepository.GetPermissions(page.SiteId, EntityNames.Page, page.PageId).ToList();
page = _pages.UpdatePage(page); // preserve new path and deleted status
var newPath = page.Path;
var deleted = page.IsDeleted;
page.Path = currentPage.Path;
page.IsDeleted = currentPage.IsDeleted;
// save url mapping if page path changed // update page
if (currentPage.Path != page.Path) UpdatePage(page, page.Path, newPath, deleted);
{
var urlMapping = _urlMappings.GetUrlMapping(page.SiteId, currentPage.Path);
if (urlMapping != null)
{
urlMapping.MappedUrl = page.Path;
_urlMappings.UpdateUrlMapping(urlMapping);
}
}
// get differences between current and new page permissions // get differences between current and new page permissions
var added = GetPermissionsDifferences(page.PermissionList, currentPermissions); var added = GetPermissionsDifferences(page.PermissionList, currentPermissions);
@ -320,6 +318,7 @@ namespace Oqtane.Controllers
}); });
} }
} }
// permissions removed // permissions removed
foreach (Permission permission in 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); _syncManager.AddSyncEvent(_alias, EntityNames.Site, page.SiteId, SyncEventActions.Refresh);
// personalized page // personalized page
@ -378,6 +376,39 @@ namespace Oqtane.Controllers
return page; 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<Permission> GetPermissionsDifferences(List<Permission> permissions1, List<Permission> permissions2) private List<Permission> GetPermissionsDifferences(List<Permission> permissions1, List<Permission> permissions2)
{ {
var differences = new List<Permission>(); var differences = new List<Permission>();

View File

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

View File

@ -524,11 +524,6 @@ namespace Oqtane.Extensions
// manage user // manage user
if (user != null) if (user != null)
{ {
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
// manage roles // manage roles
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>(); var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); var userRoles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
@ -588,6 +583,14 @@ namespace Oqtane.Extensions
} }
} }
var userrole = userRoles.FirstOrDefault(item => item.Role.Name == RoleNames.Registered);
if (!user.IsDeleted && userrole != null && Utilities.IsEffectiveAndNotExpired(userrole.EffectiveDate, userrole.ExpiryDate))
{
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
// create claims identity // create claims identity
identityuser = await _identityUserManager.FindByNameAsync(user.Username); identityuser = await _identityUserManager.FindByNameAsync(user.Username);
user.SecurityStamp = identityuser.SecurityStamp; user.SecurityStamp = identityuser.SecurityStamp;
@ -647,6 +650,12 @@ namespace Oqtane.Extensions
_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); _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);
}
}
} }
else // claims invalid else // claims invalid
{ {

View File

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

View File

@ -363,10 +363,13 @@ namespace Oqtane.Managers
} }
else else
{ {
user = _users.GetUser(identityuser.UserName); if (await _identityUserManager.IsEmailConfirmedAsync(identityuser))
{
user = GetUser(identityuser.UserName, alias.SiteId);
if (user != null) if (user != null)
{ {
if (await _identityUserManager.IsEmailConfirmedAsync(identityuser)) // ensure user is registered for site
if (user.Roles.Contains(RoleNames.Registered))
{ {
user.IsAuthenticated = true; user.IsAuthenticated = true;
user.LastLoginOn = DateTime.UtcNow; user.LastLoginOn = DateTime.UtcNow;
@ -383,10 +386,15 @@ namespace Oqtane.Managers
} }
else else
{ {
_logger.Log(LogLevel.Information, this, LogFunction.Security, "User Email Address Not Verified {Username}", user.Username); _logger.Log(LogLevel.Information, this, LogFunction.Security, "User {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 else
{ {

View File

@ -57,7 +57,7 @@
<ItemGroup> <ItemGroup>
<ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" /> <ModuleTemplateFiles Include="$(ProjectDir)wwwroot\Modules\Templates\**\*.*" />
<ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" /> <ThemeTemplateFiles Include="$(ProjectDir)wwwroot\Themes\Templates\**\*.*" />
<MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)MySql.EntityFrameworkCore.dll;$(OutputPath)MySql.Data.dll" /> <MySQLFiles Include="$(OutputPath)Oqtane.Database.MySQL.dll;$(OutputPath)Oqtane.Database.MySQL.pdb;$(OutputPath)Pomelo.EntityFrameworkCore.MySql.dll;$(OutputPath)MySql.Data.dll" />
<PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" /> <PostgreSQLFiles Include="$(OutputPath)Oqtane.Database.PostgreSQL.dll;$(OutputPath)Oqtane.Database.PostgreSQL.pdb;$(OutputPath)EFCore.NamingConventions.dll;$(OutputPath)Npgsql.EntityFrameworkCore.PostgreSQL.dll;$(OutputPath)Npgsql.dll" />
<SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" /> <SqliteFiles Include="$(OutputPath)Oqtane.Database.Sqlite.dll;$(OutputPath)Oqtane.Database.Sqlite.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.Sqlite.dll" />
<SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" /> <SqlServerFiles Include="$(OutputPath)Oqtane.Database.SqlServer.dll;$(OutputPath)Oqtane.Database.SqlServer.pdb;$(OutputPath)Microsoft.EntityFrameworkCore.SqlServer.dll" />

View File

@ -253,12 +253,12 @@ namespace Oqtane.Pages
if (download) if (download)
{ {
_syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download"); _syncManager.AddSyncEvent(_alias, EntityNames.File, file.FileId, "Download");
return PhysicalFile(filepath, file.GetMimeType(), downloadName); return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName), downloadName);
} }
else else
{ {
HttpContext.Response.Headers.Append(HeaderNames.ETag, etag); HttpContext.Response.Headers.Append(HeaderNames.ETag, etag);
return PhysicalFile(filepath, file.GetMimeType()); return PhysicalFile(filepath, MimeUtilities.GetMimeType(downloadName));
} }
} }

View File

@ -12,9 +12,9 @@ export function onUpdate() {
let key = getKey(script); let key = getKey(script);
if (enhancedNavigation) { 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'); 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); reloadScript(script);
} }
} }
@ -43,17 +43,13 @@ function reloadScript(script) {
replaceScript(script); replaceScript(script);
} }
} catch (error) { } catch (error) {
if (script.src) { console.error(`Blazor Script Reload failed to load script: ${getKey(script)}`, error);
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);
}
} }
} }
function isValid(script) { function isValid(script) {
if (script.innerHTML.includes('document.write(')) { 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 false;
} }
return true; return true;