AuthorBuddy/Components/Pages/Setup2FA.razor

198 lines
5.4 KiB
Text
Raw Normal View History

2026-05-21 11:04:42 +02:00
@page "/setup-2fa"
@inject IAuthService AuthService
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime
2026-05-25 06:38:16 +02:00
@inject ILocalizationService Loc
2026-05-21 11:04:42 +02:00
@rendermode InteractiveServer
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
2026-05-25 06:38:16 +02:00
<PageTitle>@Loc["page.title.setup2fa"]</PageTitle>
2026-05-21 11:04:42 +02:00
<div class="auth-page">
<div class="auth-card">
2026-05-25 06:38:16 +02:00
<div class="auth-logo">
<img src="/Assets/logo.jpg" alt="Author Buddy" />
</div>
<h2>@Loc["setup2fa.heading"]</h2>
2026-05-21 11:04:42 +02:00
@if (isEnabled)
{
<div class="auth-success">
2026-05-25 06:38:16 +02:00
<p>@Loc["setup2fa.status.enabled"]</p>
<button @onclick="Disable2FA" disabled="@isBusy">@Loc["setup2fa.button.disable"]</button>
2026-05-21 11:04:42 +02:00
</div>
}
else if (showSecret)
{
2026-05-25 06:38:16 +02:00
<p>@Loc["setup2fa.instruction"]</p>
2026-05-21 11:04:42 +02:00
<div class="qr-container">
<img src="@qrCodeDataUri" alt="QR Code" />
</div>
<div class="form-group">
2026-05-25 06:38:16 +02:00
<label>@Loc["setup2fa.label.secret_key"]</label>
2026-05-21 11:04:42 +02:00
<div class="secret-display">
<code>@secret</code>
2026-05-25 06:38:16 +02:00
<button @onclick="CopySecret">@Loc["common.copy"]</button>
2026-05-21 11:04:42 +02:00
</div>
</div>
<div class="form-group">
2026-05-25 06:38:16 +02:00
<label for="code">@Loc["setup2fa.label.verification_code"]</label>
2026-05-21 11:04:42 +02:00
<input id="code" @bind="verificationCode" maxlength="6" @onkeydown="HandleKeyDown" />
</div>
<div class="auth-actions">
2026-07-04 10:32:35 +02:00
<button @onclick="Enable2FA" disabled="@_enableDisabled">@Loc["setup2fa.button.enable"]</button>
2026-05-25 06:38:16 +02:00
<button @onclick="Cancel">@Loc["common.cancel"]</button>
2026-05-21 11:04:42 +02:00
</div>
}
else
{
2026-05-25 06:38:16 +02:00
<p>@Loc["setup2fa.description"]</p>
2026-05-21 11:04:42 +02:00
<div class="auth-actions">
2026-05-25 06:38:16 +02:00
<button @onclick="StartSetup">@Loc["setup2fa.button.setup"]</button>
2026-05-21 11:04:42 +02:00
</div>
}
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="auth-error">@errorMessage</div>
}
</div>
</div>
@code {
private string secret = string.Empty;
private string verificationCode = string.Empty;
private string errorMessage = string.Empty;
private string? qrCodeDataUri;
private bool showSecret;
private bool isEnabled;
private bool isBusy;
private string? username;
2026-07-04 10:32:35 +02:00
private bool _enableDisabled => isBusy || string.IsNullOrWhiteSpace(verificationCode);
2026-05-21 11:04:42 +02:00
protected override async Task OnInitializedAsync()
{
isEnabled = await AuthService.Is2FAEnabledAsync();
var user = await AuthService.GetCurrentUserAsync();
username = user?.Name;
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
await Enable2FA();
}
private async Task StartSetup()
{
isBusy = true;
try
{
secret = await AuthService.Generate2FASecretAsync();
var uri = AuthService.Get2FAProvisioningUri(secret, username ?? "user");
qrCodeDataUri = await GenerateQRCodeDataUri(uri);
showSecret = true;
}
catch (Exception ex)
{
2026-05-25 06:38:16 +02:00
errorMessage = $"{Loc["common.error"]} {ex.Message}";
2026-05-21 11:04:42 +02:00
}
finally
{
isBusy = false;
}
}
private async Task Enable2FA()
{
if (string.IsNullOrWhiteSpace(verificationCode) || verificationCode.Length != 6)
{
2026-05-25 06:38:16 +02:00
errorMessage = Loc["setup2fa.error.invalid_code"];
2026-05-21 11:04:42 +02:00
return;
}
if (!TotpHelper.VerifyCode(secret, verificationCode))
{
2026-05-25 06:38:16 +02:00
errorMessage = Loc["setup2fa.error.code_invalid"];
2026-05-21 11:04:42 +02:00
return;
}
isBusy = true;
errorMessage = string.Empty;
try
{
var result = await AuthService.Enable2FAAsync(secret);
if (result)
{
isEnabled = true;
showSecret = false;
}
else
{
2026-05-25 06:38:16 +02:00
errorMessage = Loc["setup2fa.error.enable_failed"];
2026-05-21 11:04:42 +02:00
}
}
catch (Exception ex)
{
2026-05-25 06:38:16 +02:00
errorMessage = $"{Loc["common.error"]} {ex.Message}";
2026-05-21 11:04:42 +02:00
}
finally
{
isBusy = false;
}
}
private async Task Disable2FA()
{
isBusy = true;
try
{
var result = await AuthService.Disable2FAAsync();
if (result)
{
isEnabled = false;
showSecret = false;
secret = string.Empty;
}
}
catch (Exception ex)
{
2026-05-25 06:38:16 +02:00
errorMessage = $"{Loc["common.error"]} {ex.Message}";
2026-05-21 11:04:42 +02:00
}
finally
{
isBusy = false;
}
}
private async Task<string?> GenerateQRCodeDataUri(string uri)
{
try
{
var encoded = System.Web.HttpUtility.UrlEncode(uri);
return $"https://api.qrserver.com/v1/create-qr-code/?size=200x200&data={encoded}";
}
catch
{
return null;
}
}
private async Task CopySecret()
{
if (!string.IsNullOrEmpty(secret))
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", secret);
}
private void Cancel()
{
Navigation.NavigateTo("/");
}
}