Files
chapter-organizer/WebApp/Components/Login.razor
T

144 lines
5.2 KiB
Plaintext

@page "/login"
@using System.ComponentModel.DataAnnotations
@using WebApp.Authentication
@inject NavigationManager Navigation
@inject LoginRateLimitService RateLimitService
@inject IHttpContextAccessor HttpContextAccessor
<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>
}
<EditForm Model="@_loginModel" OnValidSubmit="HandleLogin">
<DataAnnotationsValidator />
<MudTextField @bind-Value="_loginModel.Email"
Label="Email"
Variant="Variant.Outlined"
InputType="InputType.Email"
Required="true"
Class="mb-4"
Disabled="@_isSubmitting"
For="@(() => _loginModel.Email)" />
<MudTextField @bind-Value="_loginModel.Password"
Label="Password"
Variant="Variant.Outlined"
InputType="@_passwordInput"
Required="true"
Class="mb-4"
Disabled="@_isSubmitting"
Adornment="Adornment.End"
AdornmentIcon="@_passwordInputIcon"
OnAdornmentClick="TogglePasswordVisibility"
For="@(() => _loginModel.Password)" />
<div class="d-flex justify-space-between align-center mb-4">
<MudCheckBox @bind-Value="_loginModel.RememberMe"
Label="Remember me?"
Color="Color.Primary"
Disabled="@_isSubmitting" />
</div>
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Primary"
Size="Size.Large"
FullWidth="true"
Disabled="@_isSubmitting">
@if (_isSubmitting)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
<span>Signing in...</span>
}
else
{
<span>Sign In</span>
}
</MudButton>
</EditForm>
</MudPaper>
</div>
@code {
private LoginModel _loginModel = new();
private string? _errorMessage;
private bool _isSubmitting = false;
private bool _passwordVisibility;
private InputType _passwordInput = InputType.Password;
private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff;
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;
}
private async Task HandleLogin()
{
_isSubmitting = true;
_errorMessage = null;
try
{
// Get client IP address
var ipAddress = HttpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString()
?? "unknown";
// 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;
}
// Navigate to controller endpoint with credentials
var uri = $"/Auth/CookieLogin?email={Uri.EscapeDataString(_loginModel.Email)}" +
$"&password={Uri.EscapeDataString(_loginModel.Password)}" +
$"&rememberMe={_loginModel.RememberMe}";
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;
}
}
}