From 27aaee6293cad48c726c74bd4970736f1e8974d1 Mon Sep 17 00:00:00 2001 From: danial23 Date: Tue, 20 May 2025 03:05:37 -0400 Subject: [PATCH] Create minimal app --- CSR.Application/Interfaces/IUserService.cs | 4 + CSR.Application/Services/UserService.cs | 16 ++ CSR.Domain/Entities/User.cs | 2 +- CSR.WebUI/Pages/Admin.cshtml | 9 ++ CSR.WebUI/Pages/Admin.cshtml.cs | 13 ++ CSR.WebUI/Pages/Auth.cshtml | 89 +++++++++++ CSR.WebUI/Pages/Auth.cshtml.cs | 166 +++++++++++++++++++++ CSR.WebUI/Pages/Error.cshtml.cs | 9 +- CSR.WebUI/Pages/Index.cshtml | 3 +- CSR.WebUI/Pages/Index.cshtml.cs | 9 +- CSR.WebUI/Pages/Privacy.cshtml | 8 - CSR.WebUI/Pages/Privacy.cshtml.cs | 19 --- CSR.WebUI/Pages/Shared/_Layout.cshtml | 11 +- CSR.WebUI/Pages/User.cshtml | 8 + CSR.WebUI/Pages/User.cshtml.cs | 15 ++ CSR.WebUI/Program.cs | 15 ++ CSR.WebUI/appsettings.Development.json | 2 +- 17 files changed, 346 insertions(+), 52 deletions(-) create mode 100644 CSR.WebUI/Pages/Admin.cshtml create mode 100644 CSR.WebUI/Pages/Admin.cshtml.cs create mode 100644 CSR.WebUI/Pages/Auth.cshtml create mode 100644 CSR.WebUI/Pages/Auth.cshtml.cs delete mode 100644 CSR.WebUI/Pages/Privacy.cshtml delete mode 100644 CSR.WebUI/Pages/Privacy.cshtml.cs create mode 100644 CSR.WebUI/Pages/User.cshtml create mode 100644 CSR.WebUI/Pages/User.cshtml.cs diff --git a/CSR.Application/Interfaces/IUserService.cs b/CSR.Application/Interfaces/IUserService.cs index 4f854bf..1e93ef6 100644 --- a/CSR.Application/Interfaces/IUserService.cs +++ b/CSR.Application/Interfaces/IUserService.cs @@ -10,4 +10,8 @@ public interface IUserService string email, string password ); + + record LoginResult(Domain.Entities.User? User, string? Error); + + Task Login(string username, string password); } diff --git a/CSR.Application/Services/UserService.cs b/CSR.Application/Services/UserService.cs index c02321f..26ac506 100644 --- a/CSR.Application/Services/UserService.cs +++ b/CSR.Application/Services/UserService.cs @@ -50,4 +50,20 @@ public class UserService(IUserRepository userRepository, Domain.Interfaces.IPass return new IUserService.RegisterNewUserResult(user, null); } + + public async Task Login(string username, string password) + { + var user = await _userRepository.GetByUsernameAsync(username); + if (user == null) + { + return new IUserService.LoginResult(null, "User not found."); + } + + if (!user.VerifyPassword(password, _passwordHasher)) + { + return new IUserService.LoginResult(null, "Wrong password."); + } + + return new IUserService.LoginResult(user, null); + } } diff --git a/CSR.Domain/Entities/User.cs b/CSR.Domain/Entities/User.cs index 5400a21..3dd55be 100644 --- a/CSR.Domain/Entities/User.cs +++ b/CSR.Domain/Entities/User.cs @@ -71,7 +71,7 @@ public class User } - public bool VerifyPasswordAgainstHash(string providedPassword, IPasswordHasher passwordHasher) + public bool VerifyPassword(string providedPassword, IPasswordHasher passwordHasher) { return passwordHasher.VerifyHashedPassword(this, PasswordHash, providedPassword); } diff --git a/CSR.WebUI/Pages/Admin.cshtml b/CSR.WebUI/Pages/Admin.cshtml new file mode 100644 index 0000000..4cb7b29 --- /dev/null +++ b/CSR.WebUI/Pages/Admin.cshtml @@ -0,0 +1,9 @@ + +@page +@model AdminPageModel +@{ + ViewData["Title"] = "Admin"; +} +

@ViewData["Title"]

+ +

page full of admin stuff

diff --git a/CSR.WebUI/Pages/Admin.cshtml.cs b/CSR.WebUI/Pages/Admin.cshtml.cs new file mode 100644 index 0000000..7c66dc0 --- /dev/null +++ b/CSR.WebUI/Pages/Admin.cshtml.cs @@ -0,0 +1,13 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace CSR.WebUI.Pages +{ + [Authorize(Policy = "AdminOnly")] + public class AdminPageModel : PageModel + { + public void OnGet() + { + } + } +} \ No newline at end of file diff --git a/CSR.WebUI/Pages/Auth.cshtml b/CSR.WebUI/Pages/Auth.cshtml new file mode 100644 index 0000000..a15fc3d --- /dev/null +++ b/CSR.WebUI/Pages/Auth.cshtml @@ -0,0 +1,89 @@ +@page +@model CSR.WebUI.Pages.AuthModel +@{ + ViewData["Title"] = "Authenticate"; +} + +
+
+
+

Login

+
+ @if (Model.LoginErrorMessages != null && Model.LoginErrorMessages.Any()) + { + + } +
+ + + +
+
+ + + +
+
+ +
+
+
+
+
+

Register

+
+ @if (Model.RegisterErrorMessages != null && Model.RegisterErrorMessages.Any()) + { + + } +
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+
+
+
+ +@section Scripts { + + +} \ No newline at end of file diff --git a/CSR.WebUI/Pages/Auth.cshtml.cs b/CSR.WebUI/Pages/Auth.cshtml.cs new file mode 100644 index 0000000..4010308 --- /dev/null +++ b/CSR.WebUI/Pages/Auth.cshtml.cs @@ -0,0 +1,166 @@ +namespace CSR.WebUI.Pages; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using CSR.Application.Interfaces; +using CSR.Domain.Entities; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +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 +{ + private readonly IUserService _userService = userService; + + [BindProperty] + public LoginModel LoginInput { get; set; } = new(); + + [BindProperty] + public RegisterModel RegisterInput { get; set; } = new(); + + public List LoginErrorMessages { get; set; } = []; + public List RegisterErrorMessages { get; set; } = []; + + public class LoginModel + { + [Required(ErrorMessage = "Username is required.")] + [Display(Name = "Username")] + public string Username { get; set; } = string.Empty; + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } = string.Empty; + } + + public class RegisterModel + { + [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.")] + [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() + { + } + + public async Task OnPostRegisterAsync() + { + if (!ModelState.IsValid) + { + RegisterErrorMessages.Add("Please correct the form errors."); + 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(); + } + } + + public async Task OnPostLoginAsync() + { + if (string.IsNullOrWhiteSpace(LoginInput.Username) || string.IsNullOrWhiteSpace(LoginInput.Password)) + { + LoginErrorMessages.Add("Username and Password are required."); + 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); + } + + private async Task SendCookies(string username, string role) + { + var claims = new List + { + new(ClaimTypes.Name, username), + new(ClaimTypes.Role, role) + }; + + var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); + var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); + + 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)) + { + // Default redirect if role is not defined or user is null + return RedirectToPage("/Index"); + } + + return user.Role.Name switch + { + "Admin" => RedirectToPage("/Admin"), + "User" => RedirectToPage("/User"), + _ => RedirectToPage("/Index"), + }; + } +} \ No newline at end of file diff --git a/CSR.WebUI/Pages/Error.cshtml.cs b/CSR.WebUI/Pages/Error.cshtml.cs index 55fe3d9..a9e3500 100644 --- a/CSR.WebUI/Pages/Error.cshtml.cs +++ b/CSR.WebUI/Pages/Error.cshtml.cs @@ -6,18 +6,13 @@ namespace CSR.WebUI.Pages; [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] [IgnoreAntiforgeryToken] -public class ErrorModel : PageModel +public class ErrorModel(ILogger logger) : PageModel { public string? RequestId { get; set; } public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - private readonly ILogger _logger; - - public ErrorModel(ILogger logger) - { - _logger = logger; - } + private readonly ILogger _logger = logger; public void OnGet() { diff --git a/CSR.WebUI/Pages/Index.cshtml b/CSR.WebUI/Pages/Index.cshtml index 03f7f0f..9b8831b 100644 --- a/CSR.WebUI/Pages/Index.cshtml +++ b/CSR.WebUI/Pages/Index.cshtml @@ -1,10 +1,9 @@ @page @model IndexModel @{ - ViewData["Title"] = "Home page"; + ViewData["Title"] = "Home"; }

Welcome

-

Learn about building Web apps with ASP.NET Core.

diff --git a/CSR.WebUI/Pages/Index.cshtml.cs b/CSR.WebUI/Pages/Index.cshtml.cs index 18a1b5a..19defb8 100644 --- a/CSR.WebUI/Pages/Index.cshtml.cs +++ b/CSR.WebUI/Pages/Index.cshtml.cs @@ -3,14 +3,9 @@ using Microsoft.AspNetCore.Mvc.RazorPages; namespace CSR.WebUI.Pages; -public class IndexModel : PageModel +public class IndexModel(ILogger logger) : PageModel { - private readonly ILogger _logger; - - public IndexModel(ILogger logger) - { - _logger = logger; - } + private readonly ILogger _logger = logger; public void OnGet() { diff --git a/CSR.WebUI/Pages/Privacy.cshtml b/CSR.WebUI/Pages/Privacy.cshtml deleted file mode 100644 index 5c16860..0000000 --- a/CSR.WebUI/Pages/Privacy.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@page -@model PrivacyModel -@{ - ViewData["Title"] = "Privacy Policy"; -} -

@ViewData["Title"]

- -

Use this page to detail your site's privacy policy.

diff --git a/CSR.WebUI/Pages/Privacy.cshtml.cs b/CSR.WebUI/Pages/Privacy.cshtml.cs deleted file mode 100644 index ad84265..0000000 --- a/CSR.WebUI/Pages/Privacy.cshtml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace CSR.WebUI.Pages; - -public class PrivacyModel : PageModel -{ - private readonly ILogger _logger; - - public PrivacyModel(ILogger logger) - { - _logger = logger; - } - - public void OnGet() - { - } -} - diff --git a/CSR.WebUI/Pages/Shared/_Layout.cshtml b/CSR.WebUI/Pages/Shared/_Layout.cshtml index 01b5588..1ed7a9a 100644 --- a/CSR.WebUI/Pages/Shared/_Layout.cshtml +++ b/CSR.WebUI/Pages/Shared/_Layout.cshtml @@ -3,7 +3,7 @@ - @ViewData["Title"] - CSR.WebUI + @ViewData["Title"] Dan Mehr @@ -12,7 +12,7 @@