Merge pull request #5317 from sbwalker/dev

Fix #4789 - allow user email verification to be managed by administrator
This commit is contained in:
Shaun Walker 2025-05-16 11:13:20 -04:00 committed by GitHub
commit 4d5780c192
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 63 additions and 26 deletions

View File

@ -144,7 +144,7 @@ else
user = await UserService.VerifyEmailAsync(user, PageState.QueryString["token"]);
if (user != null)
{
await logger.LogInformation(LogFunction.Security, "Email Verified For For Username {Username}", _username);
await logger.LogInformation(LogFunction.Security, "Email Verified For Username {Username}", _username);
AddModuleMessage(Localizer["Success.Account.Verified"], MessageType.Info);
}
else

View File

@ -18,13 +18,13 @@
<ModuleMessage Message="@_passwordrequirements" Type="MessageType.Info" />
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username"></Label>
<Label Class="col-sm-3" For="username" HelpText="The unique username for a user. Note that this field can not be modified." ResourceKey="Username">Username:</Label>
<div class="col-sm-9">
<input id="username" class="form-control" @bind="@_username" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password"></Label>
<Label Class="col-sm-3" For="password" HelpText="The user's password. Please choose a password which is sufficiently secure." ResourceKey="Password">Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="password" type="@_passwordtype" class="form-control" @bind="@_password" autocomplete="new-password" />
@ -33,7 +33,7 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm"></Label>
<Label Class="col-sm-3" For="confirm" HelpText="Please enter the password again to confirm it matches with the value above" ResourceKey="Confirm">Confirm Password:</Label>
<div class="col-sm-9">
<div class="input-group">
<input id="confirm" type="@_passwordtype" class="form-control" @bind="@_confirm" autocomplete="new-password" />
@ -42,13 +42,22 @@
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email"></Label>
<Label Class="col-sm-3" For="email" HelpText="The email address where the user will receive notifications" ResourceKey="Email">Email:</Label>
<div class="col-sm-9">
<input id="email" class="form-control" @bind="@_email" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName"></Label>
<Label Class="col-sm-3" For="confirmed" HelpText="Indicates if the user's email is verified" ResourceKey="Confirmed">Confirmed?</Label>
<div class="col-sm-9">
<select id="confirmed" class="form-select" @bind="@_confirmed">
<option value="True">@SharedLocalizer["Yes"]</option>
<option value="False">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="displayname" HelpText="The full name of the user" ResourceKey="DisplayName">Full Name:</Label>
<div class="col-sm-9">
<input id="displayname" class="form-control" @bind="@_displayname" />
</div>
@ -68,7 +77,7 @@
@if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))
{
<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">Deleted?</Label>
<div class="col-sm-9">
<select id="isdeleted" class="form-select" @bind="@_isdeleted">
<option value="True">@SharedLocalizer["Yes"]</option>
@ -78,13 +87,13 @@
</div>
}
<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">Last Login:</Label>
<div class="col-sm-9">
<input id="lastlogin" class="form-control" @bind="@_lastlogin" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress"></Label>
<Label Class="col-sm-3" For="lastipaddress" HelpText="The IP Address of the user recorded during their last login" ResourceKey="LastIPAddress">Last IP Address:</Label>
<div class="col-sm-9">
<input id="lastipaddress" class="form-control" @bind="@_lastipaddress" readonly />
</div>
@ -167,6 +176,7 @@
private string _togglepassword = string.Empty;
private string _confirm = string.Empty;
private string _email = string.Empty;
private string _confirmed = string.Empty;
private string _displayname = string.Empty;
private string _timezoneid = string.Empty;
private string _isdeleted;
@ -204,6 +214,7 @@
{
_username = user.Username;
_email = user.Email;
_confirmed = user.EmailConfirmed.ToString();
_displayname = user.DisplayName;
_timezoneid = PageState.User.TimeZoneId;
_isdeleted = user.IsDeleted.ToString();
@ -255,6 +266,7 @@
user.Username = _username;
user.Password = _password;
user.Email = _email;
user.EmailConfirmed = bool.Parse(_confirmed);
user.DisplayName = string.IsNullOrWhiteSpace(_displayname) ? _username : _displayname;
user.TimeZoneId = _timezoneid;
if (UserSecurity.IsAuthorized(PageState.User, RoleNames.Host))

View File

@ -121,10 +121,10 @@
<value>Forgot Password</value>
</data>
<data name="Success.Account.Verified" xml:space="preserve">
<value>User Account Verified Successfully. You Can Now Login With Your Username And Password Below.</value>
<value>User Account Email Address Verified Successfully. You Can Now Login With Your Username And Password.</value>
</data>
<data name="Message.Account.NotVerified" xml:space="preserve">
<value>User Account Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
<value>User Account Email Address Could Not Be Verified. Please Contact Your Administrator For Further Instructions.</value>
</data>
<data name="Success.Account.Linked" xml:space="preserve">
<value>User Account Linked Successfully. You Can Now Login With Your External Login Below.</value>

View File

@ -216,4 +216,10 @@
<data name="TimeZone.HelpText" xml:space="preserve">
<value>The user's time zone</value>
</data>
<data name="Confirmed.Text" xml:space="preserve">
<value>Confirmed?</value>
</data>
<data name="Confirmed.HelpText" xml:space="preserve">
<value>Indicates if the user's email is verified</value>
</data>
</root>

View File

@ -140,6 +140,7 @@ namespace Oqtane.Controllers
filtered.LastLoginOn = user.LastLoginOn;
filtered.LastIPAddress = user.LastIPAddress;
filtered.TwoFactorRequired = user.TwoFactorRequired;
filtered.EmailConfirmed = user.EmailConfirmed;
filtered.Roles = user.Roles;
filtered.CreatedBy = user.CreatedBy;
filtered.CreatedOn = user.CreatedOn;
@ -200,10 +201,15 @@ namespace Oqtane.Controllers
[Authorize]
public async Task<User> Put(int id, [FromBody] User user)
{
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && _users.GetUser(user.UserId, false) != null
var existing = _userManager.GetUser(user.UserId, user.SiteId);
if (ModelState.IsValid && user.SiteId == _tenantManager.GetAlias().SiteId && user.UserId == id && existing != null
&& (_userPermissions.IsAuthorized(User, user.SiteId, EntityNames.User, -1, PermissionNames.Write, RoleNames.Admin) || User.Identity.Name == user.Username))
{
user.EmailConfirmed = User.IsInRole(RoleNames.Admin);
// only administrators can update the email confirmation
if (!User.IsInRole(RoleNames.Admin))
{
user.EmailConfirmed = existing.EmailConfirmed;
}
user = await _userManager.UpdateUser(user);
}
else

View File

@ -65,7 +65,12 @@ namespace Oqtane.Managers
{
user.SiteId = siteid;
user.Roles = GetUserRoles(user.UserId, user.SiteId);
user.SecurityStamp = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult()?.SecurityStamp;
var identityuser = _identityUserManager.FindByNameAsync(user.Username).GetAwaiter().GetResult();
if (identityuser != null)
{
user.SecurityStamp = identityuser.SecurityStamp;
user.EmailConfirmed = identityuser.EmailConfirmed;
}
user.Settings = _settings.GetSettings(EntityNames.User, user.UserId)
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
}
@ -245,23 +250,31 @@ namespace Oqtane.Managers
{
identityuser.Email = user.Email;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
}
// if email address changed and it is not confirmed, verification is required for new email address
if (!user.EmailConfirmed)
if (user.EmailConfirmed)
{
if (!identityuser.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
string body = "Dear " + user.DisplayName + ",\n\nThe Email Address For Your User Account Has Been Verified. You Can Now Login With Your Username And Password.";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
else
{
identityuser.EmailConfirmed = false;
await _identityUserManager.UpdateAsync(identityuser); // security stamp not updated
string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
string url = alias.Protocol + alias.Name + "/login?name=" + user.Username + "&token=" + WebUtility.UrlEncode(token);
string body = "Dear " + user.DisplayName + ",\n\nIn Order To Verify The Email Address Associated To Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
var notification = new Notification(user.SiteId, user, "User Account Verification", body);
_notifications.AddNotification(notification);
}
}
if (user.EmailConfirmed)
{
var emailConfirmationToken = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
await _identityUserManager.ConfirmEmailAsync(identityuser, emailConfirmationToken);
}
user = _users.UpdateUser(user);
_syncManager.AddSyncEvent(_tenantManager.GetAlias(), EntityNames.User, user.UserId, SyncEventActions.Update);