Add login code support for the app

This commit is contained in:
Dennis Reimann 2024-06-10 17:04:51 +02:00
parent 048d0c445f
commit 6bc5c12051
No known key found for this signature in database
GPG key ID: 5009E1797F03F8D0
6 changed files with 83 additions and 23 deletions

View file

@ -19,6 +19,7 @@ using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using NicolasDorier.RateLimits;
using PosViewType = BTCPayServer.Plugins.PointOfSale.PosViewType;
@ -74,7 +75,7 @@ public partial class AppApiController
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>> Login(LoginRequest login)
{
const string errorMessage = "Invalid login attempt.";
var errorMessage = "Invalid login attempt.";
if (ModelState.IsValid)
{
// Require the user to pass basic checks (approval, confirmed email, not disabled) before they can log on
@ -96,9 +97,43 @@ public partial class AppApiController
// TODO: Add FIDO and LNURL Auth
return signInResult.Succeeded
? TypedResults.Empty
: TypedResults.Problem(signInResult.ToString(), statusCode: 401);
if (signInResult.IsLockedOut)
{
_logger.LogWarning("User {Email} tried to log in, but is locked out", user.Email);
}
else if (signInResult.Succeeded)
{
_logger.LogInformation("User {Email} logged in", user.Email);
return TypedResults.Empty;
}
errorMessage = signInResult.ToString();
}
return TypedResults.Problem(errorMessage, statusCode: 401);
}
[AllowAnonymous]
[HttpPost("login/code")]
[RateLimitsFilter(ZoneLimits.Login, Scope = RateLimitsScope.RemoteAddress)]
public async Task<Results<Ok<AccessTokenResponse>, EmptyHttpResult, ProblemHttpResult>> LoginWithCode([FromBody] string loginCode)
{
const string errorMessage = "Invalid login attempt.";
if (!string.IsNullOrEmpty(loginCode))
{
var code = loginCode.Split(';').First();
var userId = userLoginCodeService.Verify(code);
var user = userId is null ? null : await userManager.FindByIdAsync(userId);
if (!UserService.TryCanLogin(user, out var message))
{
return TypedResults.Problem(message, statusCode: 401);
}
signInManager.AuthenticationScheme = AuthenticationSchemes.GreenfieldBearer;
await signInManager.SignInAsync(user, false, "LoginCode");
_logger.LogInformation("User {Email} logged in with a login code", user.Email);
return TypedResults.Empty;
}
return TypedResults.Problem(errorMessage, statusCode: 401);
@ -141,6 +176,7 @@ public partial class AppApiController
if (user != null)
{
await signInManager.SignOutAsync();
_logger.LogInformation("User {Email} logged out", user.Email);
return Results.Ok();
}
return Results.Unauthorized();

View file

@ -1,12 +1,13 @@
#nullable enable
using System;
using System.Threading.Tasks;
using BTCPayApp.CommonServer;
using BTCPayApp.CommonServer.Models;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Data;
using BTCPayServer.Fido2;
using BTCPayServer.Logging;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Services.Rates;
@ -17,6 +18,8 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BTCPayServer.App.API;
@ -35,9 +38,14 @@ public partial class AppApiController(
UriResolver uriResolver,
DefaultRulesCollection defaultRules,
RateFetcher rateFactory,
LinkGenerator linkGenerator,
UserLoginCodeService userLoginCodeService,
Logs logs,
IOptionsMonitor<BearerTokenOptions> bearerTokenOptions)
: Controller
{
private readonly ILogger _logger = logs.PayServer;
[AllowAnonymous]
[HttpGet("instance")]
public async Task<Results<Ok<AppInstanceInfo>, NotFound>> Instance()

View file

@ -130,7 +130,8 @@ namespace BTCPayServer.Controllers
{
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";

View file

@ -15,7 +15,9 @@ namespace BTCPayServer.Controllers
throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
return View(nameof(LoginCodes), _userLoginCodeService.GetOrGenerate(user.Id));
var indexUrl = _linkGenerator.IndexLink(Request.Scheme, Request.Host, Request.PathBase);
var loginCode = _userLoginCodeService.GetOrGenerate(user.Id);
return View(nameof(LoginCodes), $"{loginCode};{indexUrl};{user.Email}");
}
}
}

View file

@ -109,5 +109,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);
}
}
}

View file

@ -22,22 +22,26 @@
@section PageFootContent
{
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
<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()
(function () {
const SECONDS = 60
const progress = document.getElementById('progress')
const progressbar = document.getElementById('progressbar')
let remaining = SECONDS
let handle = setInterval(update, 1000)
function 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) {
clearInterval(handle)
window.location.reload()
}
}
})()
</script>
}