From 95b976fb4bdb649be3893405c1b7dd05060aa9bb Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 8 Apr 2025 22:49:03 +0900 Subject: [PATCH 1/2] Restore behavior: Invite non existing users when adding a store user --- .../Controllers/UIStoresController.Users.cs | 89 ++++++++++++++++--- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/BTCPayServer/Controllers/UIStoresController.Users.cs b/BTCPayServer/Controllers/UIStoresController.Users.cs index 077751538..a28979b2f 100644 --- a/BTCPayServer/Controllers/UIStoresController.Users.cs +++ b/BTCPayServer/Controllers/UIStoresController.Users.cs @@ -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 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.
You may alternatively share this link with them: {0}", evt.InvitationLink] + : StringLocalizer["An invitation email has not been sent, because the server does not have an email server configured.
You need to share this link with them: {0}", 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 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 UpdateStoreUser(string storeId, string userId, StoreUsersViewModel.StoreUserViewModel vm) From b3bb295c0c88d021f4a3e2b9d681bac681a1f023 Mon Sep 17 00:00:00 2001 From: "nicolas.dorier" Date: Tue, 8 Apr 2025 22:52:19 +0900 Subject: [PATCH 2/2] Update translation files --- BTCPayServer/Services/Translations.Default.cs | 72 +++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/BTCPayServer/Services/Translations.Default.cs b/BTCPayServer/Services/Translations.Default.cs index 47a536c28..8b4e38b53 100644 --- a/BTCPayServer/Services/Translations.Default.cs +++ b/BTCPayServer/Services/Translations.Default.cs @@ -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.
You may alternatively share this link with them: {0}": "", + "An invitation email has not been sent, because the server does not have an email server configured.
You need to share this link with them: {0}": "", "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: {StoreName} {ItemDescription} {OrderId}": "", "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 {0}.": "", @@ -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, {0} translates {1} to {2}.": "", @@ -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 store’s 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);