mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 06:24:24 +01:00
Onboarding: Invite new users (#5714)
* Server Users: More precise message when inviting users This lets the admin who invited a new user know whether or not an email has been sent. If the SMTP server hasn't been set up, they need to share the invite link with the user. * Onboarding: Invite new users - Separates the user self-registration and invite cases - Adds invitation email for users created by the admin - Adds invitation tokens to verify user was invited - Adds handler action for invite links - Refactors `UserEventHostedService` * Remove duplicate status message from views that use the wizard layout * Auto-approve users created by an admin * Notify admins via email if a new account requires approval * Update wording * Fix update user error * Fix redirect to email confirmation in invite action * Fix precondition checks after signup * Improve admin notification Send notification only if the user does not require email confirmation or when they confirmed their email address. Rationale: We want to inform admins only about qualified users and not annoy them with bot registrations. * Allow approval alongside resending confirm email * Use user email in log messages instead of ID * Prevent unnecessary notification after email confirmation * Use ApplicationUser type explicitly * Fix after rebase * Refactoring: Do not subclass UserRegisteredEvent
This commit is contained in:
@@ -600,7 +600,6 @@ namespace BTCPayServer.Controllers
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
settings.FirstRun = false;
|
||||
await _SettingsRepository.UpdateSetting(settings);
|
||||
|
||||
await _SettingsRepository.FirstAdminRegistered(policies, _Options.UpdateUrl != null, _Options.DisableRegistration, Logs);
|
||||
RegisteredAdmin = true;
|
||||
}
|
||||
@@ -614,15 +613,17 @@ namespace BTCPayServer.Controllers
|
||||
RegisteredUserId = user.Id;
|
||||
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Account created.";
|
||||
if (policies.RequiresConfirmedEmail)
|
||||
var requiresConfirmedEmail = policies.RequiresConfirmedEmail && !user.EmailConfirmed;
|
||||
var requiresUserApproval = policies.RequiresUserApproval && !user.Approved;
|
||||
if (requiresConfirmedEmail)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " Please confirm your email.";
|
||||
}
|
||||
if (policies.RequiresUserApproval)
|
||||
if (requiresUserApproval)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] += " The new account requires approval by an admin before you can log in.";
|
||||
}
|
||||
if (policies.RequiresConfirmedEmail || policies.RequiresUserApproval)
|
||||
if (requiresConfirmedEmail || requiresUserApproval)
|
||||
{
|
||||
return RedirectToAction(nameof(Login));
|
||||
}
|
||||
@@ -670,25 +671,31 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
var result = await _userManager.ConfirmEmailAsync(user, code);
|
||||
if (!await _userManager.HasPasswordAsync(user))
|
||||
if (result.Succeeded)
|
||||
{
|
||||
_eventAggregator.Publish(new UserConfirmedEmailEvent
|
||||
{
|
||||
User = user,
|
||||
RequestUri = Request.GetAbsoluteRootUri()
|
||||
});
|
||||
|
||||
var hasPassword = await _userManager.HasPasswordAsync(user);
|
||||
if (hasPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
});
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your email has been confirmed but you still need to set your password."
|
||||
Message = "Your email has been confirmed. Please set your password."
|
||||
});
|
||||
return RedirectToAction("SetPassword", new { email = user.Email, code = await _userManager.GeneratePasswordResetTokenAsync(user) });
|
||||
}
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Success,
|
||||
Message = "Your email has been confirmed."
|
||||
});
|
||||
return RedirectToAction("Login", new { email = user.Email });
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
return View("Error");
|
||||
@@ -743,14 +750,20 @@ namespace BTCPayServer.Controllers
|
||||
throw new ApplicationException("A code must be supplied for password reset.");
|
||||
}
|
||||
|
||||
var user = string.IsNullOrEmpty(userId) ? null : await _userManager.FindByIdAsync(userId);
|
||||
var hasPassword = user != null && await _userManager.HasPasswordAsync(user);
|
||||
if (!string.IsNullOrEmpty(userId))
|
||||
{
|
||||
var user = await _userManager.FindByIdAsync(userId);
|
||||
email = user?.Email;
|
||||
}
|
||||
|
||||
var model = new SetPasswordViewModel { Code = code, Email = email, EmailSetInternally = !string.IsNullOrEmpty(email) };
|
||||
return View(model);
|
||||
return View(new SetPasswordViewModel
|
||||
{
|
||||
Code = code,
|
||||
Email = email,
|
||||
EmailSetInternally = !string.IsNullOrEmpty(email),
|
||||
HasPassword = hasPassword
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("/login/set-password")]
|
||||
@@ -762,6 +775,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return View(model);
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByEmailAsync(model.Email);
|
||||
if (!UserService.TryCanLogin(user, out _))
|
||||
{
|
||||
@@ -781,9 +795,64 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
AddErrors(result);
|
||||
model.HasPassword = await _userManager.HasPasswordAsync(user);
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("/invite/{userId}/{code}")]
|
||||
public async Task<IActionResult> AcceptInvite(string userId, string code)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(code))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = await _userManager.FindByInvitationTokenAsync(userId, Uri.UnescapeDataString(code));
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var requiresEmailConfirmation = user.RequiresEmailConfirmation && !user.EmailConfirmed;
|
||||
var requiresSetPassword = !await _userManager.HasPasswordAsync(user);
|
||||
|
||||
if (requiresEmailConfirmation)
|
||||
{
|
||||
return await RedirectToConfirmEmail(user);
|
||||
}
|
||||
if (requiresSetPassword)
|
||||
{
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Invitation accepted. Please set your password."
|
||||
});
|
||||
return await RedirectToSetPassword(user);
|
||||
}
|
||||
|
||||
// Inform user that a password has been set on account creation
|
||||
TempData.SetStatusMessageModel(new StatusMessageModel
|
||||
{
|
||||
Severity = StatusMessageModel.StatusSeverity.Info,
|
||||
Message = "Your password has been set by the user who invited you."
|
||||
});
|
||||
|
||||
return RedirectToAction(nameof(Login), new { email = user.Email });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToConfirmEmail(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
|
||||
return RedirectToAction(nameof(ConfirmEmail), new { userId = user.Id, code });
|
||||
}
|
||||
|
||||
private async Task<IActionResult> RedirectToSetPassword(ApplicationUser user)
|
||||
{
|
||||
var code = await _userManager.GeneratePasswordResetTokenAsync(user);
|
||||
return RedirectToAction(nameof(SetPassword), new { userId = user.Id, email = user.Email, code });
|
||||
}
|
||||
|
||||
#region Helpers
|
||||
|
||||
private void AddErrors(IdentityResult result)
|
||||
|
||||
Reference in New Issue
Block a user