2026-05-21 11:04:42 +02:00
|
|
|
@page "/login"
|
|
|
|
|
@inject IAuthService AuthService
|
|
|
|
|
@inject CustomAuthStateProvider AuthStateProvider
|
|
|
|
|
@inject NavigationManager Navigation
|
|
|
|
|
@inject ISettingsService SettingsService
|
2026-05-25 06:38:16 +02:00
|
|
|
@inject ILocalizationService Loc
|
2026-05-21 11:04:42 +02:00
|
|
|
|
|
|
|
|
@rendermode InteractiveServer
|
|
|
|
|
|
2026-05-25 06:38:16 +02:00
|
|
|
<PageTitle>@Loc["page.title.login"]</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["login.heading"]</h2>
|
2026-05-21 11:04:42 +02:00
|
|
|
|
|
|
|
|
@if (!show2FA)
|
|
|
|
|
{
|
|
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label for="username">@Loc["login.label.username"]</label>
|
2026-05-21 11:04:42 +02:00
|
|
|
<input id="username" @bind="username" @onkeydown="HandleKeyDown" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label for="password">@Loc["login.label.password"]</label>
|
2026-05-21 11:04:42 +02:00
|
|
|
<input id="password" type="password" @bind="password" @onkeydown="HandleKeyDown" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="auth-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="DoLogin" disabled="@isBusy">@Loc["login.button.submit"]</button>
|
|
|
|
|
<a href="/register">@Loc["login.link.register"]</a>
|
2026-05-21 11:04:42 +02:00
|
|
|
</div>
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
<p>@Loc["login.2fa.instruction"]</p>
|
2026-05-21 11:04:42 +02:00
|
|
|
<div class="form-group">
|
2026-05-25 06:38:16 +02:00
|
|
|
<label for="code">@Loc["login.2fa.label.code"]</label>
|
2026-05-21 11:04:42 +02:00
|
|
|
<input id="code" @bind="twoFactorCode" maxlength="6" @onkeydown="HandleKeyDown" />
|
|
|
|
|
</div>
|
|
|
|
|
<div class="auth-actions">
|
2026-05-25 06:38:16 +02:00
|
|
|
<button @onclick="Verify2FA" disabled="@isBusy">@Loc["login.2fa.button.verify"]</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 username = string.Empty;
|
|
|
|
|
private string password = string.Empty;
|
|
|
|
|
private string twoFactorCode = string.Empty;
|
|
|
|
|
private string errorMessage = string.Empty;
|
|
|
|
|
private bool isBusy;
|
|
|
|
|
private bool show2FA;
|
|
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
|
{
|
|
|
|
|
if (AuthService.IsLoggedIn)
|
|
|
|
|
{
|
|
|
|
|
Navigation.NavigateTo("/");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var users = await SettingsService.GetUsersAsync();
|
|
|
|
|
if (users.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
Navigation.NavigateTo("/register");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
|
|
|
{
|
|
|
|
|
if (firstRender && AuthService.IsLoggedIn)
|
|
|
|
|
Navigation.NavigateTo("/");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task HandleKeyDown(KeyboardEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
if (e.Key == "Enter")
|
|
|
|
|
{
|
|
|
|
|
if (show2FA)
|
|
|
|
|
await Verify2FA();
|
|
|
|
|
else
|
|
|
|
|
await DoLogin();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task DoLogin()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = Loc["login.error.empty_fields"];
|
2026-05-21 11:04:42 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isBusy = true;
|
|
|
|
|
errorMessage = string.Empty;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await AuthService.LoginAsync(username, password);
|
|
|
|
|
if (AuthService.Is2FARequired)
|
|
|
|
|
{
|
|
|
|
|
show2FA = true;
|
|
|
|
|
password = string.Empty;
|
|
|
|
|
}
|
|
|
|
|
else if (result)
|
|
|
|
|
{
|
|
|
|
|
AuthStateProvider.NotifyStateChanged();
|
|
|
|
|
Navigation.NavigateTo("/");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = Loc["login.error.invalid_credentials"];
|
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 Verify2FA()
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(twoFactorCode) || twoFactorCode.Length != 6)
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = Loc["login.2fa.error.invalid_code"];
|
2026-05-21 11:04:42 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isBusy = true;
|
|
|
|
|
errorMessage = string.Empty;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var result = await AuthService.Verify2FAAsync(twoFactorCode);
|
|
|
|
|
if (result)
|
|
|
|
|
{
|
|
|
|
|
AuthStateProvider.NotifyStateChanged();
|
|
|
|
|
Navigation.NavigateTo("/");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2026-05-25 06:38:16 +02:00
|
|
|
errorMessage = Loc["login.2fa.error.code_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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|