Files
chapter-organizer/WebApp/Components/Features/Authentication/Login.razor
T
poprhythm 2d5d075879 Feature-based folder structure
1. Created feature-based folder structure - Components now organized by domain feature
  2. Moved all components - 20+ files moved to new locations
  3. Updated _Imports.razor - Added all new namespace paths for global component access
  4. Updated CustomThemes.cs namespace - Changed from WebApp.Components.Layout to WebApp.Components.Shared.Layout
  5. Removed old using directives - Cleaned up Login.razor and Routes.razor
  6. Removed empty directories - Cleaned up old folder structure
2025-12-03 22:04:23 -05:00

152 lines
6.0 KiB
Plaintext

@page "/login"
@layout EmptyLayout
@using System.ComponentModel.DataAnnotations
@using WebApp.Authentication
@using Microsoft.AspNetCore.Antiforgery
@using Microsoft.AspNetCore.Components.Web
@using MudInputType = MudBlazor.InputType
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.WebUtilities
@inject NavigationManager Navigation
@inject LoginRateLimitService RateLimitService
@inject IHttpContextAccessor HttpContextAccessor
@inject IAntiforgery Antiforgery
@inject IJSRuntime JS
<div class="d-flex justify-center align-center" style="min-height: 100vh;">
<MudPaper Elevation="3" Class="pa-8" MaxWidth="400px" Style="width: 100%;">
<MudText Typo="Typo.h4" Align="Align.Center" GutterBottom="true">
TSA Chapter Organizer
</MudText>
<MudText Typo="Typo.h6" Align="Align.Center" GutterBottom="true" Class="mb-6">
Sign In
</MudText>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<MudAlert Severity="Severity.Error" Class="mb-4">@_errorMessage</MudAlert>
}
<form id="loginForm" method="post" action="/Auth/CookieLogin" @onkeydown="HandleKeyDown">
<input type="hidden" name="__RequestVerificationToken" value="@_antiforgeryToken" />
<input type="hidden" id="emailInput" name="email" value="" />
<input type="hidden" id="passwordInput" name="password" value="" />
<input type="hidden" id="rememberMeInput" name="rememberMe" value="" />
<MudTextField @bind-Value="_loginModel.Email"
Label="Email"
Variant="Variant.Outlined"
InputType="MudInputType.Email"
Required="true"
Class="mb-4" />
<MudTextField @bind-Value="_loginModel.Password"
Label="Password"
Variant="Variant.Outlined"
InputType="@_passwordInput"
Required="true"
Class="mb-4"
Adornment="Adornment.End"
AdornmentIcon="@_passwordInputIcon"
OnAdornmentClick="TogglePasswordVisibility" />
<div class="d-flex justify-space-between align-center mb-4">
<MudCheckBox @bind-Value="_loginModel.RememberMe"
Label="Remember me?"
Color="Color.Primary" />
</div>
<MudButton ButtonType="ButtonType.Button"
Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
OnClick="HandleFormSubmit">
<span>Sign In</span>
</MudButton>
</form>
</MudPaper>
</div>
@code {
private LoginModel _loginModel = new();
private string? _errorMessage;
private bool _passwordVisibility;
private MudInputType _passwordInput = MudInputType.Password;
private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff;
private string _antiforgeryToken = string.Empty;
protected override void OnInitialized()
{
// Generate antiforgery token
var httpContext = HttpContextAccessor.HttpContext;
if (httpContext != null)
{
var tokenSet = Antiforgery.GetAndStoreTokens(httpContext);
_antiforgeryToken = tokenSet.RequestToken ?? string.Empty;
}
// Check for error message from query parameter (set by controller on failed login)
var uri = new Uri(Navigation.Uri);
var queryParams = QueryHelpers.ParseQuery(uri.Query);
if (queryParams.TryGetValue("error", out var errorValue))
{
_errorMessage = errorValue.ToString();
}
}
private class LoginModel
{
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
[MaxLength(100, ErrorMessage = "Email must be less than 100 characters")]
[RegularExpression(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", ErrorMessage = "Please enter a valid email address")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
[MinLength(8, ErrorMessage = "Password must be at least 8 characters")]
[MaxLength(100, ErrorMessage = "Password must be less than 100 characters")]
[DataType(DataType.Password)]
public string Password { get; set; } = string.Empty;
public bool RememberMe { get; set; }
}
private void TogglePasswordVisibility()
{
_passwordVisibility = !_passwordVisibility;
_passwordInputIcon = _passwordVisibility
? Icons.Material.Filled.Visibility
: Icons.Material.Filled.VisibilityOff;
_passwordInput = _passwordVisibility
? MudInputType.Text
: MudInputType.Password;
}
private async Task HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter")
{
// Blur the active element to ensure MudTextField bindings update
await JS.InvokeVoidAsync("eval", "document.activeElement.blur()");
// Small delay to allow bindings to process
await Task.Delay(50);
// Now submit the form
await HandleFormSubmit();
}
}
private async Task HandleFormSubmit()
{
// Update hidden inputs with current model values, then submit the form
await JS.InvokeVoidAsync("eval", $@"
document.getElementById('emailInput').value = {System.Text.Json.JsonSerializer.Serialize(_loginModel.Email)};
document.getElementById('passwordInput').value = {System.Text.Json.JsonSerializer.Serialize(_loginModel.Password)};
document.getElementById('rememberMeInput').value = '{_loginModel.RememberMe.ToString().ToLower()}';
document.getElementById('loginForm').submit();
");
}
}