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:
@@ -102,6 +102,17 @@ namespace BTCPayServer.Tests
|
||||
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)
|
||||
{
|
||||
var wait = new WebDriverWait(driver, SeleniumTester.ImplicitWait);
|
||||
|
||||
@@ -16,6 +16,7 @@ using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Wallets;
|
||||
using BTCPayServer.Views.Manage;
|
||||
using BTCPayServer.Views.Server;
|
||||
using BTCPayServer.Views.Stores;
|
||||
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
|
||||
// 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/Common.csproj" />
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<Content Update="Views\StorePullPayments\PullPayments.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
<Content Update="Views\StorePullPayments\Payouts.cshtml">
|
||||
<Content Update="Views\Account\_ViewImports.cshtml">
|
||||
<Pack>false</Pack>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
@@ -33,6 +34,7 @@ namespace BTCPayServer.Controllers
|
||||
readonly Configuration.BTCPayServerOptions _Options;
|
||||
private readonly BTCPayServerEnvironment _btcPayServerEnvironment;
|
||||
private readonly Fido2Service _fido2Service;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly EventAggregator _eventAggregator;
|
||||
readonly ILogger _logger;
|
||||
|
||||
@@ -47,6 +49,7 @@ namespace BTCPayServer.Controllers
|
||||
BTCPayServerEnvironment btcPayServerEnvironment,
|
||||
EventAggregator eventAggregator,
|
||||
Fido2Service fido2Service,
|
||||
UserLoginCodeService userLoginCodeService,
|
||||
Logs logs)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@@ -56,6 +59,7 @@ namespace BTCPayServer.Controllers
|
||||
_Options = options;
|
||||
_btcPayServerEnvironment = btcPayServerEnvironment;
|
||||
_fido2Service = fido2Service;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logs.PayServer;
|
||||
Logs = logs;
|
||||
@@ -73,7 +77,6 @@ namespace BTCPayServer.Controllers
|
||||
[Route("~/Account/Login", Order = 2)]
|
||||
public async Task<IActionResult> Login(string returnUrl = null, string email = null)
|
||||
{
|
||||
|
||||
if (User.Identity.IsAuthenticated && string.IsNullOrEmpty(returnUrl))
|
||||
return RedirectToLocal();
|
||||
// Clear the existing external cookie to ensure a clean login process
|
||||
@@ -85,13 +88,36 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
return View(new LoginViewModel()
|
||||
{
|
||||
Email = email
|
||||
});
|
||||
return View(nameof(Login), new LoginViewModel() { 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]
|
||||
[AllowAnonymous]
|
||||
[Route("~/login", Order = 1)]
|
||||
@@ -104,6 +130,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
return RedirectToAction("Login");
|
||||
}
|
||||
|
||||
ViewData["ReturnUrl"] = returnUrl;
|
||||
if (ModelState.IsValid)
|
||||
{
|
||||
|
||||
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 Fido2Service _fido2Service;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
private readonly UserLoginCodeService _userLoginCodeService;
|
||||
private readonly UserService _userService;
|
||||
readonly StoreRepository _StoreRepository;
|
||||
|
||||
@@ -50,7 +51,8 @@ namespace BTCPayServer.Controllers
|
||||
IAuthorizationService authorizationService,
|
||||
Fido2Service fido2Service,
|
||||
LinkGenerator linkGenerator,
|
||||
UserService userService
|
||||
UserService userService,
|
||||
UserLoginCodeService userLoginCodeService
|
||||
)
|
||||
{
|
||||
_userManager = userManager;
|
||||
@@ -63,6 +65,7 @@ namespace BTCPayServer.Controllers
|
||||
_authorizationService = authorizationService;
|
||||
_fido2Service = fido2Service;
|
||||
_linkGenerator = linkGenerator;
|
||||
_userLoginCodeService = userLoginCodeService;
|
||||
_userService = userService;
|
||||
_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.AddSingleton<UserLoginCodeService>();
|
||||
|
||||
var mvcBuilder= services.AddMvc(o =>
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace BTCPayServer.Models.AccountViewModels
|
||||
[Required]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; }
|
||||
public string LoginCode { get; set; }
|
||||
|
||||
[Display(Name = "Remember me?")]
|
||||
public bool RememberMe { get; set; }
|
||||
|
||||
@@ -11,5 +11,7 @@ namespace BTCPayServer.Models.ManageViewModels
|
||||
public bool Is2faEnabled { get; set; }
|
||||
|
||||
public List<Fido2Credential> Credentials { get; set; }
|
||||
|
||||
public string LoginCode { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,6 @@
|
||||
Layout = "_LayoutSimple";
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
}
|
||||
|
||||
<div class="row justify-content-center mb-2">
|
||||
<div class="col text-center">
|
||||
<a asp-controller="Home" asp-action="Index" tabindex="-1">
|
||||
@@ -19,7 +15,7 @@
|
||||
|
||||
<h1 class="h2 mb-3">Welcome to your BTCPay Server</h1>
|
||||
|
||||
<partial name="_StatusMessage" />
|
||||
<partial name="_StatusMessage"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,12 +26,12 @@
|
||||
<h4 class="modal-title">Sign In</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post">
|
||||
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)" >
|
||||
<form asp-route-returnurl="@ViewData["ReturnUrl"]" method="post" id="login-form" asp-action="Login">
|
||||
<fieldset disabled="@(ViewData.ContainsKey("disabled") ? "disabled" : null)">
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -43,14 +39,22 @@
|
||||
<label asp-for="Password" class="form-label"></label>
|
||||
<a asp-action="ForgotPassword" tabindex="5">Forgot password?</a>
|
||||
</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>
|
||||
</div>
|
||||
<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>
|
||||
</fieldset>
|
||||
</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)
|
||||
{
|
||||
<p class="text-center mt-2 mb-0">
|
||||
@@ -67,3 +71,20 @@
|
||||
<partial name="_BTCPaySupporters"/>
|
||||
</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
|
||||
{
|
||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2
|
||||
Index, ChangePassword, TwoFactorAuthentication, APIKeys, Notifications, Fido2, LoginCodes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,28 +57,28 @@
|
||||
<h5>Disable 2FA</h5>
|
||||
<p class="mb-0 me-3">Re-enabling will not require you to reconfigure your app.</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
<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" 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>
|
||||
<h5>Reset recovery codes</h5>
|
||||
<p class="mb-0 me-3">Regenerate your 2FA recovery codes.</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</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">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
<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 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" />
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
else
|
||||
@@ -88,7 +88,7 @@
|
||||
<h5>Enable 2FA</h5>
|
||||
<p class="mb-0 me-3">Using apps such as Google or Microsoft Authenticator.</p>
|
||||
</div>
|
||||
<vc:icon symbol="caret-right" />
|
||||
<vc:icon symbol="caret-right"/>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
@@ -122,4 +122,4 @@
|
||||
</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.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.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"/>
|
||||
</nav>
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
<qrcode-capture v-if="noStreamApiSupport" v-on:decode="onDecode" :camera="cameraOff? 'off': cameras[camera]" :device-id="cameras[camera]"/>
|
||||
|
||||
</div>
|
||||
<div v-if="isLoaded && requestInput && cameras.length >2" class="d-flex justify-content-center align-items-center">
|
||||
<button class="btn btn-link text-center" v-on:click="nextCamera">Switch camera</button>
|
||||
<div v-if="isLoaded && requestInput && cameras.length > 2" class="d-flex justify-content-center align-items-center mt-3">
|
||||
<button class="btn btn-secondary text-center" v-on:click="nextCamera">Switch camera</button>
|
||||
</div>
|
||||
<div v-else-if="qrData || errorMessage">
|
||||
<div v-if="errorMessage" class="alert alert-danger" role="alert">
|
||||
@@ -107,13 +107,12 @@ function initCameraScanningApp(title, onDataSubmit, modalId) {
|
||||
},
|
||||
methods: {
|
||||
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++;
|
||||
}else if(this.camera == this.cameras.length -1){
|
||||
|
||||
this.camera = 1;
|
||||
}else{
|
||||
this.camera ++;
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user