Enhance authentication flow by adding return URL support
This commit updates the authentication process to include a return URL parameter, allowing users to be redirected back to their original page after logging in. Changes were made to the AuthController, Login component, and Routes component to handle the return URL appropriately. Additionally, improvements were made to the TeamScheduler and TeamSchedulerSolution classes for better team and student management. These enhancements improve user experience and navigation within the application.
This commit is contained in:
@@ -85,7 +85,7 @@ public class TeamScheduler
|
||||
var m = new int[_students.Length,_teams.Length];
|
||||
foreach (var i in _students)
|
||||
foreach (var t in _teams)
|
||||
m[i, t] = _studentObjects[i].Teams.Contains(_teamObjects[t]) ? 1 : 0;
|
||||
m[i, t] = _studentObjects[i].Teams.Any(team => team.Id == _teamObjects[t].Id) ? 1 : 0;
|
||||
|
||||
// Decision variables:
|
||||
// x[t,s] = 1 if meeting of team t takes place at time slot s, else 0
|
||||
|
||||
@@ -84,6 +84,7 @@ public class TeamSchedulerSolution(
|
||||
public Team[] StudentUnassignedTeams(Student student)
|
||||
{
|
||||
var meetingTeams = TimeSlots.SelectMany(t => t.Teams);
|
||||
return student.Teams.Where(e => !meetingTeams.Contains(e)).ToArray();
|
||||
var meetingTeamIds = meetingTeams.Select(t => t.Id).ToHashSet();
|
||||
return student.Teams.Where(e => !meetingTeamIds.Contains(e.Id)).ToArray();
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,8 @@ namespace WebApp.Authentication
|
||||
public async Task<IActionResult> CookieLogin(
|
||||
[FromForm] string email,
|
||||
[FromForm] string password,
|
||||
[FromForm] bool rememberMe = false)
|
||||
[FromForm] bool rememberMe = false,
|
||||
[FromForm] string? returnUrl = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -42,7 +43,10 @@ namespace WebApp.Authentication
|
||||
ipAddress, remaining);
|
||||
|
||||
var errorMsg = Uri.EscapeDataString($"Too many failed attempts. Try again in {remaining?.Minutes ?? 15} minutes.");
|
||||
return Redirect($"/login?error={errorMsg}");
|
||||
var redirectUrl = string.IsNullOrEmpty(returnUrl)
|
||||
? $"/login?error={errorMsg}"
|
||||
: $"/login?error={errorMsg}&returnUrl={Uri.EscapeDataString(returnUrl)}";
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
// Validate credentials
|
||||
@@ -57,7 +61,10 @@ namespace WebApp.Authentication
|
||||
"Failed login attempt for {Email} from {IpAddress}",
|
||||
email, ipAddress);
|
||||
|
||||
return Redirect("/login?error=Invalid%20email%20or%20password.");
|
||||
var redirectUrl = string.IsNullOrEmpty(returnUrl)
|
||||
? "/login?error=Invalid%20email%20or%20password."
|
||||
: $"/login?error=Invalid%20email%20or%20password.&returnUrl={Uri.EscapeDataString(returnUrl)}";
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
// Success - clear rate limit tracking
|
||||
@@ -89,13 +96,22 @@ namespace WebApp.Authentication
|
||||
"Successful login for {Email} ({Role}) from {IpAddress}",
|
||||
result.Email, result.Role, ipAddress);
|
||||
|
||||
// Validate return URL is local to prevent open redirect attacks
|
||||
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||
{
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
return Redirect("/");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during login process");
|
||||
TempData["LoginError"] = "An error occurred. Please try again.";
|
||||
return Redirect("/login");
|
||||
var redirectUrl = string.IsNullOrEmpty(returnUrl)
|
||||
? "/login"
|
||||
: $"/login?returnUrl={Uri.EscapeDataString(returnUrl)}";
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<input type="hidden" id="emailInput" name="email" value="" />
|
||||
<input type="hidden" id="passwordInput" name="password" value="" />
|
||||
<input type="hidden" id="rememberMeInput" name="rememberMe" value="" />
|
||||
<input type="hidden" id="returnUrlInput" name="returnUrl" value="@_returnUrl" />
|
||||
|
||||
<MudTextField @bind-Value="_loginModel.Email"
|
||||
Label="Email"
|
||||
@@ -75,6 +76,7 @@
|
||||
private MudInputType _passwordInput = MudInputType.Password;
|
||||
private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff;
|
||||
private string _antiforgeryToken = string.Empty;
|
||||
private string? _returnUrl;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
@@ -86,13 +88,17 @@
|
||||
_antiforgeryToken = tokenSet.RequestToken ?? string.Empty;
|
||||
}
|
||||
|
||||
// Check for error message from query parameter (set by controller on failed login)
|
||||
// Check for error message and returnUrl from query parameters
|
||||
var uri = new Uri(Navigation.Uri);
|
||||
var queryParams = QueryHelpers.ParseQuery(uri.Query);
|
||||
if (queryParams.TryGetValue("error", out var errorValue))
|
||||
{
|
||||
_errorMessage = errorValue.ToString();
|
||||
}
|
||||
if (queryParams.TryGetValue("returnUrl", out var returnUrlValue))
|
||||
{
|
||||
_returnUrl = returnUrlValue.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private class LoginModel
|
||||
@@ -141,10 +147,12 @@
|
||||
private async Task HandleFormSubmit()
|
||||
{
|
||||
// Update hidden inputs with current model values, then submit the form
|
||||
var returnUrlValue = string.IsNullOrEmpty(_returnUrl) ? "" : System.Text.Json.JsonSerializer.Serialize(_returnUrl);
|
||||
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('returnUrlInput').value = {returnUrlValue};
|
||||
document.getElementById('loginForm').submit();
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1,79 +1 @@
|
||||
@using Core.Entities
|
||||
@using MudBlazor
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">Event Details</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (EventOccurrence == null)
|
||||
{
|
||||
<MudText>No event data available.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudStack Spacing="3">
|
||||
@if (EventDefinition != null)
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Event</MudText>
|
||||
<MudText Typo="Typo.h6">@EventDefinition.Name</MudText>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Occurrence</MudText>
|
||||
<MudText Typo="Typo.body1">@EventOccurrence.Name</MudText>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Date & Time</MudText>
|
||||
<MudText Typo="Typo.body1">
|
||||
@EventOccurrence.StartTime.ToString("f")
|
||||
@if (EventOccurrence.EndTime.HasValue)
|
||||
{
|
||||
<text> - @EventOccurrence.EndTime.Value.ToString("t")</text>
|
||||
}
|
||||
</MudText>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrEmpty(EventOccurrence.Location))
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Location</MudText>
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LocationOn" Size="Size.Small" Class="mr-1" />
|
||||
@EventOccurrence.Location
|
||||
</MudText>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(EventOccurrence.Time))
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Time</MudText>
|
||||
<MudText Typo="Typo.body1">@EventOccurrence.Time</MudText>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (StudentFirstNames != null && StudentFirstNames.Any())
|
||||
{
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Students</MudText>
|
||||
<MudText Typo="Typo.body1">
|
||||
<MudIcon Icon="@Icons.Material.Filled.People" Size="Size.Small" Class="mr-1" />
|
||||
@string.Join(", ", StudentFirstNames)
|
||||
</MudText>
|
||||
</div>
|
||||
}
|
||||
</MudStack>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventOccurrence? EventOccurrence { get; set; }
|
||||
[Parameter] public EventDefinition? EventDefinition { get; set; }
|
||||
[Parameter] public List<string>? StudentFirstNames { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -174,8 +174,9 @@
|
||||
|
||||
private async void ToggleRequiredTeam(Team unassignedTeam)
|
||||
{
|
||||
if (_scheduledTeams.Contains(unassignedTeam))
|
||||
_scheduledTeams = _scheduledTeams.Where(t => t != unassignedTeam);
|
||||
var scheduledTeamIds = _scheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
if (scheduledTeamIds.Contains(unassignedTeam.Id))
|
||||
_scheduledTeams = _scheduledTeams.Where(t => t.Id != unassignedTeam.Id);
|
||||
else
|
||||
{
|
||||
_scheduledTeams = _scheduledTeams.Concat([unassignedTeam]);
|
||||
@@ -227,7 +228,9 @@
|
||||
await Context.Students
|
||||
.AsNoTracking()
|
||||
.Include(e => e.Teams)
|
||||
.ThenInclude(e => e.Captain)
|
||||
.ThenInclude(t => t.Event)
|
||||
.Include(e => e.Teams)
|
||||
.ThenInclude(t => t.Captain)
|
||||
.Include(e => e.EventRankings)
|
||||
.ThenInclude(e => e.EventDefinition)
|
||||
.OrderBy(e => e.FirstName).ToArrayAsync();
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
<MudText Typo="Typo.h6">@TimeSlotName</MudText>
|
||||
@foreach (var team in Teams.OrderByEventFormatFirst().ThenBy(e => e.ToString()))
|
||||
{
|
||||
var removed = !ScheduledTeams.Contains(team);
|
||||
var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var removed = !scheduledTeamIds.Contains(team.Id);
|
||||
|
||||
<MudLink Typo="Typo.body1"
|
||||
Class="d-flex align-center"
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
@foreach (var unassignedTeam in UnassignedTeams(student))
|
||||
{
|
||||
var isPossibleAddition = PossibleAdditions.Contains(unassignedTeam, new TeamIdComparer());
|
||||
var isScheduled = ScheduledTeams.Contains(unassignedTeam);
|
||||
var scheduledTeamIds = ScheduledTeams.Select(t => t.Id).ToHashSet();
|
||||
var isScheduled = scheduledTeamIds.Contains(unassignedTeam.Id);
|
||||
var color = isPossibleAddition ? Color.Success : Color.Default;
|
||||
|
||||
if (unassignedTeam != UnassignedTeams(student).First())
|
||||
|
||||
@@ -172,7 +172,18 @@
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
// Load all students with their teams
|
||||
// Load teams with their students separately to avoid circular reference
|
||||
var teamsWithStudents = await Context.Teams
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Students)
|
||||
.Include(t => t.Event)
|
||||
.Include(t => t.Captain)
|
||||
.ToListAsync();
|
||||
|
||||
// Create a dictionary for quick lookup of team students
|
||||
var teamStudentsDict = teamsWithStudents.ToDictionary(t => t.Id, t => t.Students.ToList());
|
||||
|
||||
// Load all students with their teams (without loading team students to avoid circular reference)
|
||||
var students = await Context.Students
|
||||
.AsNoTracking()
|
||||
.Include(s => s.Teams)
|
||||
@@ -181,6 +192,18 @@
|
||||
.ThenInclude(t => t.Captain)
|
||||
.ToListAsync();
|
||||
|
||||
// Populate Students for each team from the separately loaded teams
|
||||
foreach (var student in students)
|
||||
{
|
||||
foreach (var team in student.Teams)
|
||||
{
|
||||
if (teamStudentsDict.TryGetValue(team.Id, out var teamStudents))
|
||||
{
|
||||
team.Students = teamStudents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter to only students with teams
|
||||
var studentTeams = students
|
||||
.Where(s => s.Teams.Any(t => t?.Event != null && (!_showRegionalOnly || t.Event.RegionalEvent)))
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
|
||||
<NotAuthorized>
|
||||
@{
|
||||
navigationManager.NavigateTo("/login", true);
|
||||
var currentUri = navigationManager.Uri;
|
||||
var baseUri = navigationManager.BaseUri;
|
||||
var relativePath = currentUri.Replace(baseUri, "").TrimStart('/');
|
||||
var returnUrl = string.IsNullOrEmpty(relativePath) ? "/" : "/" + relativePath;
|
||||
var encodedReturnUrl = Uri.EscapeDataString(returnUrl);
|
||||
navigationManager.NavigateTo($"/login?returnUrl={encodedReturnUrl}", true);
|
||||
}
|
||||
</NotAuthorized>
|
||||
</AuthorizeRouteView>
|
||||
|
||||
@@ -41,7 +41,7 @@ public class CalendarEventItem : CalendarItem
|
||||
EventDefinition = occurrence.EventDefinition;
|
||||
// Set base class properties that the calendar component uses
|
||||
StudentFirstNames = studentFirstNames?.ToList() ?? [];
|
||||
Text = occurrence.EventDefinition?.ShortName;
|
||||
Text = occurrence.EventDefinition?.ShortName ?? string.Empty;
|
||||
Start = occurrence.StartTime;
|
||||
End = occurrence.EndTime ?? occurrence.StartTime.AddHours(1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user