Files
Budgeteer/BudgetApp/Components/Pages/Login.razor
T
2026-03-02 16:00:18 +00:00

130 lines
6.4 KiB
Plaintext

@page "/login"
@layout AuthLayout
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager Navigation
@inject IAuthenticationSchemeProvider SchemeProvider
<PageTitle>Budgeteer Beta — Sign In</PageTitle>
<div class="text-center mb-4">
<span style="font-size: 3rem;">💰</span>
<h2 class="mt-2 mb-0 fw-bold">Budgeteer <span class="badge bg-warning text-dark" style="font-size: 0.45em; vertical-align: middle;">beta</span></h2>
<p class="text-muted">Sign in to your account</p>
</div>
<EditForm Model="Model" OnValidSubmit="HandleLogin" FormName="login">
<DataAnnotationsValidator />
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<div class="mb-3">
<label class="form-label">Email</label>
<InputText @bind-Value="Model.Email" class="form-control" placeholder="you@example.com" />
<ValidationMessage For="() => Model.Email" />
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<InputText @bind-Value="Model.Password" type="password" class="form-control" />
<ValidationMessage For="() => Model.Password" />
</div>
<button type="submit" class="btn btn-primary w-100">Sign In</button>
</EditForm>
@if (externalProviders.Count > 0)
{
<hr class="my-4" />
<p class="text-center text-muted mb-3">Or sign in with</p>
<div class="d-flex flex-column gap-2">
@foreach (var provider in externalProviders)
{
<form method="post" action="/api/auth/external-login">
<input type="hidden" name="provider" value="@provider.Name" />
<input type="hidden" name="returnUrl" value="/" />
<input type="hidden" name="__RequestVerificationToken" value="@antiforgeryToken" />
<button type="submit" class="btn btn-outline-secondary w-100 d-flex align-items-center justify-content-center gap-2">
@GetProviderIcon(provider.DisplayName ?? provider.Name)
<span>@(provider.DisplayName ?? provider.Name)</span>
</button>
</form>
}
</div>
}
<p class="text-center mt-3">
Don't have an account? <a href="/register">Register</a>
</p>
@code {
[SupplyParameterFromForm]
private LoginModel Model { get; set; } = new();
private string? errorMessage;
private List<AuthenticationScheme> externalProviders = [];
private string antiforgeryToken = string.Empty;
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
protected override async Task OnInitializedAsync()
{
var schemes = await SchemeProvider.GetAllSchemesAsync();
externalProviders = schemes
.Where(s => !string.IsNullOrEmpty(s.DisplayName)
&& s.Name != IdentityConstants.ApplicationScheme
&& s.Name != IdentityConstants.ExternalScheme
&& s.Name != IdentityConstants.TwoFactorUserIdScheme
&& s.Name != IdentityConstants.TwoFactorRememberMeScheme)
.ToList();
if (HttpContext is not null)
{
var tokens = HttpContext.RequestServices
.GetRequiredService<Microsoft.AspNetCore.Antiforgery.IAntiforgery>()
.GetAndStoreTokens(HttpContext);
antiforgeryToken = tokens.RequestToken ?? string.Empty;
}
}
private async Task HandleLogin()
{
errorMessage = null;
var result = await SignInManager.PasswordSignInAsync(
Model.Email, Model.Password, isPersistent: false, lockoutOnFailure: false);
if (result.Succeeded)
{
Navigation.NavigateTo("/", forceLoad: true);
}
else
{
errorMessage = "Invalid email or password.";
}
}
private static MarkupString GetProviderIcon(string provider) => provider switch
{
"Google" => new MarkupString("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"><path fill=\"#4285F4\" d=\"M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.27-4.74 3.27-8.1z\"/><path fill=\"#34A853\" d=\"M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z\"/><path fill=\"#FBBC05\" d=\"M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z\"/><path fill=\"#EA4335\" d=\"M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z\"/></svg>"),
"Microsoft" => new MarkupString("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 23 23\"><rect fill=\"#f25022\" x=\"1\" y=\"1\" width=\"10\" height=\"10\"/><rect fill=\"#00a4ef\" x=\"1\" y=\"12\" width=\"10\" height=\"10\"/><rect fill=\"#7fba00\" x=\"12\" y=\"1\" width=\"10\" height=\"10\"/><rect fill=\"#ffb900\" x=\"12\" y=\"12\" width=\"10\" height=\"10\"/></svg>"),
"GitHub" => new MarkupString("<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\"><path d=\"M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\"/></svg>"),
_ => new MarkupString("")
};
private sealed class LoginModel
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Email is required")]
[System.ComponentModel.DataAnnotations.EmailAddress]
public string Email { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Password is required")]
public string Password { get; set; } = string.Empty;
}
}