a0313687da
- File: WebApp/ChapterSettings.cs - Change: Renamed StateContainer.UserId to ScheduledTeams - Impact: Property name now accurately reflects what it stores 2. ✅ Added Structured Logging with Serilog - Packages Added: - Serilog.AspNetCore - Serilog.Sinks.Console - Serilog.Sinks.File - Files Modified: - Program.cs - Added Serilog configuration with console and file logging - appsettings.json - Added Serilog minimum log levels - appsettings.Development.json - Added Debug level logging for development - Benefits: - Structured log output for better parsing/analysis - Automatic file rotation (daily, 30 days retention) - Logs stored in logs/webapp-.txt - Better formatted console output 3. ✅ Added Global Error Handling - File Created: WebApp/Components/Shared/AppErrorBoundary.razor - File Modified: WebApp/Components/App.razor - Features: - Catches unhandled exceptions throughout the app - Shows detailed error info in Development environment - Shows user-friendly message in Production - Logs errors automatically - Provides "Return to Home" button 4. ✅ Enhanced Input Validation - File Modified: WebApp/Components/Login.razor - Validations Added: - Email: Required, valid email format, max 100 chars, regex validation - Password: Required, min 8 chars, max 100 chars - Benefits: - Client-side validation before submission - Clear error messages for users - Prevents invalid data submission
153 lines
6.0 KiB
Plaintext
153 lines
6.0 KiB
Plaintext
@page "/login"
|
|
@using WebApp.Components.Layout
|
|
@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();
|
|
");
|
|
}
|
|
}
|