mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-10 00:09:18 +01:00
POS: Option for user sign in via the QR code (#6231)
* Login Code: Turn into Blazor component and extend with data for the app * POS: Add login code for POS frontend * Improve components, fix test
This commit is contained in:
parent
b5590a38fe
commit
272cc3d3c9
14 changed files with 253 additions and 75 deletions
|
@ -3426,11 +3426,14 @@ namespace BTCPayServer.Tests
|
|||
var user = s.RegisterNewUser();
|
||||
s.GoToHome();
|
||||
s.GoToProfile(ManageNavPages.LoginCodes);
|
||||
var code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
s.ClickPagePrimary();
|
||||
Assert.NotEqual(code, s.Driver.FindElement(By.Id("logincode")).GetAttribute("value"));
|
||||
|
||||
code = s.Driver.FindElement(By.Id("logincode")).GetAttribute("value");
|
||||
string code = null;
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
string prevCode = code;
|
||||
await s.Driver.Navigate().RefreshAsync();
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
Assert.NotEqual(prevCode, code);
|
||||
TestUtils.Eventually(() => { code = s.Driver.FindElement(By.CssSelector("#LoginCode .qr-code")).GetAttribute("alt"); });
|
||||
s.Logout();
|
||||
s.GoToLogin();
|
||||
s.Driver.SetAttribute("LoginCode", "value", "bad code");
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
<use href="@GetPathTo(Symbol)"></use>
|
||||
</svg>
|
||||
@code {
|
||||
public string GetPathTo(string symbol)
|
||||
[Parameter, EditorRequired]
|
||||
public string Symbol { get; set; }
|
||||
|
||||
private string GetPathTo(string symbol)
|
||||
{
|
||||
var versioned = FileVersionProvider.AddFileVersionToPath(default, "img/icon-sprite.svg");
|
||||
var rootPath = (BTCPayServerOptions.RootPath ?? "/").WithTrailingSlash();
|
||||
return $"{rootPath}{versioned}#{Symbol}";
|
||||
return $"{rootPath}{versioned}#{symbol}";
|
||||
}
|
||||
[Parameter]
|
||||
public string Symbol { get; set; }
|
||||
}
|
||||
|
|
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
42
BTCPayServer/Blazor/PosLoginCode.razor
Normal file
|
@ -0,0 +1,42 @@
|
|||
@using Microsoft.AspNetCore.Http
|
||||
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
|
||||
@if (Users?.Any() is true)
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass">
|
||||
<label for="SignedInUser" class="form-label">Signed in user</label>
|
||||
<select id="SignedInUser" class="form-select" value="@_userId" @onchange="@(e => _userId = e.Value?.ToString())">
|
||||
<option value="">None, just open the URL</option>
|
||||
@foreach (var u in Users)
|
||||
{
|
||||
<option value="@u.Key">@u.Value</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (string.IsNullOrEmpty(_userId))
|
||||
{
|
||||
<QrCode Data="@PosUrl" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<UserLoginCode UserId="@_userId" RedirectUrl="@PosPath" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string PosPath { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string,string> Users { get; set; }
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private string _userId;
|
||||
private string PosUrl => Request.GetAbsoluteRoot() + PosPath;
|
||||
private HttpRequest Request => HttpContextAccessor.HttpContext?.Request;
|
||||
private string CssClass => $"form-group {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
29
BTCPayServer/Blazor/QrCode.razor
Normal file
29
BTCPayServer/Blazor/QrCode.razor
Normal file
|
@ -0,0 +1,29 @@
|
|||
@using QRCoder
|
||||
|
||||
@if (!string.IsNullOrEmpty(Data))
|
||||
{
|
||||
<img @attributes="Attrs" style="image-rendering:pixelated;image-rendering:-moz-crisp-edges;min-width:@(Size)px;min-height:@(Size)px" src="data:image/png;base64,@(GetBase64(Data))" class="@CssClass" alt="@Data" />
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public string Data { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly QRCodeGenerator QrGenerator = new();
|
||||
|
||||
private string GetBase64(string data)
|
||||
{
|
||||
var qrCodeData = QrGenerator.CreateQrCode(data, QRCodeGenerator.ECCLevel.Q);
|
||||
var qrCode = new PngByteQRCode(qrCodeData);
|
||||
var bytes = qrCode.GetGraphic(5, [0, 0, 0, 255], [0xf5, 0xf5, 0xf7, 255]);
|
||||
return Convert.ToBase64String(bytes);
|
||||
}
|
||||
|
||||
private string CssClass => $"qr-code {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
100
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
100
BTCPayServer/Blazor/UserLoginCode.razor
Normal file
|
@ -0,0 +1,100 @@
|
|||
@using System.Timers
|
||||
@using BTCPayServer.Data
|
||||
@using BTCPayServer.Fido2
|
||||
@using Microsoft.AspNetCore.Http
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using Microsoft.AspNetCore.Mvc
|
||||
@using Microsoft.AspNetCore.Routing
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject UserManager<ApplicationUser> UserManager;
|
||||
@inject UserLoginCodeService UserLoginCodeService;
|
||||
@inject LinkGenerator LinkGenerator;
|
||||
@inject IHttpContextAccessor HttpContextAccessor;
|
||||
@implements IDisposable
|
||||
|
||||
@if (!string.IsNullOrEmpty(_data))
|
||||
{
|
||||
<div @attributes="Attrs" class="@CssClass" style="width:@(Size)px">
|
||||
<div class="qr-container mb-2">
|
||||
<QrCode Data="@_data" Size="Size"/>
|
||||
</div>
|
||||
<p class="text-center text-muted mb-1" id="progress">Valid for @_seconds 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 @(Percent < 15 ? "bg-warning" : null)" role="progressbar" style="width:@Percent%" id="progressbar"></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string RedirectUrl { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int Size { get; set; } = 256;
|
||||
|
||||
[Parameter(CaptureUnmatchedValues = true)]
|
||||
public Dictionary<string, object> Attrs { get; set; }
|
||||
|
||||
private static readonly double Seconds = UserLoginCodeService.ExpirationTime.TotalSeconds;
|
||||
private double _seconds = Seconds;
|
||||
private string _data;
|
||||
private ApplicationUser _user;
|
||||
private Timer _timer;
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
UserId ??= await GetUserId();
|
||||
if (!string.IsNullOrEmpty(UserId)) _user = await UserManager.FindByIdAsync(UserId);
|
||||
if (_user == null) return;
|
||||
|
||||
GenerateCodeAndStartTimer();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer?.Dispose();
|
||||
}
|
||||
|
||||
private void GenerateCodeAndStartTimer()
|
||||
{
|
||||
var loginCode = UserLoginCodeService.GetOrGenerate(_user.Id);
|
||||
_data = GetData(loginCode);
|
||||
_seconds = Seconds;
|
||||
_timer?.Dispose();
|
||||
_timer = new Timer(1000);
|
||||
_timer.Elapsed += CountDownTimer;
|
||||
_timer.Enabled = true;
|
||||
}
|
||||
|
||||
private void CountDownTimer(object source, ElapsedEventArgs e)
|
||||
{
|
||||
if (_seconds > 0)
|
||||
_seconds -= 1;
|
||||
else
|
||||
GenerateCodeAndStartTimer();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserId()
|
||||
{
|
||||
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||
return state.User.Identity?.IsAuthenticated is true
|
||||
? UserManager.GetUserId(state.User)
|
||||
: null;
|
||||
}
|
||||
|
||||
private string GetData(string loginCode)
|
||||
{
|
||||
var req = HttpContextAccessor.HttpContext?.Request;
|
||||
if (req == null) return loginCode;
|
||||
return !string.IsNullOrEmpty(RedirectUrl)
|
||||
? LinkGenerator.LoginCodeLink(loginCode, RedirectUrl, req.Scheme, req.Host, req.PathBase)
|
||||
: $"{loginCode};{LinkGenerator.IndexLink(req.Scheme, req.Host, req.PathBase)};{_user.Email}";
|
||||
}
|
||||
|
||||
private double Percent => Math.Round(_seconds / Seconds * 100);
|
||||
private string CssClass => $"user-login-code d-inline-flex flex-column {(Attrs?.ContainsKey("class") is true ? Attrs["class"] : "")}".Trim();
|
||||
}
|
|
@ -123,15 +123,30 @@ namespace BTCPayServer.Controllers
|
|||
return View(nameof(Login), new LoginViewModel { Email = email });
|
||||
}
|
||||
|
||||
// GET is for signin via the POS backend
|
||||
[HttpGet("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginUsingCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
[HttpPost("/login/code")]
|
||||
[AllowAnonymous]
|
||||
[ValidateAntiForgeryToken]
|
||||
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
|
||||
public async Task<IActionResult> LoginWithCode(string loginCode, string returnUrl = null)
|
||||
{
|
||||
return await LoginCodeResult(loginCode, returnUrl);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> LoginCodeResult(string loginCode, string returnUrl)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(loginCode))
|
||||
{
|
||||
var userId = _userLoginCodeService.Verify(loginCode);
|
||||
var code = loginCode.Split(';').First();
|
||||
var userId = _userLoginCodeService.Verify(code);
|
||||
if (userId is null)
|
||||
{
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Login code was invalid";
|
||||
|
|
|
@ -1,21 +1,12 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BTCPayServer.Controllers
|
||||
{
|
||||
public partial class UIManageController
|
||||
{
|
||||
[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)}'.");
|
||||
}
|
||||
namespace BTCPayServer.Controllers;
|
||||
|
||||
return View(nameof(LoginCodes), _userLoginCodeService.GetOrGenerate(user.Id));
|
||||
}
|
||||
public partial class UIManageController
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult LoginCodes()
|
||||
{
|
||||
return View();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,6 @@ namespace BTCPayServer.Controllers
|
|||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly IHtmlHelper Html;
|
||||
private readonly UserService _userService;
|
||||
private readonly UriResolver _uriResolver;
|
||||
|
@ -62,7 +61,6 @@ namespace BTCPayServer.Controllers
|
|||
UserService userService,
|
||||
UriResolver uriResolver,
|
||||
IFileService fileService,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
IHtmlHelper htmlHelper
|
||||
)
|
||||
{
|
||||
|
@ -76,7 +74,6 @@ namespace BTCPayServer.Controllers
|
|||
_authorizationService = authorizationService;
|
||||
_fido2Service = fido2Service;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
Html = htmlHelper;
|
||||
_userService = userService;
|
||||
_uriResolver = uriResolver;
|
||||
|
|
|
@ -3,7 +3,6 @@ using System;
|
|||
using BTCPayServer;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
|
@ -52,6 +51,12 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.Login), "UIAccount", null , scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string LoginCodeLink(this LinkGenerator urlHelper, string loginCode, string returnUrl, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(nameof(UIAccountController.LoginUsingCode), "UIAccount", new { loginCode, returnUrl }, scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string ResetPasswordLink(this LinkGenerator urlHelper, string userId, string code, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
|
@ -109,5 +114,14 @@ namespace Microsoft.AspNetCore.Mvc
|
|||
values: new { storeId = wallet?.StoreId ?? walletIdOrStoreId, pullPaymentId, payoutState },
|
||||
scheme, host, pathbase);
|
||||
}
|
||||
|
||||
public static string IndexLink(this LinkGenerator urlHelper, string scheme, HostString host, string pathbase)
|
||||
{
|
||||
return urlHelper.GetUriByAction(
|
||||
action: nameof(UIHomeController.Index),
|
||||
controller: "UIHome",
|
||||
values: null,
|
||||
scheme, host, pathbase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace BTCPayServer.Fido2
|
|||
public class UserLoginCodeService
|
||||
{
|
||||
private readonly IMemoryCache _memoryCache;
|
||||
public static readonly TimeSpan ExpirationTime = TimeSpan.FromSeconds(60);
|
||||
|
||||
public UserLoginCodeService(IMemoryCache memoryCache)
|
||||
{
|
||||
|
@ -29,10 +30,10 @@ namespace BTCPayServer.Fido2
|
|||
}
|
||||
return _memoryCache.GetOrCreate(GetCacheKey(userId), entry =>
|
||||
{
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
entry.AbsoluteExpirationRelativeToNow = ExpirationTime;
|
||||
var code = Encoders.Hex.EncodeData(RandomUtils.GetBytes(20));
|
||||
using var newEntry = _memoryCache.CreateEntry(code);
|
||||
newEntry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1);
|
||||
newEntry.AbsoluteExpirationRelativeToNow = ExpirationTime;
|
||||
newEntry.Value = userId;
|
||||
|
||||
return code;
|
||||
|
|
|
@ -627,6 +627,8 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
}
|
||||
|
||||
vm.ExampleCallback = "{\n \"id\":\"SkdsDghkdP3D3qkj7bLq3\",\n \"url\":\"https://btcpay.example.com/invoice?id=SkdsDghkdP3D3qkj7bLq3\",\n \"status\":\"paid\",\n \"price\":10,\n \"currency\":\"EUR\",\n \"invoiceTime\":1520373130312,\n \"expirationTime\":1520374030312,\n \"currentTime\":1520373179327,\n \"exceptionStatus\":false,\n \"buyerFields\":{\n \"buyerEmail\":\"customer@example.com\",\n \"buyerNotify\":false\n },\n \"paymentSubtotals\": {\n \"BTC\":114700\n },\n \"paymentTotals\": {\n \"BTC\":118400\n },\n \"transactionCurrency\": \"BTC\",\n \"amountPaid\": \"1025900\",\n \"exchangeRates\": {\n \"BTC\": {\n \"EUR\": 8721.690715789999,\n \"USD\": 10817.99\n }\n }\n}";
|
||||
|
||||
await FillUsers(vm);
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
|
@ -655,6 +657,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
}
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
await FillUsers(vm);
|
||||
return View("PointOfSale/UpdatePointOfSale", vm);
|
||||
}
|
||||
|
||||
|
@ -715,5 +718,11 @@ namespace BTCPayServer.Plugins.PointOfSale.Controllers
|
|||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
|
||||
private AppData GetCurrentApp() => HttpContext.GetAppData();
|
||||
|
||||
private async Task FillUsers(UpdatePointOfSaleViewModel vm)
|
||||
{
|
||||
var users = await _storeRepository.GetStoreUsers(GetCurrentStore().Id);
|
||||
vm.StoreUsers = users.Select(u => (u.Id, u.Email, u.StoreRole.Role)).ToDictionary(u => u.Id, u => $"{u.Email} ({u.Role})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ namespace BTCPayServer.Plugins.PointOfSale.Models
|
|||
public string CustomTipPercentages { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
public Dictionary<string, string> StoreUsers { get; set; }
|
||||
|
||||
[Display(Name = "Redirect invoice to redirect url automatically after paid")]
|
||||
public string RedirectAutomatically { get; set; } = string.Empty;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
ViewData.SetActivePage(AppsNavPages.Update, "Update Point of Sale", Model.Id);
|
||||
Csp.UnsafeEval();
|
||||
var checkoutFormOptions = await FormDataService.GetSelect(Model.StoreId, Model.FormId);
|
||||
var posPath = Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id });
|
||||
}
|
||||
|
||||
@section PageHeadContent {
|
||||
|
@ -23,14 +24,7 @@
|
|||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script src="~/vendor/summernote/summernote-bs5.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script src="~/pos/admin.js" asp-append-version="true"></script>
|
||||
<script>
|
||||
const qrApp = initQRShow({ title: "Scan to view the Point of Sale" });
|
||||
const posViewUrl = @Safe.Json($"{Context.Request.GetAbsoluteRoot()}{Url.Action("ViewPointOfSale", "UIPointOfSale", new { appId = Model.Id })}");
|
||||
delegate('click', '#displayQRCode', () => { qrApp.showData(posViewUrl); });
|
||||
</script>
|
||||
}
|
||||
|
||||
<form method="post" permissioned="@Policies.CanModifyStoreSettings">
|
||||
|
@ -46,7 +40,7 @@
|
|||
{
|
||||
<div class="btn-group" role="group" aria-label="View Point of Sale">
|
||||
<a class="btn btn-secondary" asp-controller="UIPointOfSale" asp-action="ViewPointOfSale" asp-route-appId="@Model.Id" id="ViewApp" target="_blank">View</a>
|
||||
<button type="button" class="btn btn-secondary px-3 d-inline-flex align-items-center" id="displayQRCode">
|
||||
<button type="button" class="btn btn-secondary px-3 d-inline-flex align-items-center" data-bs-toggle="modal" data-bs-target="#OpenPosModal">
|
||||
<vc:icon symbol="qr-code" />
|
||||
</button>
|
||||
</div>
|
||||
|
@ -316,6 +310,19 @@
|
|||
</div>
|
||||
|
||||
<partial name="_Confirm" model="@(new ConfirmModel("Delete app", "This app will be removed from this store.", "Delete"))" permission="@Policies.CanModifyStoreSettings" />
|
||||
<partial name="ShowQR" />
|
||||
|
||||
|
||||
<div class="modal fade" id="OpenPosModal" tabindex="-1" aria-labelledby="ConfirmTitle" aria-hidden="true">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Scan the QR code to open the Point of Sale</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body pt-0">
|
||||
<component type="typeof(BTCPayServer.Blazor.PosLoginCode)" render-mode="ServerPrerendered" param-Users="@Model.StoreUsers" param-PosPath="@posPath" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,43 +1,11 @@
|
|||
@model string
|
||||
@inject UserManager<ApplicationUser> UserManager;
|
||||
@{
|
||||
ViewData.SetActivePage(ManageNavPages.LoginCodes, "Login Codes");
|
||||
}
|
||||
|
||||
<div class="sticky-header">
|
||||
<h2 text-translate="true">@ViewData["Title"]</h2>
|
||||
<a id="page-primary" class="btn btn-primary" asp-action="LoginCodes">Regenerate code</a>
|
||||
</div>
|
||||
<partial name="_StatusMessage" />
|
||||
<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" style="width:256px">
|
||||
<div class="qr-container mb-2">
|
||||
<vc:qr-code data="@Model"/>
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
}
|
||||
<component type="typeof(BTCPayServer.Blazor.UserLoginCode)" render-mode="ServerPrerendered" param-UserId="@UserManager.GetUserId(User)" param-id="@("LoginCode")"/>
|
||||
|
|
Loading…
Add table
Reference in a new issue