Fixed login form, navigation

This commit is contained in:
danial23 2025-05-20 11:26:05 -04:00
parent 27aaee6293
commit 51766eca42
Signed by: danial23
SSH key fingerprint: SHA256:IJ8VP0j2WMUVweTYnzUUnEjNgPnGx+mAt+RhqWZ01bU
4 changed files with 175 additions and 182 deletions

View file

@ -4,16 +4,16 @@
ViewData["Title"] = "Authenticate"; ViewData["Title"] = "Authenticate";
} }
<div class="row" style="display: flex; flex-direction: row;"> <div class="row">
<div class="col-md-4"> <div class="col-md-6 offset-md-3">
<form method="post"> <form method="post">
<h4>Login</h4> <h4>Login or Register</h4>
<hr /> <hr />
@if (Model.LoginErrorMessages != null && Model.LoginErrorMessages.Any()) @if (Model.ErrorMessages != null && Model.ErrorMessages.Any())
{ {
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"> <div class="text-danger" role="alert">
<ul> <ul>
@foreach (var error in Model.LoginErrorMessages) @foreach (var error in Model.ErrorMessages)
{ {
<li>@error</li> <li>@error</li>
} }
@ -21,52 +21,23 @@
</div> </div>
} }
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input asp-for="LoginInput.Username" class="form-control" autocomplete="username" aria-required="true" placeholder="username" /> <input asp-for="Input.Username" class="form-control" autocomplete="username" aria-required="true" placeholder="username" />
<label asp-for="LoginInput.Username">Username</label> <label asp-for="Input.Username">Username</label>
<span asp-validation-for="LoginInput.Username" class="text-danger"></span> <span asp-validation-for="Input.Username" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input asp-for="LoginInput.Password" class="form-control" type="password" autocomplete="current-password" aria-required="true" placeholder="password" /> <input asp-for="Input.Password" class="form-control" type="password" autocomplete="current-password" aria-required="true" placeholder="password" />
<label asp-for="LoginInput.Password">Password</label> <label asp-for="Input.Password">Password</label>
<span asp-validation-for="LoginInput.Password" class="text-danger"></span> <span asp-validation-for="Input.Password" class="text-danger"></span>
</div>
<div>
<button type="submit" asp-page-handler="Login" class="w-100 btn btn-lg btn-secondary">Login</button>
</div>
</form>
</div>
<div class="col-md-4">
<form method="post">
<h4>Register</h4>
<hr />
@if (Model.RegisterErrorMessages != null && Model.RegisterErrorMessages.Any())
{
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert">
<ul>
@foreach (var error in Model.RegisterErrorMessages)
{
<li>@error</li>
}
</ul>
</div>
}
<div class="form-floating mb-3">
<input asp-for="RegisterInput.Username" class="form-control" autocomplete="username" aria-required="true" placeholder="username" />
<label asp-for="RegisterInput.Username">Username</label>
<span asp-validation-for="RegisterInput.Username" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3"> <div class="form-floating mb-3">
<input asp-for="RegisterInput.Email" class="form-control" autocomplete="email" placeholder="name@example.com" /> <input asp-for="Input.Email" class="form-control" autocomplete="email" placeholder="name@example.com" id="Input_Email" />
<label asp-for="RegisterInput.Email">Email</label> <label asp-for="Input.Email">Email (Optional for Login)</label>
<span asp-validation-for="RegisterInput.Email" class="text-danger"></span> <span asp-validation-for="Input.Email" class="text-danger"></span>
</div> </div>
<div class="form-floating mb-3"> <div class="d-grid gap-2 d-md-flex justify-content-md-between">
<input asp-for="RegisterInput.Password" class="form-control" type="password" autocomplete="current-password" aria-required="true" placeholder="password" /> <button type="submit" asp-page-handler="Login" class="btn btn-lg btn-secondary flex-grow-1 me-2" id="loginButton">Login</button>
<label asp-for="RegisterInput.Password">Password</label> <button type="submit" asp-page-handler="Register" class="btn btn-lg btn-primary flex-grow-1 ms-2">Register</button>
<span asp-validation-for="RegisterInput.Password" class="text-danger"></span>
</div>
<div>
<button type="submit" asp-page-handler="Register" class="w-100 btn btn-lg btn-primary">Login</button>
</div> </div>
</form> </form>
</div> </div>
@ -76,13 +47,19 @@
<partial name="_ValidationScriptsPartial" /> <partial name="_ValidationScriptsPartial" />
<script> <script>
$(document).ready(function() { $(document).ready(function() {
// when login button is clicked, remove validation from email field // When the login button is clicked
$("button[asp-page-handler='Login']").click(function() { $("#loginButton").click(function() {
$("#Input_Email").removeAttr("data-val"); var emailInput = $("#Input_Email");
$("#Input_Email").removeAttr("data-val-required"); var form = $("#authForm");
// revalidate the form to clear error
$("form").validate().element("#Input_Email"); // Remove the data-val-required attribute for client-side validation
return true; emailInput.removeAttr("data-val-required");
// If jQuery Validate has already initialized the form,
// re-parse the validation for the email field to reflect the change.
if (form.data('validator')) {
form.validate().element(emailInput);
}
}); });
}); });
</script> </script>

View file

@ -7,160 +7,153 @@ using System.Threading.Tasks;
using CSR.Application.Interfaces; using CSR.Application.Interfaces;
using CSR.Domain.Entities; using CSR.Domain.Entities;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using System.Security.Claims; using System.Security.Claims;
using System.Runtime.InteropServices;
// If you are using ASP.NET Core Identity for sign-in management, you might need:
// using Microsoft.AspNetCore.Identity;
// using System.Security.Claims;
public class AuthModel(IUserService userService) : PageModel public class AuthModel(IUserService userService) : PageModel
{ {
private readonly IUserService _userService = userService; private readonly IUserService _userService = userService;
[BindProperty] [BindProperty]
public LoginModel LoginInput { get; set; } = new(); public InputModel Input { get; set; } = new();
[BindProperty] public List<string> ErrorMessages { get; set; } = [];
public RegisterModel RegisterInput { get; set; } = new();
public List<string> LoginErrorMessages { get; set; } = []; public class InputModel
public List<string> RegisterErrorMessages { get; set; } = []; {
[Required(ErrorMessage = "Username is required.")]
[Display(Name = "Username")]
[StringLength(32, ErrorMessage = "Username cannot be longer than 32 characters.")]
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers and underscore.")]
public string Username { get; set; } = string.Empty;
public class LoginModel [EmailAddress]
[Display(Name = "Email")]
public string? Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required.")]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = string.Empty;
}
public void OnGet()
{
ErrorMessages.Clear();
}
public async Task<IActionResult> OnPostLoginAsync()
{
// Clear any previous registration errors
ModelState.Remove("Input.Email");
if (!ModelState.IsValid)
{ {
[Required(ErrorMessage = "Username is required.")] foreach (var modelState in ModelState.Values)
[Display(Name = "Username")] {
public string Username { get; set; } = string.Empty; foreach (var e in modelState.Errors)
{
[Required] ErrorMessages.Add(e.ErrorMessage);
[DataType(DataType.Password)] }
[Display(Name = "Password")] }
public string Password { get; set; } = string.Empty; return Page();
} }
public class RegisterModel var (user, error) = await _userService.Login(Input.Username, Input.Password);
if (user == null)
{ {
[Required(ErrorMessage = "Username is required.")] ErrorMessages.Add(error ?? "Login failed.");
[Display(Name = "Username")] Console.WriteLine(error);
[StringLength(32, ErrorMessage = "Username cannot be longer than 32 characters.")] return Page();
[RegularExpression(@"^[a-zA-Z0-9_]+$", ErrorMessage = "Username can only contain letters, numbers and underscore.")]
[DataType(DataType.Text)]
public string Username { get; set; } = string.Empty;
[Required(ErrorMessage = "Email is required for registration.")]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = string.Empty;
[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } = string.Empty;
} }
public void OnGet() await SendCookies(user.Username, user.Role.Name);
return RedirectToPageBasedOnRole(user);
}
public async Task<IActionResult> OnPostLogoutAsync()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
return RedirectToPage("/Index"); // Redirect to home page after logout
}
public async Task<IActionResult> OnPostRegisterAsync()
{
// Email is required for registration, so add a validation error if it's missing
if (string.IsNullOrWhiteSpace(Input.Email))
{ {
ModelState.AddModelError("Input.Email", "Email is required for registration.");
} }
public async Task<IActionResult> OnPostRegisterAsync() if (!ModelState.IsValid)
{ {
if (!ModelState.IsValid) foreach (var modelState in ModelState.Values)
{
foreach (var error in modelState.Errors)
{ {
RegisterErrorMessages.Add("Please correct the form errors."); ErrorMessages.Add(error.ErrorMessage);
return Page();
}
// Email is required for registration
if (string.IsNullOrWhiteSpace(RegisterInput.Email))
{
RegisterErrorMessages.Add("Email is required for registration.");
return Page();
}
var result = await _userService.RegisterNewUser(RegisterInput.Username, RegisterInput.Email, RegisterInput.Password);
if (result.User != null)
{
// Registration successful
await _userService.Login(RegisterInput.Username, RegisterInput.Password);
await SendCookies(result.User.Username, result.User.Role.Name);
return RedirectToPageBasedOnRole(result.User);
}
else
{
if (result.Errors != null)
{
RegisterErrorMessages.AddRange(result.Errors);
}
else
{
RegisterErrorMessages.Add("An unknown error occurred during registration.");
}
return Page();
} }
}
return Page();
} }
public async Task<IActionResult> OnPostLoginAsync() var result = await _userService.RegisterNewUser(Input.Username, Input.Email!, Input.Password);
if (result.User != null)
{ {
if (string.IsNullOrWhiteSpace(LoginInput.Username) || string.IsNullOrWhiteSpace(LoginInput.Password)) await _userService.Login(Input.Username, Input.Password);
{ await SendCookies(result.User.Username, result.User.Role.Name);
LoginErrorMessages.Add("Username and Password are required."); return RedirectToPageBasedOnRole(result.User);
return Page();
}
var (user, error) = await _userService.Login(LoginInput.Username, LoginInput.Password);
if (user == null)
{
LoginErrorMessages.Add(error ?? "Login failed.");
return Page();
}
await SendCookies(user.Username, user.Role.Name);
return RedirectToPageBasedOnRole(user);
} }
else
private async Task SendCookies(string username, string role)
{ {
var claims = new List<Claim> if (result.Errors != null)
{
ErrorMessages.AddRange(result.Errors);
}
else
{
ErrorMessages.Add("An unknown error occurred during registration.");
}
return Page();
}
}
private async Task SendCookies(string username, string role)
{
var claims = new List<Claim>
{ {
new(ClaimTypes.Name, username), new(ClaimTypes.Name, username),
new(ClaimTypes.Role, role) new(ClaimTypes.Role, role)
}; };
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
var authProperties = new AuthenticationProperties var authProperties = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(5)
};
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, authProperties);
}
private RedirectToPageResult RedirectToPageBasedOnRole(User user)
{ {
if (user == null || string.IsNullOrWhiteSpace(user.Role.Name)) IsPersistent = true,
{ ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(5)
// Default redirect if role is not defined or user is null };
return RedirectToPage("/Index");
}
return user.Role.Name switch await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, authProperties);
{ }
"Admin" => RedirectToPage("/Admin"),
"User" => RedirectToPage("/User"), private RedirectToPageResult RedirectToPageBasedOnRole(User user)
_ => RedirectToPage("/Index"), {
}; if (user == null || string.IsNullOrWhiteSpace(user.Role.Name))
{
return RedirectToPage("/Index");
} }
return user.Role.Name switch
{
"Admin" => RedirectToPage("/Admin"),
"User" => RedirectToPage("/User"),
_ => RedirectToPage("/Index"),
};
}
} }

View file

@ -19,9 +19,32 @@
</button> </button>
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
<ul class="navbar-nav flex-grow-1"> <ul class="navbar-nav flex-grow-1">
<li class="nav-item"> @if (User.Identity.IsAuthenticated)
<a class="nav-link text-dark" asp-area="" asp-page="/Auth">Login/Register</a> {
</li> if (User.IsInRole("Admin"))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Admin">[Admin Page]</a>
</li>
}
if (User.IsInRole("User") || User.IsInRole("Admin"))
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/User">[User Profile]</a>
</li>
}
<li class="nav-item">
<form class="form-inline" asp-page="/Auth" asp-page-handler="Logout" method="post">
<button type="submit" class="nav-link btn btn-link text-dark">[Logout]</button>
</form>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Auth">[Login or Register]</a>
</li>
}
</ul> </ul>
</div> </div>
</div> </div>

View file

@ -7,6 +7,6 @@
} }
}, },
"Database": { "Database": {
"Path": "/home/danial23/Desktop/csr.db" "Path": "/home/danial23/dl/csr.db"
} }
} }