Delete confirmation modals (#2614)

* Refactor confirm view: separate modal

* Add delete confirmation modals for apps and FIDO2

* Add delete confirmation modals for 2FA actions

* Add delete confirmation modals for api keys and webhooks

* Add delete confirmation modals for stores and store users

* Add delete confirmation modals for LND seed and SSH

* Add delete confirmation modals for rate rule scripting

* Test fixes and improvements

* Add delete confirmation modals for dynamic DNS

* Add delete confirmation modals for store access tokens

* Add confirmation modals for pull payment archiving

* Refactor confirm modal code

* Add confirmation input, update wording

* Update modal styles

* Upgrade ChromeDriver

* Simplify and unify confirmation input

* Test fixes

* Fix wording

* Add modals for wallet replace and removal
This commit is contained in:
d11n 2021-09-07 04:55:53 +02:00 committed by GitHub
parent 6d317937c7
commit 06db29dd43
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 527 additions and 464 deletions

View file

@ -23,7 +23,7 @@
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.13" />
<PackageReference Include="Selenium.Support" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver" Version="3.141.0" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="90.0.4430.2400" />
<PackageReference Include="Selenium.WebDriver.ChromeDriver" Version="92.0.4515.10700" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.2">
<PrivateAssets>all</PrivateAssets>

View file

@ -135,7 +135,8 @@ namespace BTCPayServer.Tests
if (Driver.PageSource.Contains("id=\"ChangeWalletLink\""))
{
Driver.FindElement(By.Id("ChangeWalletLink")).Click();
Driver.FindElement(By.Id("continue")).Click();
Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("REPLACE");
Driver.FindElement(By.Id("ConfirmContinue")).Click();
}
if (isImport)

View file

@ -85,7 +85,8 @@ namespace BTCPayServer.Tests
Assert.True(passEl.Displayed);
Assert.Contains(passEl.Text, "hellorockstar", StringComparison.OrdinalIgnoreCase);
s.Driver.FindElement(By.Id("delete")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
seedEl = s.Driver.FindElement(By.Id("Seed"));
Assert.Contains("Seed removed", seedEl.Text, StringComparison.OrdinalIgnoreCase);
@ -249,12 +250,14 @@ namespace BTCPayServer.Tests
// Let's try to disable it now
s.Driver.FindElement(By.Id("disable")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
policies = await settings.GetSettingAsync<PoliciesSettings>();
Assert.True(policies.DisableSSHService);
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DISABLE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.Navigate().GoToUrl(s.Link("/server/services/ssh"));
Assert.True(s.Driver.PageSource.Contains("404 - Page not found", StringComparison.OrdinalIgnoreCase));
policies = await settings.GetSettingAsync<PoliciesSettings>();
Assert.True(policies.DisableSSHService);
policies.DisableSSHService = false;
await settings.UpdateSetting(policies);
}
@ -296,7 +299,7 @@ namespace BTCPayServer.Tests
{
// Cleanup old test run
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
}
s.Driver.FindElement(By.Id("AddDynamicDNS")).Click();
s.Driver.AssertNoError();
@ -323,7 +326,7 @@ namespace BTCPayServer.Tests
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns"));
Assert.Contains("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
s.Driver.Navigate().GoToUrl(s.Link("/server/services/dynamic-dns/pouet.hello.com/delete"));
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.AssertNoError();
Assert.DoesNotContain("/server/services/dynamic-dns/pouet.hello.com/delete", s.Driver.PageSource);
@ -427,8 +430,9 @@ namespace BTCPayServer.Tests
s.Logout();
s.LogIn(alice);
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.FindElement(By.LinkText("Remove")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.FindElement(By.LinkText("Delete")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
s.Driver.FindElement(By.Id("Stores")).Click();
s.Driver.Navigate().GoToUrl(storeUrl);
Assert.Contains("ReturnUrl", s.Driver.Url);
@ -674,7 +678,8 @@ namespace BTCPayServer.Tests
var deletes = s.Driver.FindElements(By.LinkText("Delete"));
Assert.Equal(2, deletes.Count);
deletes[0].Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmInput")).SendKeys("DELETE");
s.Driver.FindElement(By.Id("ConfirmContinue")).Click();
deletes = s.Driver.FindElements(By.LinkText("Delete"));
Assert.Single(deletes);
s.FindAlertMessage();
@ -761,7 +766,7 @@ namespace BTCPayServer.Tests
s.Driver.ToggleCollapse("danger-zone");
s.Driver.FindElement(By.Id("delete-store")).Click();
s.Driver.FindElement(By.Id("continue")).Click();
s.Driver.WaitForElement(By.Id("ConfirmContinue")).Click();
s.FindAlertMessage();
}
}

View file

@ -100,7 +100,7 @@ namespace BTCPayServer.Controllers
if (appData == null)
return NotFound();
if (await _AppService.DeleteApp(appData))
TempData[WellKnownTempData.SuccessMessage] = "App removed successfully";
TempData[WellKnownTempData.SuccessMessage] = "App deleted successfully.";
return RedirectToAction(nameof(ListApps));
}
@ -177,19 +177,13 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet]
[Route("{appId}/delete")]
[HttpGet("{appId}/delete")]
public async Task<IActionResult> DeleteApp(string appId)
{
var appData = await GetOwnedApp(appId);
if (appData == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = $"Delete app {appData.Name} ({appData.AppType})",
Description = "This app will be removed from this store",
Action = "Delete"
});
return View("Confirm", new ConfirmModel("Delete app", $"The app <strong>{appData.Name}</strong> and its settings will be permanently deleted. Are you sure?", "Delete"));
}
private Task<AppData> GetOwnedApp(string appId, AppType? type = null)
@ -197,7 +191,6 @@ namespace BTCPayServer.Controllers
return _AppService.GetAppDataIfOwner(GetUserId(), appId, type);
}
private string GetUserId()
{
return _UserManager.GetUserId(User);

View file

@ -34,34 +34,6 @@ namespace BTCPayServer.Controllers
return View(model);
}
[HttpGet]
public async Task<IActionResult> Disable2faWarning()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
if (!user.TwoFactorEnabled)
{
throw new ApplicationException(
$"Unexpected error occurred disabling 2FA for user with ID '{user.Id}'.");
}
return View("Confirm",
new ConfirmModel()
{
Title = $"Disable two-factor authentication (2FA)",
DescriptionHtml = true,
Description =
$"Disabling 2FA does not change the keys used in authenticator apps. If you wish to change the key used in an authenticator app you should <a href=\"{Url.Action(nameof(ResetAuthenticatorWarning))}\"> reset your authenticator keys</a>.",
Action = "Disable 2FA",
ActionUrl = Url.ActionLink(nameof(Disable2fa))
});
}
[HttpPost]
public async Task<IActionResult> Disable2fa()
{
var user = await _userManager.GetUserAsync(User);
@ -134,21 +106,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(GenerateRecoveryCodes), new {confirm = false});
}
[HttpGet]
public IActionResult ResetAuthenticatorWarning()
{
return View("Confirm",
new ConfirmModel()
{
Title = $"Reset authenticator key",
Description =
$"This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes.{Environment.NewLine}If you do not complete your authenticator app configuration you may lose access to your account.",
Action = "Reset",
ActionUrl = Url.ActionLink(nameof(ResetAuthenticator))
});
}
[HttpPost]
public async Task<IActionResult> ResetAuthenticator()
{
var user = await _userManager.GetUserAsync(User);
@ -164,25 +121,6 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(EnableAuthenticator));
}
[HttpGet]
public async Task<IActionResult> GenerateRecoveryCodes(bool confirm = true)
{
if (!confirm)
{
return await GenerateRecoveryCodes();
}
return View("Confirm",
new ConfirmModel()
{
Title = $"Are you sure you want to generate new recovery codes?",
Description = "Your existing recovery codes will no longer be valid!",
Action = "Generate",
ActionUrl = Url.ActionLink(nameof(GenerateRecoveryCodes))
});
}
[HttpPost]
public async Task<IActionResult> GenerateRecoveryCodes()
{
var recoveryCodes = (string[])TempData[RecoveryCodesKey];

View file

@ -30,26 +30,26 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKey(string id)
[HttpGet("~/api-keys/{id}/delete")]
public async Task<IActionResult> DeleteAPIKey(string id)
{
var key = await _apiKeyRepository.GetKey(id);
if (key == null || key.UserId != _userManager.GetUserId(User))
{
return NotFound();
}
return View("Confirm", new ConfirmModel()
return View("Confirm", new ConfirmModel
{
Title = $"Delete API Key {(string.IsNullOrEmpty(key.Label) ? string.Empty : key.Label)}",
Title = "Delete API key",
DescriptionHtml = true,
Description = $"Any application using this API key will immediately lose access: <code>{key.Id}</code>",
Description = $"Any application using the API key <strong>{key.Label ?? key.Id}<strong> will immediately lose access.",
Action = "Delete",
ActionUrl = Url.ActionLink(nameof(RemoveAPIKeyPost), values: new { id })
ActionUrl = Url.ActionLink(nameof(DeleteAPIKeyPost), values: new { id })
});
}
[HttpPost("api-keys/{id}/delete")]
public async Task<IActionResult> RemoveAPIKeyPost(string id)
[HttpPost("~/api-keys/{id}/delete")]
public async Task<IActionResult> DeleteAPIKeyPost(string id)
{
var key = await _apiKeyRepository.GetKey(id);
if (key == null || key.UserId != _userManager.GetUserId(User))

View file

@ -194,7 +194,7 @@ namespace BTCPayServer.Controllers
return View(model);
}
[Route("server/users/{userId}/delete")]
[HttpGet("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUser(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
@ -208,24 +208,19 @@ namespace BTCPayServer.Controllers
if (admins.Count == 1)
{
// return
return View("Confirm", new ConfirmModel("Unable to Delete Last Admin",
"This is the last Admin, so it can't be removed"));
return View("Confirm", new ConfirmModel("Delete admin",
"Unable to proceed: As the user <strong>{user.Email}</strong> is the last admin, it cannot be removed."));
}
return View("Confirm", new ConfirmModel("Delete Admin " + user.Email,
"Are you sure you want to delete this Admin and delete all accounts, users and data associated with the server account?",
return View("Confirm", new ConfirmModel("Delete admin",
$"The admin <strong>{user.Email}</strong> will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?",
"Delete"));
}
else
{
return View("Confirm", new ConfirmModel("Delete user " + user.Email,
"This user will be permanently deleted",
"Delete"));
}
return View("Confirm", new ConfirmModel("Delete user", $"The user <strong>{user.Email}</strong> will be permanently deleted. Are you sure?", "Delete"));
}
[Route("server/users/{userId}/delete")]
[HttpPost]
[HttpPost("server/users/{userId}/delete")]
public async Task<IActionResult> DeleteUserPost(string userId)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);

View file

@ -546,20 +546,13 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet]
[Route("server/services/{serviceName}/{cryptoCode}/removelndseed")]
[HttpGet("server/services/{serviceName}/{cryptoCode}/removelndseed")]
public IActionResult RemoveLndSeed(string serviceName, string cryptoCode)
{
return View("Confirm", new ConfirmModel()
{
Title = "Delete LND Seed",
Description = "Please make sure you made a backup of the seed and password before deleting the LND backup seed from the server, are you sure to continue?",
Action = "Delete"
});
return View("Confirm", new ConfirmModel("Delete LND seed", "This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup. Are you sure?", "Delete"));
}
[HttpPost]
[Route("server/services/{serviceName}/{cryptoCode}/removelndseed")]
[HttpPost("server/services/{serviceName}/{cryptoCode}/removelndseed")]
public async Task<IActionResult> RemoveLndSeedPost(string serviceName, string cryptoCode)
{
var service = GetService(serviceName, cryptoCode);
@ -819,23 +812,20 @@ namespace BTCPayServer.Controllers
this.RouteData.Values.Remove(nameof(hostname));
return RedirectToAction(nameof(DynamicDnsServices));
}
[HttpGet]
[Route("server/services/dynamic-dns/{hostname}/delete")]
[HttpGet("server/services/dynamic-dns/{hostname}/delete")]
public async Task<IActionResult> DeleteDynamicDnsService(string hostname)
{
var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
var settings = await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>() ?? new DynamicDnsSettings();
var i = settings.Services.FindIndex(d => d.Hostname.Equals(hostname, StringComparison.OrdinalIgnoreCase));
if (i == -1)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete the dynamic dns service for " + hostname,
Description = "BTCPayServer will stop updating this DNS record periodically",
Action = "Delete"
});
return View("Confirm",
new ConfirmModel("Delete dynamic DNS service",
$"Deleting the dynamic DNS service for <strong>{hostname}</strong> means your BTCPay Server will stop updating the associated DNS record periodically.", "Delete"));
}
[HttpPost]
[Route("server/services/dynamic-dns/{hostname}/delete")]
[HttpPost("server/services/dynamic-dns/{hostname}/delete")]
public async Task<IActionResult> DeleteDynamicDnsServicePost(string hostname)
{
var settings = (await _SettingsRepository.GetSettingAsync<DynamicDnsSettings>()) ?? new DynamicDnsSettings();
@ -845,11 +835,11 @@ namespace BTCPayServer.Controllers
settings.Services.RemoveAt(i);
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Dynamic DNS service successfully removed";
this.RouteData.Values.Remove(nameof(hostname));
RouteData.Values.Remove(nameof(hostname));
return RedirectToAction(nameof(DynamicDnsServices));
}
[Route("server/services/ssh")]
[HttpGet("server/services/ssh")]
public async Task<IActionResult> SSHService()
{
if (!await CanShowSSHService())
@ -902,8 +892,7 @@ namespace BTCPayServer.Controllers
return _Options.SSHSettings?.AuthorizedKeysFile != null && System.IO.File.Exists(_Options.SSHSettings.AuthorizedKeysFile);
}
[HttpPost]
[Route("server/services/ssh")]
[HttpPost("server/services/ssh")]
public async Task<IActionResult> SSHService(SSHServiceViewModel viewModel, string command = null)
{
if (!await CanShowSSHService())
@ -959,26 +948,22 @@ namespace BTCPayServer.Controllers
}
return RedirectToAction(nameof(SSHService));
}
else if (command is "disable")
if (command is "disable")
{
return RedirectToAction(nameof(SSHServiceDisable));
}
return NotFound();
}
[Route("server/services/ssh/disable")]
[HttpGet("server/services/ssh/disable")]
public IActionResult SSHServiceDisable()
{
return View("Confirm", new ConfirmModel()
{
Action = "Disable",
Title = "Disable modification of SSH settings",
Description = "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.",
ButtonClass = "btn-danger"
});
return View("Confirm", new ConfirmModel("Disable modification of SSH settings", "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.", "Disable"));
}
[Route("server/services/ssh/disable")]
[HttpPost]
[HttpPost("server/services/ssh/disable")]
public async Task<IActionResult> SSHServiceDisablePost()
{
var policies = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new PoliciesSettings();

View file

@ -66,12 +66,7 @@ namespace BTCPayServer.Controllers
if (webhook is null)
return NotFound();
return View("Confirm", new ConfirmModel
{
Title = $"Delete a webhook",
Description = "This webhook will be removed from this store, do you wish to continue?",
Action = "Delete"
});
return View("Confirm", new ConfirmModel("Delete webhook", "This webhook will be removed from this store. Are you sure?", "Delete"));
}
[HttpPost("{storeId}/webhooks/{webhookId}/remove")]

View file

@ -398,6 +398,9 @@ namespace BTCPayServer.Controllers
vm.Config = ProtectString(derivation.ToJson());
vm.IsHotWallet = derivation.IsHotWallet;
ViewData["ReplaceDescription"] = WalletReplaceWarning(derivation.IsHotWallet);
ViewData["RemoveDescription"] = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode);
return View(vm);
}
@ -411,20 +414,11 @@ namespace BTCPayServer.Controllers
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var walletType = derivation.IsHotWallet ? "hot" : "watch-only";
var additionalText = derivation.IsHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger fw-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger fw-bold\">Do not replace the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-start mb-0\">Replacing the wallet will erase the current wallet data from the server. " +
"The current wallet will be replaced once you finish the setup of the new wallet. If you cancel the setup, the current wallet will stay active .</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet",
Description = description,
Description = WalletReplaceWarning(derivation.IsHotWallet),
DescriptionHtml = true,
Action = "Setup new wallet"
});
@ -458,20 +452,11 @@ namespace BTCPayServer.Controllers
}
var derivation = GetExistingDerivationStrategy(cryptoCode, store);
var walletType = derivation.IsHotWallet ? "hot" : "watch-only";
var additionalText = derivation.IsHotWallet
? ""
: " or imported into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
var description =
$"<p class=\"text-danger fw-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger fw-bold\">Do not remove the wallet if you have not backed it up{additionalText}.</p>" +
"<p class=\"text-start mb-0\">Removing the wallet will erase the wallet data from the server. " +
$"The store won't be able to receive {network.CryptoCode} onchain payments until a new wallet is set up.</p>";
return View("Confirm", new ConfirmModel
{
Title = $"Remove {network.CryptoCode} wallet",
Description = description,
Description = WalletRemoveWarning(derivation.IsHotWallet, network.CryptoCode),
DescriptionHtml = true,
Action = "Remove"
});
@ -595,5 +580,30 @@ namespace BTCPayServer.Controllers
return await stream.ReadToEndAsync();
}
}
private string WalletWarning(bool isHotWallet, string info)
{
var walletType = isHotWallet ? "hot" : "watch-only";
var additionalText = isHotWallet
? ""
: " or imported it into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet";
return
$"<p class=\"text-danger fw-bold\">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\"text-danger fw-bold\">Do not proceed if you have not backed up the wallet{additionalText}.</p>" +
$"<p class=\"text-start mb-0\">This action will erase the current wallet data from the server. {info}</p>";
}
private string WalletReplaceWarning(bool isHotWallet)
{
return WalletWarning(isHotWallet,
"The current wallet will be replaced once you finish the setup of the new wallet. " +
"If you cancel the setup, the current wallet will stay active.");
}
private string WalletRemoveWarning(bool isHotWallet, string cryptoCode)
{
return WalletWarning(isHotWallet,
$"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up.");
}
}
}

View file

@ -184,37 +184,28 @@ namespace BTCPayServer.Controllers
ModelState.AddModelError(nameof(vm.Email), "The user already has access to this store");
return View(vm);
}
TempData[WellKnownTempData.SuccessMessage] = "User added successfully";
TempData[WellKnownTempData.SuccessMessage] = "User added successfully.";
return RedirectToAction(nameof(StoreUsers));
}
[HttpGet]
[Route("{storeId}/users/{userId}/delete")]
[HttpGet("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUser(string userId)
{
StoreUsersViewModel vm = new StoreUsersViewModel();
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = $"Remove store user",
Description = $"Are you sure you want to remove store access for {user.Email}?",
Action = "Delete"
});
return View("Confirm", new ConfirmModel("Remove store user", $"This action will prevent <strong>{user.Email}</strong> from accessing this store and its settings. Are you sure?", "Remove"));
}
[HttpPost]
[Route("{storeId}/users/{userId}/delete")]
[HttpPost("{storeId}/users/{userId}/delete")]
public async Task<IActionResult> DeleteStoreUserPost(string storeId, string userId)
{
await _Repo.RemoveStoreUser(storeId, userId);
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully";
return RedirectToAction(nameof(StoreUsers), new { storeId = storeId, userId = userId });
TempData[WellKnownTempData.SuccessMessage] = "User removed successfully.";
return RedirectToAction(nameof(StoreUsers), new { storeId, userId });
}
[HttpGet]
[Route("{storeId}/rates")]
[HttpGet("{storeId}/rates")]
public IActionResult Rates()
{
var exchanges = GetSupportedExchanges();
@ -231,8 +222,7 @@ namespace BTCPayServer.Controllers
return View(vm);
}
[HttpPost]
[Route("{storeId}/rates")]
[HttpPost("{storeId}/rates")]
public async Task<IActionResult> Rates(RatesViewModel model, string command = null, string storeId = null, CancellationToken cancellationToken = default)
{
if (command == "scripting-on")
@ -350,11 +340,10 @@ namespace BTCPayServer.Controllers
}
}
[HttpGet]
[Route("{storeId}/rates/confirm")]
[HttpGet("{storeId}/rates/confirm")]
public IActionResult ShowRateRules(bool scripting)
{
return View("Confirm", new ConfirmModel()
return View("Confirm", new ConfirmModel
{
Action = "Continue",
Title = "Rate rule scripting",
@ -365,8 +354,7 @@ namespace BTCPayServer.Controllers
});
}
[HttpPost]
[Route("{storeId}/rates/confirm")]
[HttpPost("{storeId}/rates/confirm")]
public async Task<IActionResult> ShowRateRulesPost(bool scripting)
{
var blob = CurrentStore.GetStoreBlob();
@ -374,7 +362,7 @@ namespace BTCPayServer.Controllers
blob.RateScript = blob.GetDefaultRateRules(_NetworkProvider).ToString();
CurrentStore.SetStoreBlob(blob);
await _Repo.UpdateStore(CurrentStore);
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting activated";
TempData[WellKnownTempData.SuccessMessage] = "Rate rules scripting " + (scripting ? "activated" : "deactivated");
return RedirectToAction(nameof(Rates), new { storeId = CurrentStore.Id });
}
@ -666,25 +654,17 @@ namespace BTCPayServer.Controllers
});
}
[HttpGet]
[Route("{storeId}/delete")]
[HttpGet("{storeId}/delete")]
public IActionResult DeleteStore(string storeId)
{
return View("Confirm", new ConfirmModel()
{
Action = "Delete",
Title = "Delete this store",
Description = "This action is irreversible and will remove all information related to this store. (Invoices, Apps etc...)",
ButtonClass = "btn-danger"
});
return View("Confirm", new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?", "Delete"));
}
[HttpPost]
[Route("{storeId}/delete")]
[HttpPost("{storeId}/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
await _Repo.DeleteStore(CurrentStore.Id);
TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted";
TempData[WellKnownTempData.SuccessMessage] = "Store successfully deleted.";
return RedirectToAction(nameof(UserStoresController.ListStores), "UserStores");
}
@ -744,32 +724,24 @@ namespace BTCPayServer.Controllers
model.EncodedApiKey = Encoders.Base64.EncodeData(Encoders.ASCII.DecodeData(model.ApiKey));
return View(model);
}
[HttpGet]
[Route("{storeId}/tokens/{tokenId}/revoke")]
[HttpGet("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeToken(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null || token.StoreId != CurrentStore.Id)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Action = "Revoke",
Title = "Revoke the token",
Description = $"The access token with the label \"{token.Label}\" will be revoked, do you wish to continue?",
ButtonClass = "btn-danger"
});
return View("Confirm", new ConfirmModel("Revoke the token", $"The access token with the label <strong>{token.Label}</strong> will be revoked. Do you wish to continue?", "Revoke"));
}
[HttpPost]
[Route("{storeId}/tokens/{tokenId}/revoke")]
[HttpPost("{storeId}/tokens/{tokenId}/revoke")]
public async Task<IActionResult> RevokeTokenConfirm(string tokenId)
{
var token = await _TokenRepository.GetToken(tokenId);
if (token == null ||
token.StoreId != CurrentStore.Id ||
!await _TokenRepository.DeleteToken(tokenId))
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token";
TempData[WellKnownTempData.ErrorMessage] = "Failure to revoke this token.";
else
TempData[WellKnownTempData.SuccessMessage] = "Token revoked";
return RedirectToAction(nameof(ListTokens), new { storeId = token.StoreId });

View file

@ -61,23 +61,16 @@ namespace BTCPayServer.Controllers
get; set;
}
[HttpGet]
[Route("{storeId}/me/delete")]
[HttpGet("{storeId}/me/delete")]
public IActionResult DeleteStore(string storeId)
{
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel()
{
Title = "Delete store " + store.StoreName,
Description = "This store will still be accessible to users sharing it",
Action = "Delete"
});
return View("Confirm", new ConfirmModel($"Delete store {store.StoreName}", "This store will still be accessible to users sharing it", "Delete"));
}
[HttpPost]
[Route("{storeId}/me/delete")]
[HttpPost("{storeId}/me/delete")]
public async Task<IActionResult> DeleteStorePost(string storeId)
{
var userId = GetUserId();

View file

@ -153,13 +153,7 @@ namespace BTCPayServer.Controllers
WalletId walletId,
string pullPaymentId)
{
return View("Confirm", new ConfirmModel()
{
Title = "Archive the pull payment",
Description = "Do you really want to archive this pull payment?",
ButtonClass = "btn-danger",
Action = "Archive"
});
return View("Confirm", new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"));
}
[HttpPost("{walletId}/pull-payments/{pullPaymentId}/archive")]

View file

@ -1,32 +1,31 @@
using System;
namespace BTCPayServer.Models
{
public class ConfirmModel
{
public ConfirmModel() { }
private const string ButtonClassDefault = "btn-danger";
public ConfirmModel() {}
public ConfirmModel(string title, string desc, string action = null)
public ConfirmModel(string title, string desc, string action = null, string buttonClass = ButtonClassDefault)
{
Title = title;
Description = desc;
Action = action;
ButtonClass = buttonClass;
if (Description.Contains("<strong>", StringComparison.InvariantCultureIgnoreCase))
{
DescriptionHtml = true;
}
}
public string Title
{
get; set;
}
public string Description
{
get; set;
}
public bool DescriptionHtml { get; set; } = false;
public string Action
{
get; set;
}
public string ButtonClass { get; set; } = "btn-danger";
public string Title { get; set; }
public string Description { get; set; }
public bool DescriptionHtml { get; set; }
public string Action { get; set; }
public string ButtonClass { get; set; } = ButtonClassDefault;
public string ActionUrl { get; set; }
}
}

View file

@ -6,6 +6,7 @@ namespace BTCPayServer.Models.StoreViewModels
{
public class CreateTokenViewModel
{
[Display(Name = "Public Key")]
[PubKeyValidatorAttribute]
public string PublicKey
{

View file

@ -97,7 +97,7 @@
<a asp-action="@app.ViewAction" asp-controller="AppsPublic" asp-route-appId="@app.Id" target="_blank"
title="View in New Window"><span class="fa fa-external-link"></span></a>
<span> - </span>
<a asp-action="DeleteApp" asp-route-appId="@app.Id">Remove</a>
<a asp-action="DeleteApp" asp-route-appId="@app.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The app <strong>@app.AppName</strong> and its settings will be permanently deleted from your store <strong>@app.StoreName</strong>." data-confirm-input="DELETE">Delete</a>
</td>
</tr>
}
@ -114,3 +114,5 @@
</div>
</div>
</section>
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" />

View file

@ -13,10 +13,11 @@
<tbody>
@foreach (var device in Model.Credentials)
{
var name = string.IsNullOrEmpty(device.Name) ? "Unnamed FIDO2 credential" : device.Name;
<tr>
<td>@(string.IsNullOrEmpty(device.Name)? "Unnamed FIDO2 credential": device.Name)</td>
<td>@name</td>
<td class="text-end">
<a asp-action="Remove" asp-route-id="@device.Id">Remove</a>
<a asp-action="Remove" asp-route-id="@device.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="Your account will no longer have the credential <strong>@name</strong> as an option for multi-factor authentication." data-confirm-input="REMOVE">Remove</a>
</td>
</tr>
}
@ -44,3 +45,5 @@
</div>
</div>
</form>
<partial name="_Confirm" model="@(new ConfirmModel("Remove FIDO2 credential", "Your account will no longer have the credential as an option for multi-factor authentication.", "Remove"))" />

View file

@ -50,14 +50,14 @@
@foreach (var permission in Permission.ToPermissions(permissions).Select(c => c.ToString()).Distinct().ToArray())
{
<li>
<code>@permission</code>
<code class="text-break">@permission</code>
</li>
}
</ul>
}
</td>
<td class="text-end">
<a asp-action="RemoveAPIKey" asp-route-id="@keyData.Id" asp-controller="Manage">Remove</a>
<a asp-action="DeleteAPIKey" asp-route-id="@keyData.Id" asp-controller="Manage" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="Any application using the API key <strong>@(keyData.Label ?? keyData.Id)<strong> will immediately lose access." data-confirm-input="DELETE">Delete</a>
<span>-</span>
<button type="button" class="btn btn-link only-for-js" data-qr="@index">Show QR</button>
</td>
@ -79,46 +79,47 @@
Generate new key
</a>
<partial name="_Confirm" model="@(new ConfirmModel("Delete API key", "Any application using the API key will immediately lose access.", "Delete"))" />
<partial name="ShowQR"/>
@section PageHeadContent {
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
}
@section PageFootContent {
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
<script>
document.addEventListener("DOMContentLoaded", function () {
$("[data-reveal-btn]").on("click", function (){
var $revealButton = $(this);
$revealButton.attr("hidden", "true");
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
<script>
document.addEventListener("DOMContentLoaded", function () {
$("[data-reveal-btn]").on("click", function (){
var $revealButton = $(this);
$revealButton.attr("hidden", "true");
var $apiKeyContainer = $revealButton.next("[hidden]");
$apiKeyContainer.removeAttr("hidden");
var $apiKeyContainer = $revealButton.next("[hidden]");
$apiKeyContainer.removeAttr("hidden");
(function setupCopyToClipboardButton() {
var $clipboardBtn = $apiKeyContainer.children("[data-clipboard-confirm]");
var apiKey = $apiKeyContainer.children("[data-api-key]").text().trim();
$clipboardBtn.attr("data-clipboard", apiKey);
$clipboardBtn.click(window.copyToClipboard);
})();
(function setupCopyToClipboardButton() {
var $clipboardBtn = $apiKeyContainer.children("[data-clipboard-confirm]");
var apiKey = $apiKeyContainer.children("[data-api-key]").text().trim();
$clipboardBtn.attr("data-clipboard", apiKey);
$clipboardBtn.click(window.copyToClipboard);
})();
});
var apiKeys = @Safe.Json(Model.ApiKeyDatas.Select(data => new
{
ApiKey = data.Id,
Host = Context.Request.GetAbsoluteRoot()
}));
var qrApp = initQRShow("API Key QR", "", "scan-qr-modal");
$("button[data-qr]").on("click", function (){
var data = apiKeys[parseInt($(this).data("qr"))];
qrApp.data = JSON.stringify(data);
qrApp.currentMode = "static";
qrApp.allowedModes = ["static"];
$("#scan-qr-modal").modal("show");
});
});
var apiKeys = @Safe.Json(Model.ApiKeyDatas.Select(data => new
{
ApiKey = data.Id,
Host = Context.Request.GetAbsoluteRoot()
}));
var qrApp = initQRShow("API Key QR", "", "scan-qr-modal");
$("button[data-qr]").on("click", function (){
var data = apiKeys[parseInt($(this).data("qr"))];
qrApp.data = JSON.stringify(data);
qrApp.currentMode = "static";
qrApp.allowedModes = ["static"];
$("#scan-qr-modal").modal("show");
});
});
</script>
</script>
}
<partial name="ShowQR"/>

View file

@ -3,9 +3,9 @@
ViewData.SetActivePageAndTitle(ManageNavPages.TwoFactorAuthentication, "Two-factor authentication");
}
@if(Model.Is2faEnabled)
@if (Model.Is2faEnabled)
{
if(Model.RecoveryCodesLeft == 0)
if (Model.RecoveryCodesLeft == 0)
{
<div class="alert alert-danger">
<h4 class="alert-heading mb-3">
@ -15,7 +15,7 @@
<p class="mb-0">You must <a asp-action="GenerateRecoveryCodes" class="alert-link">generate a new set of recovery codes</a> before you can log in with a recovery code.</p>
</div>
}
else if(Model.RecoveryCodesLeft == 1)
else if (Model.RecoveryCodesLeft == 1)
{
<div class="alert alert-danger">
<h4 class="alert-heading mb-3">
@ -25,7 +25,7 @@
<p class="mb-0">You can <a asp-action="GenerateRecoveryCodes" class="alert-link">generate a new set of recovery codes</a>.</p>
</div>
}
else if(Model.RecoveryCodesLeft <= 3)
else if (Model.RecoveryCodesLeft <= 3)
{
<div class="alert alert-warning">
<h4 class="alert-heading mb-3">
@ -40,48 +40,46 @@
<div class="list-group">
@if (Model.Is2faEnabled)
{
<a asp-action="Disable2faWarning" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<a asp-action="Disable2fa" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Disable two-factor authentication (2FA)" data-description="Disabling 2FA does not change the keys used in the authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys." data-confirm="Disable 2FA">
<div>
<h5 >Disable 2FA</h5>
<h5>Disable 2FA</h5>
<p class="mb-0 me-3">Disable two-factor authentication. Re-enabling will not require you to reconfigure your Authenticator app. </p>
</div>
<i class="fa fa-chevron-right"></i>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="GenerateRecoveryCodes" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<a asp-action="GenerateRecoveryCodes" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset recovery codes" data-description="Your existing recovery codes will no longer be valid!" data-confirm="Reset">
<div>
<h5 >Reset recovery codes</h5>
<h5>Reset recovery codes</h5>
<p class="mb-0 me-3">Regenerate your two-factor recovery codes.</p>
</div>
<i class="fa fa-chevron-right"></i>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<a asp-action="ResetAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-title="Reset authenticator app" data-description="This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes. If you do not complete your authenticator app configuration you may lose access to your account." data-confirm="Reset">
<div>
<h5 >Configure Authenticator app</h5>
<p class="mb-0 me-3">Display the key or QR code to configure an authenticator app with your current setup.</p>
</div>
<i class="fa fa-chevron-right"></i>
</a>
<a asp-action="ResetAuthenticatorWarning" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<div>
<h5 >Reset Authenticator app</h5>
<h5>Reset authenticator app</h5>
<p class="mb-0 me-3">Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.</p>
</div>
<i class="fa fa-chevron-right"></i>
<vc:icon symbol="caret-right" />
</a>
<a asp-action="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<div>
<h5>Configure Authenticator app</h5>
<p class="mb-0 me-3">Display the key or QR code to configure an authenticator app with your current setup.</p>
</div>
<vc:icon symbol="caret-right" />
</a>
}
else
{
<a asp-action="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
<div>
<h5 >Enable 2FA</h5>
<h5>Enable 2FA</h5>
<p class="mb-0 me-3">Enable two-factor authentication using TOTP with apps such as Google Authenticator.</p>
</div>
<i class="fa fa-chevron-right"></i>
<vc:icon symbol="caret-right" />
</a>
}
</div>
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}
<partial name="_Confirm" model="@(new ConfirmModel("Remove 2FA credential", "Your account will no longer have the credential as an option for multi-factor authentication.", "Remove"))" />

View file

@ -3,51 +3,78 @@
ViewData.SetActivePageAndTitle(ServerNavPages.Services, "Dynamic DNS Settings");
}
<h2 class="mb-4">@ViewData["PageTitle"]</h2>
<div class="row">
<div class="col-md-8">
<div class="d-sm-flex align-items-center justify-content-between mb-3">
<h2 class="mb-0">
@ViewData["PageTitle"]
<small>
<a href="https://docs.btcpayserver.org/Apps/" target="_blank" rel="noreferrer noopener">
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
</a>
</small>
</h2>
<form method="post" asp-action="DynamicDnsService">
<button id="AddDynamicDNS" class="btn btn-primary" type="submit"><span class="fa fa-plus"></span> Add service</button>
</form>
</div>
<div class="form-group">
<p>
<span>
Dynamic DNS service allows you to have a stable DNS name pointing to your server, even if your IP address change regulary. <br />
This is recommended if you are hosting BTCPayServer at home and wish to have a clearnet HTTPS address to access your server.
</span>
Dynamic DNS allows you to have a stable DNS name pointing to your server, even if your IP address changes regulary.
This is recommended if you are hosting BTCPay Server at home and wish to have a clearnet domain to access your server.
</p>
<p>
Note that you need to properly configure your NAT and BTCPay Server installation to get the HTTPS certificate.
See the documentation for <a href="https://docs.btcpayserver.org/DynamicDNS/" target="_blank" rel="noreferrer noopener">more information</a>.
</p>
<p>Note that you need to properly configure your NAT and BTCPayServer install to get HTTPS certificate. Check the documentation for <a href="https://docs.btcpayserver.org/DynamicDNS/" target="_blank" rel="noreferrer noopener">more information</a>.</p>
</div>
<form method="post" asp-action="DynamicDnsService">
<button id="AddDynamicDNS" class="btn btn-primary" type="submit"><span class="fa fa-plus"></span> Add Dynamic DNS</button>
</form>
<table class="table table-hover table-responsive-md">
<thead>
@if (Model.Any())
{
<table class="table table-sm table-responsive-md">
<thead>
<tr>
<th>Hostname</th>
<th>Last updated</th>
<th style="text-align:center;">Enabled</th>
<th style="text-align:right">Actions</th>
<th class="text-center">Enabled</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
</thead>
<tbody>
@foreach (var service in Model)
{
<tr>
<td>@service.Settings.Hostname</td>
<td>@service.LastUpdated</td>
<td style="text-align:center;">
@if(service.Settings.Enabled)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
<td class="text-center">
@if (service.Settings.Enabled)
{
<span class="text-success fa fa-check"></span>
}
else
{
<span class="text-danger fa fa-times"></span>
}
</td>
<td class="text-end">
<a asp-action="DynamicDnsService" asp-route-hostname="@service.Settings.Hostname">Edit</a>
<span> - </span>
<a asp-action="DeleteDynamicDnsService" asp-route-hostname="@service.Settings.Hostname" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="Deleting the dynamic DNS service for <strong>@service.Settings.Hostname</strong> means your BTCPay Server will stop updating the associated DNS record periodically." data-confirm-input="DELETE">Delete</a>
</td>
<td style="text-align:right"><a asp-action="DynamicDnsService" asp-route-hostname="@service.Settings.Hostname">Edit</a> <span> - </span> <a asp-action="DeleteDynamicDnsService" asp-route-hostname="@service.Settings.Hostname">Remove</a></td>
</tr>
}
</tbody>
</table>
</tbody>
</table>
}
else
{
<p class="text-secondary mt-3">
There are no dynamic DNS services yet.
</p>
}
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete dynamic DNS service", "Deleting the dynamic DNS service means your BTCPay Server will stop updating the associated DNS record periodically.", "Delete"))" />

View file

@ -14,14 +14,22 @@
<p>The recovering process is documented by LND on <a href="https://github.com/lightningnetwork/lnd/blob/master/docs/recovery.md" rel="noreferrer noopener">this page</a>.</p>
</div>
<a class="btn btn-primary @(Model.Removed ? "collapse" : "")" id="details" href="#">See confidential seed information</a>
<div class="form-group @(Model.Removed ? "" : "collapse")">
<div class="input-group">
<label asp-for="Seed" class="input-group-text"><span class="input-group-addon fa fa-eye"></span><span class="ms-2">Seed</span></label>
<textarea asp-for="Seed" onClick="this.select();" class="form-control" readonly rows="@(Model.Removed ? "1" : "3")"></textarea>
</div>
</div>
@if (!Model.Removed)
@if (Model.Removed)
{
<div class="alert alert-light d-flex align-items-center" role="alert">
<vc:icon symbol="warning" />
<span class="ms-3" id="Seed">@Model.Seed</span>
</div>
}
else
{
<div class="form-group @(Model.Removed ? "" : "collapse")">
<div class="input-group">
<label asp-for="Seed" class="input-group-text"><span class="input-group-addon fa fa-eye"></span><span class="ms-2">Seed</span></label>
<textarea asp-for="Seed" onClick="this.select();" class="form-control" readonly rows="3"></textarea>
</div>
</div>
<div class="form-group collapse">
<div class="input-group">
<label asp-for="WalletPassword" class="input-group-text"><span class="input-group-addon fa fa-lock"></span><span class="ms-2">Password</span></label>
@ -30,7 +38,7 @@
</div>
<div class="form-group collapse">
<form method="get" asp-action="RemoveLndSeed" asp-route-serviceName="@Context.GetRouteValue("serviceName")" asp-route-cryptoCode="@Context.GetRouteValue("cryptoCode")">
<button id="delete" class="btn btn-primary" type="submit">Remove Seed from server</button>
<button id="delete" class="btn btn-primary" type="submit" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-confirm-input="DELETE">Delete LND seed from server</button>
</form>
</div>
}
@ -38,8 +46,18 @@
</div>
}
@if (!Model.Removed)
{
<partial name="_Confirm" model="@(new ConfirmModel("Delete LND seed", "This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup.", "Delete"))"/>
}
@section PageFootContent {
<script>
const deleteButton = document.getElementById('delete')
deleteButton.addEventListener('click', event => {
event.preventDefault()
});
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("details").addEventListener("click", function () {
document.querySelectorAll(".form-group.collapse").forEach(el => el.classList.remove("collapse"));

View file

@ -68,13 +68,25 @@
<div class="col-md-8">
<div class="form-group">
<p>
<span>Increase the security of your instance by disabling the ability to change the SSH Settings in this BTCPay Server instance's user interface.<br /></span>
<span>Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.<br /></span>
</p>
</div>
<div>
<form method="post">
<button name="command" id="disable" type="submit" class="btn btn-outline-danger mb-5" value="disable">Disable</button>
<button name="command" id="disable" type="submit" class="btn btn-outline-danger mb-5" value="disable" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-confirm-input="DISABLE">Disable</button>
</form>
</div>
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Disable modification of SSH settings", "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.", "Disable"))"/>
@section PageFootContent {
<script>
const disableButton = document.getElementById('disable')
disableButton.dataset.action = window.location.href + '/disable'
disableButton.addEventListener('click', event => {
event.preventDefault()
})
</script>
}

View file

@ -2,41 +2,23 @@
@{
ViewData["Title"] = Model.Title;
Layout = null;
Layout = "_LayoutSimple";
}
<!DOCTYPE html>
<html lang="en">
<head>
<partial name="LayoutHead" />
</head>
<body class="bg-light">
<div class="modal-dialog modal-dialog-centered min-vh-100">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title w-100 text-center">@Model.Title</h4>
</div>
@section PageHeadContent {
<style>
body > .content-wrapper { display: flex; min-height: 100vh; }
.modal-dialog .btn-close { display: none; }
</style>
}
<div class="modal-body text-center">
@if (Model.DescriptionHtml)
{
@Safe.Raw(Model.Description)
}
else
{
@Model.Description
}
</div>
@section PageFootContent {
<script>
document.getElementById('ConfirmCancel').addEventListener('click', function () {
history.back();
return false;
})
</script>
}
@if (!string.IsNullOrEmpty(Model.Action))
{
<form method="post" class="modal-footer justify-content-center" action="@Model.ActionUrl" rel="noreferrer noopener">
<button type="submit" class="btn @Model.ButtonClass xmx-2" id="continue" style="min-width:25%;">@Model.Action</button>
<button type="submit" class="btn btn-secondary mx-2" onclick="history.back(); return false;" style="min-width:25%;">Go back</button>
</form>
}
</div>
</div>
<partial name="LayoutFoot" />
</body>
</html>
<partial name="ConfirmModal" model="Model" />

View file

@ -0,0 +1,39 @@
@model ConfirmModel
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="ConfirmTitle">@Model.Title</h4>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
<vc:icon symbol="close" />
</button>
</div>
<div class="modal-body">
<div id="ConfirmDescription">
@if (Model.DescriptionHtml)
{
@Safe.Raw(Model.Description)
}
else
{
@Model.Description
}
</div>
</div>
@if (!string.IsNullOrEmpty(Model.Action))
{
<form id="ConfirmForm" method="post" action="@Model.ActionUrl" rel="noreferrer noopener">
<div class="modal-body pt-0" id="ConfirmText" hidden>
<label for="ConfirmInput" class="form-label">Confirm the action by typing <strong id="ConfirmInputText"></strong>:</label>
<input id="ConfirmInput" class="form-control"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary only-for-js" data-bs-dismiss="modal" id="ConfirmCancel">Cancel</button>
<button type="submit" class="btn @Model.ButtonClass" id="ConfirmContinue">@Model.Action</button>
</div>
</form>
}
</div>
</div>

View file

@ -0,0 +1,41 @@
@model ConfirmModel
<div class="modal fade" id="ConfirmModal" tabindex="-1" aria-labelledby="ConfirmTitle" aria-hidden="true">
<partial name="ConfirmModal" model="Model" />
</div>
<script>
const modal = document.getElementById('ConfirmModal')
modal.addEventListener('show.bs.modal', event => {
const $target = event.relatedTarget
const $form = document.getElementById('ConfirmForm')
const $text = document.getElementById('ConfirmText')
const $title = document.getElementById('ConfirmTitle')
const $description = document.getElementById('ConfirmDescription')
const $input = document.getElementById('ConfirmInput')
const $inputText = document.getElementById('ConfirmInputText')
const $continue = document.getElementById('ConfirmContinue')
const { title, description, confirm, confirmInput } = $target.dataset
const action = $target.dataset.action || ($target.nodeName === 'A'
? $target.getAttribute('href')
: $target.form.getAttribute('action'))
if ($form) $form.setAttribute('action', action)
if (title) $title.textContent = title
if (description) $description.innerHTML = description
if (confirm) $continue.textContent = confirm
if (confirmInput) {
$text.removeAttribute('hidden')
$continue.setAttribute('disabled', 'disabled')
$inputText.textContent = confirmInput
$input.addEventListener('input', event => {
event.target.value.trim() === confirmInput
? $continue.removeAttribute('disabled')
: $continue.setAttribute('disabled', 'disabled')
})
} else {
$text.setAttribute('hidden', 'hidden')
$continue.removeAttribute('disabled')
}
});
</script>

View file

@ -46,7 +46,8 @@
<tr>
<td>@token.Label</td>
<td class="text-end">
<a asp-action="ShowToken" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id">See information</a> - <a asp-action="RevokeToken" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id">Revoke</a>
<a asp-action="ShowToken" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id">See information</a> -
<a asp-action="RevokeToken" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-tokenId="@token.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The access token with the label <strong>@token.Label</strong> will be revoked." data-confirm-input="REVOKE">Revoke</a>
</td>
</tr>
}
@ -83,3 +84,6 @@
</form>
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Revoke access token", "The access token will be revoked. Do you wish to continue?", "Revoke"))" />

View file

@ -47,9 +47,34 @@
</form>
<br>
<form method="get" asp-controller="Stores" asp-action="DeleteWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" class="mt-5">
<a asp-controller="Stores" asp-action="ReplaceWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="ChangeWalletLink" class="btn btn-secondary me-2">
<a asp-controller="Stores" asp-action="ReplaceWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode"
id="ChangeWalletLink"
class="btn btn-secondary me-2"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-title="Replace @Model.CryptoCode wallet"
data-description="@ViewData["ReplaceDescription"]"
data-confirm="Setup new wallet"
data-confirm-input="REPLACE">
Replace wallet
</a>
<button type="submit" class="btn btn-danger" id="Delete">Remove wallet</button>
<button type="submit" class="btn btn-danger" id="Delete"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-title="Remove @Model.CryptoCode wallet"
data-description="@ViewData["RemoveDescription"]"
data-confirm="Remove"
data-confirm-input="REMOVE">Remove wallet</button>
</form>
</div>
<partial name="_Confirm" model="@(new ConfirmModel($"{Model.CryptoCode} wallet", "Change", "Update"))" />
@section PageFootContent {
<script>
const deleteButton = document.getElementById('Delete')
deleteButton.addEventListener('click', event => {
event.preventDefault()
});
</script>
}

View file

@ -62,20 +62,20 @@
<h5>Test results:</h5>
<table class="table table-hover table-responsive-md">
<tbody>
@foreach (var result in Model.TestRateRules)
{
<tr>
@if (result.Error)
{
<th class="small"><span class="text-danger fa fa-times"></span> @result.CurrencyPair</th>
}
else
{
<th class="small"><span class="text-success fa fa-check"></span> @result.CurrencyPair</th>
}
<td class="small">@result.Rule</td>
</tr>
}
@foreach (var result in Model.TestRateRules)
{
<tr>
@if (result.Error)
{
<th class="small"><span class="text-danger fa fa-times"></span> @result.CurrencyPair</th>
}
else
{
<th class="small"><span class="text-success fa fa-check"></span> @result.CurrencyPair</th>
}
<td class="small">@result.Rule</td>
</tr>
}
</tbody>
</table>
</div>
@ -147,7 +147,7 @@
</p>
</div>
<p>
<button type="submit" class="btn btn-secondary" value="scripting-off" name="command">Turn off advanced rate rule scripting</button>
<button type="submit" class="btn btn-secondary" value="scripting-off" name="command" data-bs-toggle="modal" data-bs-target="#ConfirmModal">Turn off advanced rate rule scripting</button>
</p>
}
else
@ -161,7 +161,7 @@
</p>
</div>
<p>
<button type="submit" class="btn btn-secondary" value="scripting-on" name="command">Turn on advanced rate rule scripting</button>
<button type="submit" class="btn btn-secondary" value="scripting-on" name="command" data-bs-toggle="modal" data-bs-target="#ConfirmModal">Turn on advanced rate rule scripting</button>
</p>
}
<div class="form-group">
@ -191,9 +191,17 @@
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Rate rule scripting", Model.ShowScripting ? "This action will delete your rate script. Are you sure to turn off rate rules scripting?" : "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)", "Continue", Model.ShowScripting ? "btn-danger" : "btn-primary"))" />
@section PageFootContent {
<script>
var defaultScript = @Safe.Json(Model.DefaultScript);
const defaultScript = @Safe.Json(Model.DefaultScript);
const commandButton = document.querySelector('[data-bs-target="#ConfirmModal"]')
commandButton.dataset.action = window.location.href + '/confirm?scripting=@(!Model.ShowScripting)'
commandButton.addEventListener('click', event => {
event.preventDefault()
});
</script>
<partial name="_ValidationScriptsPartial" />
<partial name="_ValidationScriptsPartial"/>
}

View file

@ -51,7 +51,7 @@
<td>@user.Email</td>
<td>@user.Role</td>
<td style="text-align:right">
<a asp-action="DeleteStoreUser" asp-route-storeId="@Model.StoreId" asp-route-userId="@user.Id">Remove</a>
<a asp-action="DeleteStoreUser" asp-route-storeId="@Model.StoreId" asp-route-userId="@user.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="This action will prevent <strong>@user.Email</strong> from accessing this store and its settings." data-confirm-input="REMOVE">Remove</a>
</td>
</tr>
}
@ -60,3 +60,10 @@
</div>
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Remove store user", "This action will prevent the user from accessing this store and its settings. Are you sure?", "Delete"))" />
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View file

@ -308,12 +308,14 @@
See more actions
</button>
<div id="danger-zone" class="collapse">
<a id="delete-store" class="btn btn-outline-danger mb-5" asp-action="DeleteStore" asp-route-storeId="@Model.Id">Delete this store</a>
<a id="delete-store" class="btn btn-outline-danger mb-5" asp-action="DeleteStore" asp-route-storeId="@Model.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@Model.StoreName</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.">Delete this store</a>
</div>
}
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.", "Delete"))" />
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View file

@ -17,11 +17,11 @@
{
<table class="table table-hover table-responsive-md">
<thead>
<tr>
<th>Status</th>
<th>Url</th>
<th class="text-end">Actions</th>
</tr>
<tr>
<th>Status</th>
<th>Url</th>
<th class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var wh in Model.Webhooks)
@ -48,14 +48,16 @@
</td>
<td class="d-block text-break">@wh.Url</td>
<td class="text-end text-md-nowrap">
<a asp-action="TestWebhook" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id">Test</a> -
<a asp-action="ModifyWebhook" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id">Modify</a> -
<a asp-action="DeleteWebhook" asp-route-storeId="@this.Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id">Delete</a>
<a asp-action="TestWebhook" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id">Test</a> -
<a asp-action="ModifyWebhook" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id">Modify</a> -
<a asp-action="DeleteWebhook" asp-route-storeId="@Context.GetRouteValue("storeId")" asp-route-webhookId="@wh.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-confirm-input="DELETE">Delete</a>
</td>
</tr>
}
</tbody>
</table>
<partial name="_Confirm" model="@(new ConfirmModel("Delete webhook", "This webhook will be removed from this store.", "Delete"))" />
}
else
{
@ -67,3 +69,4 @@ else
@section PageFootContent {
<partial name="_ValidationScriptsPartial" />
}

View file

@ -76,7 +76,7 @@
{
<a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@store.Id" id="update-store-@store.Id">Settings</a><span> - </span>
}
<a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id">Remove</a>
<a asp-action="DeleteStore" asp-controller="Stores" asp-route-storeId="@store.Id" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The store <strong>@store.Name</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store." data-confirm-input="DELETE">Delete</a>
</td>
</tr>
}
@ -93,3 +93,5 @@
</div>
</div>
</section>
<partial name="_Confirm" model="@(new ConfirmModel("Delete store", "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.", "Delete"))" />

View file

@ -55,47 +55,52 @@
<div class="col-md-12">
<table class="table table-hover table-responsive-lg">
<thead class="thead-inverse">
<tr>
<th scope="col">Start</th>
<th scope="col">Name</th>
<th scope="col">Refunded</th>
<th scope="col" class="text-end">Actions</th>
</tr>
<tr>
<th scope="col">Start</th>
<th scope="col">Name</th>
<th scope="col">Refunded</th>
<th scope="col" class="text-end">Actions</th>
</tr>
</thead>
<tbody>
@foreach (var pp in Model.PullPayments)
{
<tr>
<td>@pp.StartDate.ToBrowserDate()</td>
<td>@pp.Name</td>
<td class="align-middle">
<div class="progress ppProgress" data-pp="@pp.Id" data-bs-toggle="tooltip" data-bs-html="true">
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.CompletedPercent"
aria-valuemin="0" aria-valuemax="100" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.CompletedPercent)%;">
</div>
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.AwaitingPercent"
aria-valuemin="0" aria-valuemax="100" style="background-color:orange; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.AwaitingPercent)%;">
</div>
@foreach (var pp in Model.PullPayments)
{
<tr>
<td>@pp.StartDate.ToBrowserDate()</td>
<td>@pp.Name</td>
<td class="align-middle">
<div class="progress ppProgress" data-pp="@pp.Id" data-bs-toggle="tooltip" data-bs-html="true">
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.CompletedPercent"
aria-valuemin="0" aria-valuemax="100" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.CompletedPercent)%;">
</div>
</td>
<td class="text-end">
<a asp-action="ViewPullPayment"
asp-controller="PullPayment"
asp-route-pullPaymentId="@pp.Id">View</a> -
<a class="pp-payout" asp-action="Payouts"
asp-route-walletId="@Context.GetRouteValue("walletId")"
asp-route-pullPaymentId="@pp.Id">Payouts</a> -
<a asp-action="ArchivePullPayment"
asp-route-walletId="@Context.GetRouteValue("walletId")"
asp-route-pullPaymentId="@pp.Id">Archive</a>
</td>
</tr>
}
<div class="progress-bar" role="progressbar" aria-valuenow="@pp.Progress.AwaitingPercent"
aria-valuemin="0" aria-valuemax="100" style="background-color:orange; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width:@(pp.Progress.AwaitingPercent)%;">
</div>
</div>
</td>
<td class="text-end">
<a asp-action="ViewPullPayment"
asp-controller="PullPayment"
asp-route-pullPaymentId="@pp.Id">View</a> -
<a class="pp-payout" asp-action="Payouts"
asp-route-walletId="@Context.GetRouteValue("walletId")"
asp-route-pullPaymentId="@pp.Id">Payouts</a> -
<a asp-action="ArchivePullPayment"
asp-route-walletId="@Context.GetRouteValue("walletId")"
asp-route-pullPaymentId="@pp.Id"
data-bs-toggle="modal"
data-bs-target="#ConfirmModal"
data-description="Do you really want to archive the pull payment <strong>@pp.Name</strong>?">Archive</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<partial name="_Confirm" model="@(new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"))" />
@section PageFootContent {
<script>
var ppProgresses = document.getElementsByClassName("ppProgress");

View file

@ -5286,8 +5286,8 @@ fieldset:disabled .btn {
}
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out;
transform: translate(0, -50px);
transition: transform 0.3s cubic-bezier(0.175, 0.855, 0.32, 1.275);
transform: translate(0, -0.5rem);
}
.modal.show .modal-dialog {
@ -5346,7 +5346,7 @@ fieldset:disabled .btn {
}
.modal-backdrop.show {
opacity: 0.5;
opacity: 0.85;
}
.modal-header {
@ -6081,7 +6081,7 @@ fieldset:disabled .btn {
}
.offcanvas-backdrop.show {
opacity: 0.5;
opacity: 0.85;
}
.offcanvas-header {

View file

@ -251,6 +251,12 @@ h2 small .fa-question-circle-o {
color: inherit;
}
.list-group-item .icon-caret-right {
flex: 0 0 24px;
height: 24px;
align-self: center;
}
.account-form {
max-width: 36em;
}

View file

@ -26,7 +26,7 @@ body {
#wizard-navbar a {
position: relative;
color: var(--btcpay-body-color);
color: var(--btcpay-body-text);
display: inline-flex;
justify-content: center;
align-items: center;
@ -54,7 +54,7 @@ body {
}
#wizard-navbar a:hover::after {
background-color: var(--btcpay-border-color-medium);
background-color: var(--btcpay-body-bg-hover);
}
#wizard-navbar a:active::after {
@ -108,8 +108,5 @@ body {
}
.list-group-item .icon-caret-right {
flex: 0 0 24px;
height: 24px;
align-self: center;
margin-right: 1.5rem;
}