mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Sign in with other device (quick mobile login) (#2504)
Co-authored-by: Dennis Reimann <mail@dennisreimann.de> Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
@@ -101,6 +101,17 @@ namespace BTCPayServer.Tests
|
|||||||
{
|
{
|
||||||
driver.ExecuteJavaScript($"document.getElementById('{collapseId}').classList.add('show')");
|
driver.ExecuteJavaScript($"document.getElementById('{collapseId}').classList.add('show')");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static void SetAttribute(this IWebDriver driver, string element, string attribute, string value)
|
||||||
|
{
|
||||||
|
driver.ExecuteJavaScript($"document.getElementById('{element}').setAttribute('{attribute}', '{value}')");
|
||||||
|
}
|
||||||
|
public static void InvokeJSFunction(this IWebDriver driver, string element, string funcName)
|
||||||
|
{
|
||||||
|
driver.ExecuteJavaScript($"document.getElementById('{element}').{funcName}()");
|
||||||
|
}
|
||||||
|
|
||||||
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
public static IWebElement WaitForElement(this IWebDriver driver, By selector)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using BTCPayServer.Payments;
|
|||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
using BTCPayServer.Services.Invoices;
|
using BTCPayServer.Services.Invoices;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
using BTCPayServer.Views.Manage;
|
||||||
using BTCPayServer.Views.Server;
|
using BTCPayServer.Views.Server;
|
||||||
using BTCPayServer.Views.Stores;
|
using BTCPayServer.Views.Stores;
|
||||||
using BTCPayServer.Views.Wallets;
|
using BTCPayServer.Views.Wallets;
|
||||||
@@ -1555,6 +1556,31 @@ namespace BTCPayServer.Tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
[Trait("Selenium", "Selenium")]
|
||||||
|
public async Task CanSigninWithLoginCode()
|
||||||
|
{
|
||||||
|
using var s = CreateSeleniumTester();
|
||||||
|
await s.StartAsync();
|
||||||
|
var user = s.RegisterNewUser();
|
||||||
|
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||||
|
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||||
|
s.Driver.FindElement(By.Id("regeneratecode")).Click();
|
||||||
|
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
|
||||||
|
|
||||||
|
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||||
|
s.Logout();
|
||||||
|
s.GoToLogin();
|
||||||
|
s.Driver.SetAttribute("LoginCode", "value", "bad code");
|
||||||
|
s.Driver.InvokeJSFunction("logincode-form", "submit");
|
||||||
|
|
||||||
|
|
||||||
|
s.Driver.SetAttribute("LoginCode", "value", code);
|
||||||
|
s.Driver.InvokeJSFunction("logincode-form", "submit");
|
||||||
|
s.GoToProfile();
|
||||||
|
Assert.Contains(user, s.Driver.PageSource);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// For god know why, selenium have problems clicking on the save button, resulting in ultimate hacks
|
// For god know why, selenium have problems clicking on the save button, resulting in ultimate hacks
|
||||||
// to make it works.
|
// to make it works.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
|
||||||
<Import Project="../Build/Common.csproj" />
|
<Import Project="../Build/Common.csproj" />
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
<Content Update="Views\StorePullPayments\PullPayments.cshtml">
|
<Content Update="Views\StorePullPayments\PullPayments.cshtml">
|
||||||
<Pack>false</Pack>
|
<Pack>false</Pack>
|
||||||
</Content>
|
</Content>
|
||||||
<Content Update="Views\StorePullPayments\Payouts.cshtml">
|
<Content Update="Views\Account\_ViewImports.cshtml">
|
||||||
<Pack>false</Pack>
|
<Pack>false</Pack>
|
||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Abstractions.Constants;
|
using BTCPayServer.Abstractions.Constants;
|
||||||
using BTCPayServer.Abstractions.Extensions;
|
using BTCPayServer.Abstractions.Extensions;
|
||||||
@@ -33,6 +34,7 @@ namespace BTCPayServer.Controllers
|
|||||||
readonly Configuration.BTCPayServerOptions _Options;
|
readonly Configuration.BTCPayServerOptions _Options;
|
||||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||||
private readonly Fido2Service _fido2Service;
|
private readonly Fido2Service _fido2Service;
|
||||||
|
private readonly UserLoginCodeService _userLoginCodeService;
|
||||||
private readonly EventAggregator _eventAggregator;
|
private readonly EventAggregator _eventAggregator;
|
||||||
readonly ILogger _logger;
|
readonly ILogger _logger;
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ namespace BTCPayServer.Controllers
|
|||||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||||
EventAggregator eventAggregator,
|
EventAggregator eventAggregator,
|
||||||
Fido2Service fido2Service,
|
Fido2Service fido2Service,
|
||||||
|
UserLoginCodeService userLoginCodeService,
|
||||||
Logs logs)
|
Logs logs)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@@ -56,6 +59,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_Options = options;
|
_Options = options;
|
||||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||||
_fido2Service = fido2Service;
|
_fido2Service = fido2Service;
|
||||||
|
_userLoginCodeService = userLoginCodeService;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_logger = logs.PayServer;
|
_logger = logs.PayServer;
|
||||||
Logs = logs;
|
Logs = logs;
|
||||||
@@ -73,7 +77,6 @@ namespace BTCPayServer.Controllers
|
|||||||
[Route("~/Account/Login", Order = 2)]
|
[Route("~/Account/Login", Order = 2)]
|
||||||
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||||
return RedirectToLocal();
|
return RedirectToLocal();
|
||||||
// Clear the existing external cookie to ensure a clean login process
|
// Clear the existing external cookie to ensure a clean login process
|
||||||
@@ -85,13 +88,36 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
ViewData["ReturnUrl"] = returnUrl;
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
return View(new LoginViewModel()
|
return View(nameof(Login), new LoginViewModel() { Email = email });
|
||||||
{
|
|
||||||
Email = email
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[Route("~/login/code", Order = 1)]
|
||||||
|
[ValidateAntiForgeryToken]
|
||||||
|
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||||
|
|
||||||
|
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(loginCode))
|
||||||
|
{
|
||||||
|
var userId = _userLoginCodeService.Verify(loginCode);
|
||||||
|
if (userId is null)
|
||||||
|
{
|
||||||
|
ModelState.AddModelError(string.Empty,
|
||||||
|
"Login code was invalid");
|
||||||
|
return await Login(returnUrl, null);
|
||||||
|
}
|
||||||
|
var user = await _userManager.FindByIdAsync(userId);
|
||||||
|
|
||||||
|
_logger.LogInformation("User with ID {UserId} logged in with a login code.", user.Id);
|
||||||
|
await _signInManager.SignInAsync(user, false, "LoginCode");
|
||||||
|
return RedirectToLocal(returnUrl);
|
||||||
|
}
|
||||||
|
return await Login(returnUrl, null);
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[Route("~/login", Order = 1)]
|
[Route("~/login", Order = 1)]
|
||||||
@@ -104,6 +130,7 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
return RedirectToAction("Login");
|
return RedirectToAction("Login");
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewData["ReturnUrl"] = returnUrl;
|
ViewData["ReturnUrl"] = returnUrl;
|
||||||
if (ModelState.IsValid)
|
if (ModelState.IsValid)
|
||||||
{
|
{
|
||||||
@@ -123,7 +150,7 @@ namespace BTCPayServer.Controllers
|
|||||||
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
|
||||||
return View(model);
|
return View(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
var fido2Devices = await _fido2Service.HasCredentials(user.Id);
|
||||||
if (!await _userManager.IsLockedOutAsync(user) && fido2Devices)
|
if (!await _userManager.IsLockedOutAsync(user) && fido2Devices)
|
||||||
{
|
{
|
||||||
|
|||||||
21
BTCPayServer/Controllers/ManageController.LoginCodes.cs
Normal file
21
BTCPayServer/Controllers/ManageController.LoginCodes.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
public partial class ManageController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> LoginCodes()
|
||||||
|
{
|
||||||
|
var user = await _userManager.GetUserAsync(User);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return View(nameof(LoginCodes), _userLoginCodeService.GetOrGenerate(user.Id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,7 @@ namespace BTCPayServer.Controllers
|
|||||||
private readonly IAuthorizationService _authorizationService;
|
private readonly IAuthorizationService _authorizationService;
|
||||||
private readonly Fido2Service _fido2Service;
|
private readonly Fido2Service _fido2Service;
|
||||||
private readonly LinkGenerator _linkGenerator;
|
private readonly LinkGenerator _linkGenerator;
|
||||||
|
private readonly UserLoginCodeService _userLoginCodeService;
|
||||||
private readonly UserService _userService;
|
private readonly UserService _userService;
|
||||||
readonly StoreRepository _StoreRepository;
|
readonly StoreRepository _StoreRepository;
|
||||||
|
|
||||||
@@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
|
|||||||
IAuthorizationService authorizationService,
|
IAuthorizationService authorizationService,
|
||||||
Fido2Service fido2Service,
|
Fido2Service fido2Service,
|
||||||
LinkGenerator linkGenerator,
|
LinkGenerator linkGenerator,
|
||||||
UserService userService
|
UserService userService,
|
||||||
|
UserLoginCodeService userLoginCodeService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
@@ -63,6 +65,7 @@ namespace BTCPayServer.Controllers
|
|||||||
_authorizationService = authorizationService;
|
_authorizationService = authorizationService;
|
||||||
_fido2Service = fido2Service;
|
_fido2Service = fido2Service;
|
||||||
_linkGenerator = linkGenerator;
|
_linkGenerator = linkGenerator;
|
||||||
|
_userLoginCodeService = userLoginCodeService;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_StoreRepository = storeRepository;
|
_StoreRepository = storeRepository;
|
||||||
}
|
}
|
||||||
|
|||||||
50
BTCPayServer/Fido2/UserLoginCodeService.cs
Normal file
50
BTCPayServer/Fido2/UserLoginCodeService.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBitcoin.DataEncoders;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Fido2
|
||||||
|
{
|
||||||
|
public class UserLoginCodeService
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _memoryCache;
|
||||||
|
|
||||||
|
public UserLoginCodeService(IMemoryCache memoryCache)
|
||||||
|
{
|
||||||
|
_memoryCache = memoryCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCacheKey(string userId)
|
||||||
|
{
|
||||||
|
return $"{nameof(UserLoginCodeService)}_{userId.ToLowerInvariant()}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetOrGenerate(string userId)
|
||||||
|
{
|
||||||
|
var key = GetCacheKey(userId);
|
||||||
|
if (_memoryCache.TryGetValue(key, out var code))
|
||||||
|
{
|
||||||
|
_memoryCache.Remove(code);
|
||||||
|
_memoryCache.Remove(key);
|
||||||
|
}
|
||||||
|
return _memoryCache.GetOrCreate(GetCacheKey(userId), entry =>
|
||||||
|
{
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||||
|
var code = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20));
|
||||||
|
using var newEntry = _memoryCache.CreateEntry(code);
|
||||||
|
newEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||||
|
newEntry.Value = userId;
|
||||||
|
|
||||||
|
return code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
public string Verify(string code)
|
||||||
|
{
|
||||||
|
if (!_memoryCache.TryGetValue(code, out var userId)) return null;
|
||||||
|
_memoryCache.Remove(GetCacheKey((string)userId));
|
||||||
|
_memoryCache.Remove(code);
|
||||||
|
return (string)userId;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,6 +108,7 @@ namespace BTCPayServer.Hosting
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
services.AddScoped<Fido2Service>();
|
services.AddScoped<Fido2Service>();
|
||||||
|
services.AddSingleton<UserLoginCodeService>();
|
||||||
|
|
||||||
var mvcBuilder= services.AddMvc(o =>
|
var mvcBuilder= services.AddMvc(o =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace BTCPayServer.Models.AccountViewModels
|
|||||||
[Required]
|
[Required]
|
||||||
[DataType(DataType.Password)]
|
[DataType(DataType.Password)]
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
public string LoginCode { get; set; }
|
||||||
|
|
||||||
[Display(Name = "Remember me?")]
|
[Display(Name = "Remember me?")]
|
||||||
public bool RememberMe { get; set; }
|
public bool RememberMe { get; set; }
|
||||||
|
|||||||
@@ -11,5 +11,7 @@ namespace BTCPayServer.Models.ManageViewModels
|
|||||||
public bool Is2faEnabled { get; set; }
|
public bool Is2faEnabled { get; set; }
|
||||||
|
|
||||||
public List<Fido2Credential> Credentials { get; set; }
|
public List<Fido2Credential> Credentials { get; set; }
|
||||||
|
|
||||||
|
public string LoginCode { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,6 @@
|
|||||||
Layout = "_LayoutSimple";
|
Layout = "_LayoutSimple";
|
||||||
}
|
}
|
||||||
|
|
||||||
@section PageFootContent {
|
|
||||||
<partial name="_ValidationScriptsPartial" />
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row justify-content-center mb-2">
|
<div class="row justify-content-center mb-2">
|
||||||
<div class="col text-center">
|
<div class="col text-center">
|
||||||
<a asp-controller="Home" asp-action="Index" tabindex="-1">
|
<a asp-controller="Home" asp-action="Index" tabindex="-1">
|
||||||
@@ -19,7 +15,7 @@
|
|||||||
|
|
||||||
<h1 class="h2 mb-3">Welcome to your BTCPay Server</h1>
|
<h1 class="h2 mb-3">Welcome to your BTCPay Server</h1>
|
||||||
|
|
||||||
<partial name="_StatusMessage" />
|
<partial name="_StatusMessage"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -30,12 +26,12 @@
|
|||||||
<h4 class="modal-title">Sign In</h4>
|
<h4 class="modal-title">Sign In</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
|
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" id="login-form" asp-action="Login">
|
||||||
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)" >
|
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)">
|
||||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="Email" class="form-label"></label>
|
<label asp-for="Email" class="form-label"></label>
|
||||||
<input asp-for="Email" class="form-control" required tabindex="1" autofocus />
|
<input asp-for="Email" class="form-control" required tabindex="1" autofocus/>
|
||||||
<span asp-validation-for="Email" class="text-danger"></span>
|
<span asp-validation-for="Email" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -43,14 +39,22 @@
|
|||||||
<label asp-for="Password" class="form-label"></label>
|
<label asp-for="Password" class="form-label"></label>
|
||||||
<a asp-action="ForgotPassword" tabindex="5">Forgot password?</a>
|
<a asp-action="ForgotPassword" tabindex="5">Forgot password?</a>
|
||||||
</div>
|
</div>
|
||||||
<input asp-for="Password" class="form-control" required tabindex="2" />
|
<div class="input-group d-flex">
|
||||||
|
<input asp-for="Password" class="form-control" required tabindex="2"/>
|
||||||
|
</div>
|
||||||
<span asp-validation-for="Password" class="text-danger"></span>
|
<span asp-validation-for="Password" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mt-4">
|
<div class="form-group mt-4">
|
||||||
<button type="submit" class="btn btn-primary btn-lg w-100" id="LoginButton" tabindex="3">Sign in</button>
|
<div class="btn-group w-100">
|
||||||
|
<button type="submit" class="btn btn-primary btn-lg w-100" id="LoginButton" tabindex="3"><span class="ps-3">Sign in</span></button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-lg w-auto only-for-js" data-bs-toggle="modal" data-bs-target="#scanModal" title="Scan Login code with camera" tabindex="4"><i class="fa fa-camera"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
<form asp-action="LoginWithCode" asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" id="logincode-form">
|
||||||
|
<input asp-for="LoginCode" type="hidden" class="form-control"/>
|
||||||
|
</form>
|
||||||
@if (!(await _settingsRepository.GetPolicies()).LockSubscription)
|
@if (!(await _settingsRepository.GetPolicies()).LockSubscription)
|
||||||
{
|
{
|
||||||
<p class="text-center mt-2 mb-0">
|
<p class="text-center mt-2 mb-0">
|
||||||
@@ -67,3 +71,20 @@
|
|||||||
<partial name="_BTCPaySupporters"/>
|
<partial name="_BTCPaySupporters"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<partial name="CameraScanner"/>
|
||||||
|
|
||||||
|
@section PageFootContent {
|
||||||
|
<partial name="_ValidationScriptsPartial"/>
|
||||||
|
|
||||||
|
<bundle name="wwwroot/bundles/camera-bundle.min.js"></bundle>
|
||||||
|
<link href="~/vendor/vue-qrcode-reader/vue-qrcode-reader.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("load", async () => {
|
||||||
|
initCameraScanningApp("Scan login code", data => {
|
||||||
|
document.getElementById("LoginCode").value = data;
|
||||||
|
document.getElementById("logincode-form").submit();
|
||||||
|
}, "scanModal");
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|||||||
3
BTCPayServer/Views/Account/_ViewImports.cshtml
Normal file
3
BTCPayServer/Views/Account/_ViewImports.cshtml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@using BTCPayServer.Views.Wallets
|
||||||
|
@using BTCPayServer.Models.WalletViewModels
|
||||||
|
@addTagHelper *, BundlerMinifier.TagHelpers
|
||||||
37
BTCPayServer/Views/Manage/LoginCodes.cshtml
Normal file
37
BTCPayServer/Views/Manage/LoginCodes.cshtml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
@model string
|
||||||
|
@{
|
||||||
|
ViewData.SetActivePageAndTitle(ManageNavPages.LoginCodes, "Login codes");
|
||||||
|
}
|
||||||
|
<h2 class="mb-4">@ViewData["Title"]</h2>
|
||||||
|
<p>Easily log into BTCPay Server on another device using a simple login code from an already authenticated device.</p>
|
||||||
|
<div class="d-inline-flex flex-column qr-container" style="width:256px">
|
||||||
|
<vc:qr-code data="@Model" />
|
||||||
|
<input type="hidden" value="@Model" id="logincode">
|
||||||
|
<p class="text-center text-muted mb-1" id="progress">Valid for 60 seconds</p>
|
||||||
|
<div class="progress only-for-js" data-bs-toggle="tooltip" data-bs-placement="top">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width:100%" id="progressbar"></div>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-secondary mt-3" id="regeneratecode" asp-action="LoginCodes">Regenerate code</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@section PageFootContent
|
||||||
|
{
|
||||||
|
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
|
||||||
|
<script src="~/js/copy-to-clipboard.js"></script>
|
||||||
|
<script>
|
||||||
|
const SECONDS = 60
|
||||||
|
const progress = document.getElementById('progress')
|
||||||
|
const progressbar = document.getElementById('progressbar')
|
||||||
|
let remaining = SECONDS
|
||||||
|
const update = () => {
|
||||||
|
remaining--
|
||||||
|
const percent = Math.round(remaining/SECONDS * 100)
|
||||||
|
progress.innerText = `Valid for ${remaining} seconds`
|
||||||
|
progressbar.style.width = `${percent}%`
|
||||||
|
if (percent < 15) progressbar.classList.add('bg-warning')
|
||||||
|
if (percent < 1) document.getElementById('regeneratecode').click()
|
||||||
|
}
|
||||||
|
setInterval(update, 1000)
|
||||||
|
update()
|
||||||
|
</script>
|
||||||
|
}
|
||||||
@@ -2,6 +2,6 @@ namespace BTCPayServer.Views.Manage
|
|||||||
{
|
{
|
||||||
public enum ManageNavPages
|
public enum ManageNavPages
|
||||||
{
|
{
|
||||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2
|
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2, LoginCodes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<p>
|
<p>
|
||||||
Two-Factor Authentication (2FA) is an additional measure to protect your account.
|
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.
|
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)
|
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).
|
or a security device (like a Yubikey or your hardware wallet supporting FIDO2).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -57,28 +57,28 @@
|
|||||||
<h5>Disable 2FA</h5>
|
<h5>Disable 2FA</h5>
|
||||||
<p class="mb-0 me-3">Re-enabling will not require you to reconfigure your app.</p>
|
<p class="mb-0 me-3">Re-enabling will not require you to reconfigure your app.</p>
|
||||||
</div>
|
</div>
|
||||||
<vc:icon symbol="caret-right" />
|
<vc:icon symbol="caret-right"/>
|
||||||
</a>
|
</a>
|
||||||
<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" data-confirm-input="RESET">
|
<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" data-confirm-input="RESET">
|
||||||
<div>
|
<div>
|
||||||
<h5>Reset recovery codes</h5>
|
<h5>Reset recovery codes</h5>
|
||||||
<p class="mb-0 me-3">Regenerate your 2FA recovery codes.</p>
|
<p class="mb-0 me-3">Regenerate your 2FA recovery codes.</p>
|
||||||
</div>
|
</div>
|
||||||
<vc:icon symbol="caret-right" />
|
<vc:icon symbol="caret-right"/>
|
||||||
</a>
|
</a>
|
||||||
<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" data-confirm-input="RESET">
|
<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" data-confirm-input="RESET">
|
||||||
<div>
|
<div>
|
||||||
<h5>Reset app</h5>
|
<h5>Reset app</h5>
|
||||||
<p class="mb-0 me-3">Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.</p>
|
<p class="mb-0 me-3">Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.</p>
|
||||||
</div>
|
</div>
|
||||||
<vc:icon symbol="caret-right" />
|
<vc:icon symbol="caret-right"/>
|
||||||
</a>
|
</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="EnableAuthenticator" class="list-group-item d-flex justify-content-between align-items-center list-group-item-action py-3">
|
||||||
<div>
|
<div>
|
||||||
<h5>Configure app</h5>
|
<h5>Configure app</h5>
|
||||||
<p class="mb-0 me-3">Display the key or QR code to configure an authenticator app with your current setup.</p>
|
<p class="mb-0 me-3">Display the key or QR code to configure an authenticator app with your current setup.</p>
|
||||||
</div>
|
</div>
|
||||||
<vc:icon symbol="caret-right" />
|
<vc:icon symbol="caret-right"/>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
<h5>Enable 2FA</h5>
|
<h5>Enable 2FA</h5>
|
||||||
<p class="mb-0 me-3">Using apps such as Google or Microsoft Authenticator.</p>
|
<p class="mb-0 me-3">Using apps such as Google or Microsoft Authenticator.</p>
|
||||||
</div>
|
</div>
|
||||||
<vc:icon symbol="caret-right" />
|
<vc:icon symbol="caret-right"/>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
|
<input type="text" class="form-control" name="Name" placeholder="Security device name"/>
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="fa fa-plus"></span>
|
<span class="fa fa-plus"></span>
|
||||||
Add
|
Add
|
||||||
<span class="d-none d-md-inline-block">security device</span>
|
<span class="d-none d-md-inline-block">security device</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,4 +122,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<partial name="_Confirm" model="@(new ConfirmModel("Two-Factor Authentication", "Placeholder", "Placeholder"))" />
|
<partial name="_Confirm" model="@(new ConfirmModel("Two-Factor Authentication", "Placeholder", "Placeholder"))"/>
|
||||||
|
|||||||
@@ -5,5 +5,6 @@
|
|||||||
<a id="SectionNav-@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a>
|
<a id="SectionNav-@ManageNavPages.TwoFactorAuthentication.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.TwoFactorAuthentication)" asp-controller="Manage" asp-action="TwoFactorAuthentication">Two-Factor Authentication</a>
|
||||||
<a id="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a>
|
<a id="SectionNav-@ManageNavPages.APIKeys.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.APIKeys)" asp-controller="Manage" asp-action="APIKeys">API Keys</a>
|
||||||
<a id="SectionNav-@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
|
<a id="SectionNav-@ManageNavPages.Notifications.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.Notifications)" asp-controller="Manage" asp-action="NotificationSettings">Notifications</a>
|
||||||
|
<a id="SectionNav-@ManageNavPages.LoginCodes.ToString()" class="nav-link @ViewData.IsActivePage(ManageNavPages.LoginCodes)" asp-controller="Manage" asp-action="LoginCodes">Login Codes</a>
|
||||||
<vc:ui-extension-point location="user-nav" model="@Model"/>
|
<vc:ui-extension-point location="user-nav" model="@Model"/>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -39,8 +39,8 @@
|
|||||||
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="cameraOff? 'off': cameras[camera]" :device-id="cameras[camera]"/>
|
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="cameraOff? 'off': cameras[camera]" :device-id="cameras[camera]"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isLoaded && requestInput && cameras.length >2" class="d-flex justify-content-center align-items-center">
|
<div v-if="isLoaded && requestInput && cameras.length > 2" class="d-flex justify-content-center align-items-center mt-3">
|
||||||
<button class="btn btn-link text-center" v-on:click="nextCamera">Switch camera</button>
|
<button class="btn btn-secondary text-center" v-on:click="nextCamera">Switch camera</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="qrData || errorMessage">
|
<div v-else-if="qrData || errorMessage">
|
||||||
<div v-if="errorMessage" class="alert alert-danger" role="alert">
|
<div v-if="errorMessage" class="alert alert-danger" role="alert">
|
||||||
@@ -107,13 +107,12 @@ function initCameraScanningApp(title, onDataSubmit, modalId) {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
nextCamera: function (){
|
nextCamera: function (){
|
||||||
if (this.camera == 0){
|
if (this.camera === 0){
|
||||||
|
this.camera++;
|
||||||
|
} else if (this.camera == this.cameras.length -1) {
|
||||||
|
this.camera = 0;
|
||||||
|
} else {
|
||||||
this.camera++;
|
this.camera++;
|
||||||
}else if(this.camera == this.cameras.length -1){
|
|
||||||
|
|
||||||
this.camera = 1;
|
|
||||||
}else{
|
|
||||||
this.camera ++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user