diff --git a/WebApp/Authentication/AuthController.cs b/WebApp/Authentication/AuthController.cs index c73d27d..d865cf3 100644 --- a/WebApp/Authentication/AuthController.cs +++ b/WebApp/Authentication/AuthController.cs @@ -22,12 +22,11 @@ namespace WebApp.Authentication } [HttpPost] - [HttpGet] // Support both for navigation from Blazor [AllowAnonymous] public async Task CookieLogin( - string email, - string password, - bool rememberMe = false) + [FromForm] string email, + [FromForm] string password, + [FromForm] bool rememberMe = false) { try { @@ -42,8 +41,8 @@ namespace WebApp.Authentication "Login attempt from locked out IP: {IpAddress}. Remaining: {Remaining}", ipAddress, remaining); - TempData["LoginError"] = $"Too many failed attempts. Try again in {remaining?.Minutes ?? 15} minutes."; - return Redirect("/login"); + var errorMsg = Uri.EscapeDataString($"Too many failed attempts. Try again in {remaining?.Minutes ?? 15} minutes."); + return Redirect($"/login?error={errorMsg}"); } // Validate credentials @@ -58,8 +57,7 @@ namespace WebApp.Authentication "Failed login attempt for {Email} from {IpAddress}", email, ipAddress); - TempData["LoginError"] = "Invalid email or password."; - return Redirect("/login"); + return Redirect("/login?error=Invalid%20email%20or%20password."); } // Success - clear rate limit tracking diff --git a/WebApp/Components/Login.razor b/WebApp/Components/Login.razor index 9e1a17d..a6804e1 100644 --- a/WebApp/Components/Login.razor +++ b/WebApp/Components/Login.razor @@ -3,9 +3,16 @@ @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
@@ -21,17 +28,18 @@ @_errorMessage } - - +
+ + + + + Class="mb-4" /> + OnAdornmentClick="TogglePasswordVisibility" />
+ Color="Color.Primary" />
- - @if (_isSubmitting) - { - - Signing in... - } - else - { - Sign In - } + OnClick="HandleFormSubmit"> + Sign In - +
@code { private LoginModel _loginModel = new(); private string? _errorMessage; - private bool _isSubmitting = false; private bool _passwordVisibility; - private InputType _passwordInput = InputType.Password; + 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")] public string Email { get; set; } = string.Empty; - - [Required(ErrorMessage = "Password is required")] - [MinLength(8, ErrorMessage = "Password must be at least 8 characters")] public string Password { get; set; } = string.Empty; - public bool RememberMe { get; set; } } private void TogglePasswordVisibility() { - if (_isSubmitting) return; - _passwordVisibility = !_passwordVisibility; _passwordInputIcon = _passwordVisibility ? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff; _passwordInput = _passwordVisibility - ? InputType.Text - : InputType.Password; + ? MudInputType.Text + : MudInputType.Password; } - private void HandleLogin() + private async Task HandleKeyDown(KeyboardEventArgs e) { - _isSubmitting = true; - _errorMessage = null; - - try + if (e.Key == "Enter") { - // Get client IP address - var ipAddress = HttpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString() - ?? "unknown"; + // Blur the active element to ensure MudTextField bindings update + await JS.InvokeVoidAsync("eval", "document.activeElement.blur()"); - // Check rate limiting - if (RateLimitService.IsLockedOut(ipAddress)) - { - var remaining = RateLimitService.GetRemainingLockoutTime(ipAddress); - _errorMessage = $"Too many failed attempts. Please try again in {remaining?.Minutes ?? 15} minutes."; - return; - } + // Small delay to allow bindings to process + await Task.Delay(50); - // Navigate to controller endpoint with credentials - var uri = $"/Auth/CookieLogin?email={Uri.EscapeDataString(_loginModel.Email)}" + - $"&password={Uri.EscapeDataString(_loginModel.Password)}" + - $"&rememberMe={_loginModel.RememberMe}"; + // Now submit the form + await HandleFormSubmit(); + } + } - Navigation.NavigateTo(uri, forceLoad: true); - } - catch (Exception ex) - { - _errorMessage = "An error occurred during login. Please try again."; - Console.WriteLine($"Login error: {ex}"); - } - finally - { - _isSubmitting = false; - } + 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(); + "); } } diff --git a/WebApp/Components/Pages/EventDefinitionPages/Create.razor b/WebApp/Components/Pages/EventDefinitionPages/Create.razor index cf74e2a..528be1a 100644 --- a/WebApp/Components/Pages/EventDefinitionPages/Create.razor +++ b/WebApp/Components/Pages/EventDefinitionPages/Create.razor @@ -11,6 +11,7 @@ + diff --git a/WebApp/Components/Pages/EventDefinitionPages/Edit.razor b/WebApp/Components/Pages/EventDefinitionPages/Edit.razor index 437a77e..9b2cde0 100644 --- a/WebApp/Components/Pages/EventDefinitionPages/Edit.razor +++ b/WebApp/Components/Pages/EventDefinitionPages/Edit.razor @@ -12,6 +12,7 @@ + diff --git a/WebApp/Components/Pages/StudentPages/Create.razor b/WebApp/Components/Pages/StudentPages/Create.razor index d393888..628d351 100644 --- a/WebApp/Components/Pages/StudentPages/Create.razor +++ b/WebApp/Components/Pages/StudentPages/Create.razor @@ -11,6 +11,7 @@ + diff --git a/WebApp/Components/Pages/StudentPages/Edit.razor b/WebApp/Components/Pages/StudentPages/Edit.razor index a8dc67e..b39f64c 100644 --- a/WebApp/Components/Pages/StudentPages/Edit.razor +++ b/WebApp/Components/Pages/StudentPages/Edit.razor @@ -20,6 +20,7 @@ else /* https://www.mudblazor.com/components/form */ /* https://medium.com/@husainalbar/applying-mudblazor-for-crud-operations-in-our-blazor-project-a343037a52ef */ + diff --git a/WebApp/Components/Pages/TeamPages/Create.razor b/WebApp/Components/Pages/TeamPages/Create.razor index d91fd9b..feb8d82 100644 --- a/WebApp/Components/Pages/TeamPages/Create.razor +++ b/WebApp/Components/Pages/TeamPages/Create.razor @@ -11,6 +11,7 @@ + diff --git a/WebApp/Components/Pages/TeamPages/Edit.razor b/WebApp/Components/Pages/TeamPages/Edit.razor index b363788..3048b6e 100644 --- a/WebApp/Components/Pages/TeamPages/Edit.razor +++ b/WebApp/Components/Pages/TeamPages/Edit.razor @@ -16,6 +16,7 @@ else { + diff --git a/WebApp/Program.cs b/WebApp/Program.cs index 8d0c7a9..8635a45 100644 --- a/WebApp/Program.cs +++ b/WebApp/Program.cs @@ -38,8 +38,8 @@ builder.Services.AddDatabaseDeveloperPageExceptionFilter(); builder.Services.AddScoped(); -builder.Services.AddScoped(); // Server-side -builder.Services.AddSingleton();//Client-side +// State container for maintaining state per user connection (Blazor Server) +builder.Services.AddScoped(); // Add authentication services builder.Services.AddHttpContextAccessor();