Merge pull request #6659 from NicolasDorier/restore-invite

Restore behavior: Invite non existing users when adding a store user
This commit is contained in:
Nicolas Dorier
2025-04-08 23:23:54 +09:00
committed by GitHub
2 changed files with 110 additions and 51 deletions

View File

@@ -9,11 +9,14 @@ using BTCPayServer.Client;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Mails;
using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using NicolasDorier.RateLimits;
using static BTCPayServer.Services.Stores.StoreRepository;
namespace BTCPayServer.Controllers;
@@ -28,8 +31,15 @@ public partial class UIStoresController
return View(vm);
}
enum StoreUsersAction
{
Added,
Updated,
Invited
}
[HttpPost("{storeId}/users")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
[RateLimitsFilter(ZoneLimits.Register, Scope = RateLimitsScope.RemoteAddress)]
public async Task<IActionResult> StoreUsers(string storeId, StoreUsersViewModel vm)
{
await FillUsers(vm);
@@ -44,39 +54,90 @@ public partial class UIStoresController
ModelState.AddModelError(nameof(vm.Role), StringLocalizer["Invalid role"]);
return View(vm);
}
StoreUsersAction action;
string? inviteInfo = null;
var user = await _userManager.FindByEmailAsync(vm.Email);
if (user is null)
{
ModelState.AddModelError(nameof(vm.Email), StringLocalizer["This user does not exist"]);
action = StoreUsersAction.Invited;
if (!_policiesSettings.LockSubscription || await IsAdmin())
{
user = new ApplicationUser
{
UserName = vm.Email,
Email = vm.Email,
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
RequiresApproval = _policiesSettings.RequiresUserApproval,
Created = DateTimeOffset.UtcNow
};
var currentUser = await _userManager.GetUserAsync(HttpContext.User);
if (currentUser is not null &&
(await _userManager.CreateAsync(user)) is { Succeeded: true } result)
{
var invitationEmail = await _emailSenderFactory.IsComplete();
var evt = await UserEvent.Invited.Create(user!, currentUser, _callbackGenerator, Request, invitationEmail);
_eventAggregator.Publish(evt);
inviteInfo = invitationEmail
? StringLocalizer["An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>", evt.InvitationLink]
: StringLocalizer["An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>", evt.InvitationLink];
user = await _userManager.FindByEmailAsync(vm.Email);
}
}
}
else
{
action = (await _storeRepo.GetStoreUser(storeId, user.Id)) is not null
? StoreUsersAction.Updated
: StoreUsersAction.Added;
}
if (user is null)
{
ModelState.AddModelError(nameof(vm.Email), StringLocalizer["User not found"]);
return View(vm);
}
var isExistingStoreUser = await _storeRepo.GetStoreUser(storeId, user.Id) is not null;
var res = await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId);
if (res is AddOrUpdateStoreUserResult.Success)
{
var res = await _storeRepo.AddOrUpdateStoreUser(CurrentStore.Id, user.Id, roleId);
if (res is AddOrUpdateStoreUserResult.Success)
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Severity = StatusMessageModel.StatusSeverity.Success,
AllowDismiss = false,
Message = isExistingStoreUser
? StringLocalizer["The user has been updated successfully."].Value
: StringLocalizer["The user has been added successfully."].Value,
Message = action switch
{
StoreUsersAction.Added => StringLocalizer["The user has been added successfully."].Value,
StoreUsersAction.Updated => StringLocalizer["The user has been updated successfully."].Value,
StoreUsersAction.Invited => null,
_ => throw new ArgumentOutOfRangeException(action.ToString())
},
Html = action switch
{
StoreUsersAction.Invited => inviteInfo,
_ => null
}
});
return RedirectToAction(nameof(StoreUsers));
}
else
{
ModelState.AddModelError(nameof(vm.Email),
isExistingStoreUser
? StringLocalizer["The user could not be updated: {0}", res.ToString()]
: StringLocalizer["The user could not be added: {0}", res.ToString()]
);
action switch
{
StoreUsersAction.Updated => StringLocalizer["The user could not be updated: {0}", res.ToString()],
StoreUsersAction.Added => StringLocalizer["The user could not be added: {0}", res.ToString()],
StoreUsersAction.Invited => StringLocalizer["The user could not be invited: {0}", res.ToString()],
_ => throw new ArgumentOutOfRangeException(action.ToString())
});
return View(vm);
}
}
private async Task<bool> IsAdmin()
=> (await _authorizationService.AuthorizeAsync(User, null, new PolicyRequirement(Policies.CanCreateUser))).Succeeded;
[HttpPost("{storeId}/users/{userId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task<IActionResult> UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm)

View File

@@ -36,7 +36,6 @@ namespace BTCPayServer.Services
"{0} Node": "",
"{0} provider is not supported": "",
"{0} selected": "",
"{0} Settings": "",
"{0} Status": "",
"{0} Store": "",
"{0} Stores": "",
@@ -79,7 +78,6 @@ namespace BTCPayServer.Services
"Access Type": "",
"Account": "",
"Account created.": "",
"Account Index": "",
"Account key": "",
"Account Key": "",
"Account key path": "",
@@ -88,7 +86,6 @@ namespace BTCPayServer.Services
"Actions": "",
"Active": "",
"Add": "",
"Add account": "",
"Add additional fee (network fee) to invoice …": "",
"Add Address": "",
"Add an email address or an external URL where users can contact you for support requests through a \"Contact Us\" button, displayed at the bottom of the public facing pages.": "",
@@ -146,6 +143,8 @@ namespace BTCPayServer.Services
"Amount requested": "",
"An error occurred while resetting user password": "",
"An error occurred while saving: {0}": "",
"An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
"An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
"An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "",
"Animation": "",
"Any amount": "",
@@ -181,8 +180,6 @@ namespace BTCPayServer.Services
"archived": "",
"Archived": "",
"Archived Stores": "",
"At Least One": "",
"At Least Ten": "",
"Authenticator code": "",
"Authorize a public key to access Bitpay compatible Invoice API.": "",
"Authorize app": "",
@@ -200,7 +197,6 @@ namespace BTCPayServer.Services
"Available placeholders: <code>{StoreName} {ItemDescription} {OrderId}</code>": "",
"Awaiting": "",
"Azure Blob Storage": "",
"Back to list": "",
"Backend's language": "",
"Balance": "",
"Batch size": "",
@@ -232,6 +228,7 @@ namespace BTCPayServer.Services
"Buyer Email": "",
"Callback Notification URL": "",
"Campaign not active": "",
"Can create a new cold wallet": "",
"Can use hot wallet": "",
"Can use RPC import": "",
"Cancel": "",
@@ -252,6 +249,7 @@ namespace BTCPayServer.Services
"Change your {0} provider.": "",
"Change your password": "",
"Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface": "",
"Changing the role of user {0} failed: {1}": "",
"Charge": "",
"Cheat Mode: Send funds to this wallet": "",
"Check if NFC is supported and enabled on this device": "",
@@ -286,7 +284,6 @@ namespace BTCPayServer.Services
"Combine": "",
"Combine PSBT": "",
"Compatible wallets": "",
"Complete the email setup to send test emails.": "",
"Completed": "",
"CONFIDENTIAL: This QR Code is confidential, close this window as soon as you don't need it anymore.": "",
"Config file was not in the correct format": "",
@@ -301,6 +298,7 @@ namespace BTCPayServer.Services
"Confirm addresses": "",
"Confirm broadcasting this transaction": "",
"Confirm in the next …": "",
"Confirm Lightning Payout": "",
"Confirm new password": "",
"Confirm passphrase": "",
"Confirm password": "",
@@ -337,11 +335,8 @@ namespace BTCPayServer.Services
"Copy Tor URL": "",
"Core Lightning {0}": "",
"Could not access your camera. Is it already in use?": "",
"Could not create a new account.": "",
"Could not create new account.": "",
"Could not generate invoice: {0}": "",
"Could not load log files": "",
"Could not open the wallet: {0}": "",
"Could not save CSS file: {0}": "",
"Could not save image: {0}": "",
"Could not save logo: {0}": "",
@@ -350,6 +345,7 @@ namespace BTCPayServer.Services
"Create": "",
"Create {0} Hot Wallet": "",
"Create {0} Watch-Only Wallet": "",
"Create a new {0}": "",
"Create a new app": "",
"Create a new store": "",
"Create a new wallet": "",
@@ -363,9 +359,11 @@ namespace BTCPayServer.Services
"Create invoice to pay custom amount": "",
"Create New Token": "",
"Create Payment Request": "",
"Create pending transaction": "",
"Create Pull Payment": "",
"Create refund": "",
"Create Request": "",
"Create role": "",
"Create Store": "",
"Create temporary file link": "",
"Create Token": "",
@@ -383,6 +381,7 @@ namespace BTCPayServer.Services
"Currency": "",
"Currency is invalid": "",
"Currency pairs to test against your rule": "",
"Current effective fee rate": "",
"Current password": "",
"Current Rates source is": "",
"Currently active!": "",
@@ -512,7 +511,6 @@ namespace BTCPayServer.Services
"Edit pull payment": "",
"Edit Pull Payment": "",
"Editor": "",
"Either recipient or \"Send the email to the buyer\" is required": "",
"Either your {0} wallet is not configured, or it is not a hot wallet. This processor cannot function until a hot wallet is configured in your store.": "",
"Email": "",
"Email address": "",
@@ -523,8 +521,6 @@ namespace BTCPayServer.Services
"Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.": "",
"email rules": "",
"Email Rules": "",
"Create Email Rule": "",
"Edit Email Rule": "",
"Email rules allow BTCPay Server to send customized emails from your store based on events.": "",
"Email sent to {0}. Please verify you received it.": "",
"Email Server": "",
@@ -566,7 +562,6 @@ namespace BTCPayServer.Services
"Enter wallet seed": "",
"Enter your extended public key": "",
"Error": "",
"Error sending test email: {0}": "",
"Error updating profile": "",
"Error updating user": "",
"Error while broadcasting: {0}": "",
@@ -592,6 +587,7 @@ namespace BTCPayServer.Services
"Feature disabled": "",
"Featured Image URL": "",
"Fee block target": "",
"Fee bump method": "",
"Fee rate": "",
"Fee rate (sat/vB)": "",
"Fee will be shown for BTC and LTC onchain payments only.": "",
@@ -728,7 +724,6 @@ namespace BTCPayServer.Services
"Invalid destination or payment method": "",
"Invalid email": "",
"Invalid login attempt.": "",
"Invalid mailbox address provided. Valid formats are: '{0}' or '{1}'": "",
"Invalid network": "",
"Invalid passphrase confirmation": "",
"Invalid payout method": "",
@@ -794,6 +789,7 @@ namespace BTCPayServer.Services
"Lightning network settings": "",
"Lightning node (LNURL Auth)": "",
"Lightning Payout Processor": "",
"Lightning Payout Result": "",
"Lightning Services": "",
"Limit": "",
"Link": "",
@@ -864,7 +860,7 @@ namespace BTCPayServer.Services
"Network Fee": "",
"Never add network fee": "",
"New {0} plugin version {1} released!": "",
"New account label": "",
"New effective fee rate": "",
"New password": "",
"New role": "",
"New user {0} requires approval.": "",
@@ -875,7 +871,6 @@ namespace BTCPayServer.Services
"Next": "",
"NFC detected.": "",
"No access tokens yet.": "",
"No accounts available on the current wallet": "",
"No claim made yet.": "",
"No contributions allowed after the goal has been reached": "",
"No contributions have been made yet.": "",
@@ -904,11 +899,13 @@ namespace BTCPayServer.Services
"Node headers height: {0}": "",
"Node Info": "",
"Non-admins can access the User Creation API Endpoint": "",
"Non-admins can create Cold Wallets for their Store": "",
"Non-admins can create Hot Wallets for their Store": "",
"Non-admins can import Hot Wallets for their Store": "",
"Non-admins can use the Internal Lightning Node for their Store": "",
"Non-admins cannot access the User Creation API Endpoint": "",
"Non-supported state of invoice": "",
"None of the selected transaction can be fee bumped": "",
"Not all payout methods are supported": "",
"Not allowed to cancel this invoice": "",
"Not recommended": "",
@@ -1012,6 +1009,7 @@ namespace BTCPayServer.Services
"Pending Approval": "",
"Pending Email Verification": "",
"Pending Invitation": "",
"Pending Transaction": "",
"percent": "",
"Percentage must be a numeric value between 0 and 100": "",
"Permanent Url": "",
@@ -1029,6 +1027,7 @@ namespace BTCPayServer.Services
"Please note that creating a hot wallet is not supported by this instance for non administrators.": "",
"Please note that creating a wallet is not supported by your instance.": "",
"Please note that not all text is translatable, and future updates may modify existing translations or introduce new translatable phrases.": "",
"Please note that this instance does not support creating a new cold wallet for non-administrators. However, you can import one from other wallet software.": "",
"Please provide a connection string": "",
"Please provide a destination": "",
"Please provide an amount greater than 0": "",
@@ -1036,10 +1035,8 @@ namespace BTCPayServer.Services
"Please provide your extended public key": "",
"Please remove the NFC from the card reader": "",
"Please select an option before proceeding": "",
"Please select the view-only wallet file": "",
"Please select the view-only wallet keys file": "",
"Please select the wallet file": "",
"Please select the wallet.keys file": "",
"Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "",
"Please wait for your node to be synched": "",
"Plugin action cancelled.": "",
"Plugin scheduled to be installed.": "",
"Plugin scheduled to be uninstalled.": "",
@@ -1146,12 +1143,12 @@ namespace BTCPayServer.Services
"Remove Store Permission": "",
"Remove store user": "",
"Remove the translation from this dictionary.": "",
"Remove this email rule": "",
"Remove wallet": "",
"Removing this user would result in the store having no owner.": "",
"REPLACE": "",
"Replace {0} wallet": "",
"Replace wallet": "",
"Replacements": "",
"Reporting": "",
"Request": "",
"Request contributor data on checkout": "",
@@ -1159,7 +1156,6 @@ namespace BTCPayServer.Services
"Request Pairing": "",
"Requests": "",
"Requests may be paid in partial. They will remain valid until time expires or when paid what is due.": "",
"Required Confirmations": "",
"Required Field": "",
"Rescan Wallet": "",
"Rescan wallet for missing transactions": "",
@@ -1312,6 +1308,7 @@ namespace BTCPayServer.Services
"Sign the transaction": "",
"Sign transaction": "",
"Sign using our Vault application": "",
"Signed out": "",
"SIN": "",
"Slider": "",
"SMTP Server": "",
@@ -1347,14 +1344,12 @@ namespace BTCPayServer.Services
"Storage Provider": "",
"Storage settings updated successfully": "",
"Store": "",
"Store email rules saved.": "",
"Store has not enabled Pay Button": "",
"Store Id": "",
"Store Name": "",
"Store Overview": "",
"Store removed successfully": "",
"Store Settings": "",
"Store Speed Policy": "",
"Store successfully created": "",
"Store successfully updated": "",
"Store Users": "",
@@ -1382,9 +1377,7 @@ namespace BTCPayServer.Services
"Test": "",
"Test connection": "",
"Test Email": "",
"Test email sent — please verify you received it.": "",
"Test Results:": "",
"Test this email rule": "",
"Testing": "",
"Text": "",
"Text to display in the tip input": "",
@@ -1403,6 +1396,8 @@ namespace BTCPayServer.Services
"The batch size make sure the scan do not consume too much RAM at once by rescanning several time with smaller subset of addresses.": "",
"The brand color needs to be a valid hex color code": "",
"The card is now configured": "",
"The change output is too small to pay for additional fee.": "",
"The change output is too small to pay for additional fee. (Missing {0} BTC)": "",
"The chosen field's selected value will be copied to this field upon submission.": "",
"The combination of words below are called your recovery phrase. The recovery phrase allows you to access and restore your wallet. Write them down on a piece of paper in the exact order:": "",
"The configured name means the value of this field will adjust the invoice amount by multiplying it for public forms and the point of sale app.": "",
@@ -1468,7 +1463,12 @@ namespace BTCPayServer.Services
"The uploaded sound file needs to be an audio file": "",
"The uploaded sound file should be less than {0}": "",
"The URL to post purchase data.": "",
"The user could not be added: {0}": "",
"The user could not be invited: {0}": "",
"The user could not be updated: {0}": "",
"The user declined access to the vault.": "",
"The user has been added successfully.": "",
"The user has been updated successfully.": "",
"The user will be permanently deleted. This action will also delete all stores, invoices, apps and data associated with your user.": "",
"The user will not be able to change the field's value": "",
"The values being mirrored from another field will be mapped to another value if configured.": "",
@@ -1498,8 +1498,7 @@ namespace BTCPayServer.Services
"There are no stores yet.": "",
"There are no wallets yet. You can add wallets in the store setup.": "",
"There are no webhooks yet.": "",
"There is already an active wallet configured for {0}. Replacing it would break any existing invoices!": "",
"There isn't any UTXO available to bump fee": "",
"There isn't any UTXO available to bump fee with CPFP": "",
"There was an error generating your wallet: {0}": "",
"This account has been locked out because of multiple invalid login attempts. Please try again later.": "",
"This account has been locked out. Please try again": "",
@@ -1542,7 +1541,9 @@ namespace BTCPayServer.Services
"This QR Code is only valid for 10 minutes": "",
"This store is ready to accept transactions, good job!": "",
"This store will still be accessible to users sharing it": "",
"This transaction can't be RBF'd": "",
"This transaction will change your balance:": "",
"This version of NBXplorer is not compatible. Please update to 2.5.22 or above": "",
"This webhook will be removed from this store.": "",
"This webhook will be removed from this store. Are you sure?": "",
"This will approve the user <strong>{0}</strong>.": "",
@@ -1580,6 +1581,7 @@ namespace BTCPayServer.Services
"Transaction broadcasted successfully ({0})": "",
"Transaction Details": "",
"Transaction fee rate:": "",
"Transaction Id": "",
"transactions": "",
"Translations": "",
"Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "",
@@ -1587,6 +1589,7 @@ namespace BTCPayServer.Services
"Two-Factor Authentication": "",
"Two-Factor Authentication (2FA) is an additional measure to protect your account. In addition to your password you will be asked for a second proof on login. This can be provided by an app (such as Google or Microsoft Authenticator) or a security device (like a Yubikey or your hardware wallet supporting FIDO2).": "",
"Type": "",
"Unable to create the replacement transaction ({0})": "",
"Unarchive": "",
"Unarchive this app": "",
"Unarchive this invoice": "",
@@ -1610,6 +1613,7 @@ namespace BTCPayServer.Services
"Update Crowdfund": "",
"Update Password": "",
"Update Point of Sale": "",
"Update Role": "",
"Update to the latest version of BTCPay Server.": "",
"Update Webhook": "",
"Update your account": "",
@@ -1619,7 +1623,6 @@ namespace BTCPayServer.Services
"Upload Plugin": "",
"Upload PSBT from file…": "",
"Upload the file exported from your wallet.": "",
"Upload Wallet": "",
"Uploaded By": "",
"Url": "",
"URL": "",
@@ -1640,7 +1643,6 @@ namespace BTCPayServer.Services
"Use the stores default": "",
"User": "",
"User {0} accepted the invite to {1}.": "",
"User {0} is the last owner. Their role cannot be changed.": "",
"User accepted invitation": "",
"User approved": "",
"User can input custom amount": "",
@@ -1650,6 +1652,7 @@ namespace BTCPayServer.Services
"User enabled": "",
"User is admin": "",
"User is approved": "",
"User not found": "",
"User removed successfully.": "",
"User successfully updated": "",
"User unapproved": "",
@@ -1686,15 +1689,11 @@ namespace BTCPayServer.Services
"View all supporters": "",
"View Invite": "",
"View seed": "",
"View-Only Wallet File": "",
"View-only wallet files uploaded. The wallet will soon become available.": "",
"Waiting for NFC to be presented...": "",
"Wallet": "",
"Wallet Balance": "",
"Wallet file": "",
"Wallet file content": "",
"Wallet Keys File": "",
"Wallet Password": "",
"Wallet Recovery Seed": "",
"Wallet settings for {0} have been updated.": "",
"Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "",
@@ -1776,8 +1775,7 @@ namespace BTCPayServer.Services
"Your password has been set.": "",
"Your profile has been updated": "",
"Your two-factor authenticator app will provide you with a unique code.": "",
"Your wallet has been generated.": "",
"Zero Confirmation": ""
"Your wallet has been generated.": ""
}
""";
Default = Translations.CreateFromJson(knownTranslations);