From f1b13b28772ec6e8a281996fe4436f8778e8bc83 Mon Sep 17 00:00:00 2001 From: jojo aquino Date: Sat, 14 Dec 2024 22:29:16 +0000 Subject: [PATCH] register user and lawyers --- EnotaryoPH/EnotaryoPH.Data/Entities/Lawyer.cs | 3 + .../EnotaryoPH.Data/NotaryoDBContext.cs | 1 + .../Common/Services/IPasswordService.cs | 9 ++ .../Common/Services/PasswordService.cs | 98 +++++++++++ .../Pages/ForgotPassword.cshtml | 4 + .../Pages/ForgotPassword.cshtml.cs | 12 ++ EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml | 29 ++++ .../EnotaryoPH.Web/Pages/Login.cshtml.cs | 12 ++ .../Pages/Principal/Dashboard.cshtml | 4 + .../Pages/Principal/Dashboard.cshtml.cs | 12 ++ .../EnotaryoPH.Web/Pages/Register.cshtml | 72 +++++++++ .../EnotaryoPH.Web/Pages/Register.cshtml.cs | 152 ++++++++++++++++++ .../EnotaryoPH.Web/Pages/Register.cshtml.js | 38 +++++ .../Pages/Shared/_Layout.cshtml.css | 54 +------ EnotaryoPH/EnotaryoPH.Web/Program.cs | 3 +- .../EnotaryoPH.Web/wwwroot/css/enotaryo.css | 11 ++ 16 files changed, 460 insertions(+), 54 deletions(-) create mode 100644 EnotaryoPH/EnotaryoPH.Web/Common/Services/IPasswordService.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Common/Services/PasswordService.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.cs create mode 100644 EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.js diff --git a/EnotaryoPH/EnotaryoPH.Data/Entities/Lawyer.cs b/EnotaryoPH/EnotaryoPH.Data/Entities/Lawyer.cs index 21a81da..7955235 100644 --- a/EnotaryoPH/EnotaryoPH.Data/Entities/Lawyer.cs +++ b/EnotaryoPH/EnotaryoPH.Data/Entities/Lawyer.cs @@ -52,5 +52,8 @@ namespace EnotaryoPH.Data.Entities [Column("Status")] public string? Status { get; set; } + + [ForeignKey("UserID")] + public User User { get; set; } } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Data/NotaryoDBContext.cs b/EnotaryoPH/EnotaryoPH.Data/NotaryoDBContext.cs index e6a775d..6b403c7 100644 --- a/EnotaryoPH/EnotaryoPH.Data/NotaryoDBContext.cs +++ b/EnotaryoPH/EnotaryoPH.Data/NotaryoDBContext.cs @@ -13,5 +13,6 @@ namespace EnotaryoPH.Data protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(_configuration.GetConnectionString("NotaryoDatabase")); public DbSet? Users { get; set; } + public DbSet? Lawyers { get; set; } } } \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/IPasswordService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IPasswordService.cs new file mode 100644 index 0000000..11b689f --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/IPasswordService.cs @@ -0,0 +1,9 @@ +namespace EnotaryoPH.Web.Common.Services +{ + public interface IPasswordService + { + string HashPassword(string password); + + bool VerifyHashedPassword(string hashedPassword, string providedPassword); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Common/Services/PasswordService.cs b/EnotaryoPH/EnotaryoPH.Web/Common/Services/PasswordService.cs new file mode 100644 index 0000000..d847308 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Common/Services/PasswordService.cs @@ -0,0 +1,98 @@ +using System.Security.Cryptography; +using Microsoft.AspNetCore.Cryptography.KeyDerivation; + +namespace EnotaryoPH.Web.Common.Services +{ + public class PasswordService : IPasswordService + { + private readonly int _iterCount = 100_000; + private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + + public string HashPassword(string password) => Convert.ToBase64String(HashPasswordV3(password, _rng)); + + public bool VerifyHashedPassword(string hashedPassword, string providedPassword) + { + var decodedHashedPassword = Convert.FromBase64String(hashedPassword); + return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out var embeddedIterCount, out var prf); + } + + private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) + { + // Produce a version 3 (see comment above) text hash. + var salt = new byte[saltSize]; + rng.GetBytes(salt); + var subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); + + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + outputBytes[0] = 0x01; // format marker + WriteNetworkByteOrder(outputBytes, 1, (uint)prf); + WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); + WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); + return outputBytes; + } + + private static uint ReadNetworkByteOrder(byte[] buffer, int offset) + => ((uint)(buffer[offset + 0]) << 24) + | ((uint)(buffer[offset + 1]) << 16) + | ((uint)(buffer[offset + 2]) << 8) + | buffer[offset + 3]; + + private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount, out KeyDerivationPrf prf) + { + iterCount = default; + prf = default; + + try + { + // Read header information + prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); + iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); + var saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < 128 / 8) + { + return false; + } + var salt = new byte[saltLength]; + Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + var subkeyLength = hashedPassword.Length - 13 - salt.Length; + if (subkeyLength < 128 / 8) + { + return false; + } + var expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + var actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); + return CryptographicOperations.FixedTimeEquals(actualSubkey, expectedSubkey); + } + catch + { + // This should never occur except in the case of a malformed payload, where + // we might go off the end of the array. Regardless, a malformed payload + // implies verification failed. + return false; + } + } + + private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) + { + buffer[offset + 0] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)(value >> 0); + } + + private byte[] HashPasswordV3(string password, RandomNumberGenerator rng) => HashPasswordV3(password, rng, + prf: KeyDerivationPrf.HMACSHA512, + iterCount: _iterCount, + saltSize: 128 / 8, + numBytesRequested: 256 / 8); + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml new file mode 100644 index 0000000..68092dc --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml @@ -0,0 +1,4 @@ +@page +@model EnotaryoPH.Web.Pages.ForgotPasswordModel +@{ +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml.cs new file mode 100644 index 0000000..8a8abfb --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/ForgotPassword.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EnotaryoPH.Web.Pages +{ + public class ForgotPasswordModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml new file mode 100644 index 0000000..eb02c9d --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml @@ -0,0 +1,29 @@ +@page +@model EnotaryoPH.Web.Pages.LoginModel +@{ +} + +
+

Login

+
+
+
+
+
+
+
+
+
+
+
+ + Register  +
+
+ +
+
+
+
\ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml.cs new file mode 100644 index 0000000..f1d4420 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Login.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EnotaryoPH.Web.Pages +{ + public class LoginModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml new file mode 100644 index 0000000..9e420e2 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml @@ -0,0 +1,4 @@ +@page +@model EnotaryoPH.Web.Pages.Principal.DashboardModel +@{ +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml.cs new file mode 100644 index 0000000..d5826fa --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Principal/Dashboard.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace EnotaryoPH.Web.Pages.Principal +{ + public class DashboardModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml new file mode 100644 index 0000000..5578564 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml @@ -0,0 +1,72 @@ +@page +@model EnotaryoPH.Web.Pages.RegisterModel +@{ +} + +
+

Sign Up

+
+
+
+
+
+ + + @Html.ValidationMessageFor(x => x.Email) +
+
+ @Html.ValidationMessageFor(x => x.Password) +
+
+ @Html.ValidationMessageFor(x => x.ConfirmPassword) +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ +@section Scripts { + +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.cs b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.cs new file mode 100644 index 0000000..eebf811 --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.cs @@ -0,0 +1,152 @@ +using System.ComponentModel.DataAnnotations; +using EnotaryoPH.Data; +using EnotaryoPH.Data.Entities; +using EnotaryoPH.Web.Common.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; + +namespace EnotaryoPH.Web.Pages +{ + public class RegisterModel : PageModel + { + private readonly NotaryoDBContext _notaryoDBContext; + private readonly IPasswordService _passwordService; + + public RegisterModel(NotaryoDBContext notaryoDBContext, IPasswordService passwordService) + { + _notaryoDBContext = notaryoDBContext; + _passwordService = passwordService; + } + + public void OnGet() + { + RoleType = "Principal"; +#if DEBUG + RollNumber = "ROLL1234"; + IBPNumber = "IBP1234"; + MCLEComplianceNumber = "MCLE1234"; + MCLEDate = new DateTime(2023, 1, 15, 0, 0, 0, DateTimeKind.Utc); + PTRDate = new DateTime(2023, 1, 15, 0, 0, 0, DateTimeKind.Utc); + PTRLocation = "ptr location"; + PTRNumber = "PTR98723"; + CommissionExpiration = new DateTime(2023, 1, 15, 0, 0, 0, DateTimeKind.Utc); + CommissionLocation = "COMM LOC 8732"; + CommissionNumber = "COMM NO 8392"; + OfficeAddress = "123 Fictional Road, NY, Cubao"; + + PhoneNumber = "639151220001"; + BirthDate = new DateTime(1979, 9, 10, 0, 0, 0, DateTimeKind.Utc); + Password = "arst1234"; + ConfirmPassword = "arst1234"; + + +#endif + } + + public IActionResult OnPost() + { + if (!ModelState.IsValid) + { + return Page(); + } + + var hasDuplicateEmail = _notaryoDBContext.Users.Any(u => EF.Functions.Like(u.Email, Email)); + if (hasDuplicateEmail) + { + ModelState.AddModelError(nameof(Email), "That email already exists in the database."); + return Page(); + } + + var newUser = new User + { + BirthDate = new DateTime(BirthDate.Ticks, DateTimeKind.Utc), + Email = Email, + PasswordHash = _passwordService.HashPassword(Password), + PhoneNumber = PhoneNumber, + Role = RoleType + }; + _notaryoDBContext.Users.Add(newUser); + + if (RoleType == "Notary Public") + { + var newLawyer = new Lawyer + { + User = newUser, + CommissionExpiration = new DateTime(CommissionExpiration.Value.Ticks, DateTimeKind.Utc), + CommissionLocation = CommissionLocation, + CommissionNumber = CommissionNumber, + IBPNumber = IBPNumber, + MCLEComplianceNumber = MCLEComplianceNumber, + MCLEDate = new DateTime(MCLEDate.Value.Ticks, DateTimeKind.Utc), + OfficeAddress = OfficeAddress, + PTRDate = new DateTime(PTRDate.Value.Ticks, DateTimeKind.Utc), + PTRlocation = PTRLocation, + PTRNumber = PTRNumber, + Rollnumber = RollNumber, + Status = "New" + }; + _notaryoDBContext.Lawyers.Add(newLawyer); + } + + _notaryoDBContext.SaveChanges(); + + return RedirectToPage("/Principal/Dashboard"); + } + + [BindProperty] + public string Email { get; set; } + + [BindProperty] + public string Password { get; set; } + + [BindProperty] + [Compare(nameof(Password))] + public string ConfirmPassword { get; set; } + + [BindProperty] + public string PhoneNumber { get; set; } + + [BindProperty] + public DateTime BirthDate { get; set; } + + [BindProperty] + public string RoleType { get; set; } + + [BindProperty] + public string? RollNumber { get; set; } + + [BindProperty] + public string? IBPNumber { get; set; } + + [BindProperty] + public string? MCLEComplianceNumber { get; set; } + + [BindProperty] + public DateTime? MCLEDate { get; set; } + + [BindProperty] + public string? PTRNumber { get; set; } + + [BindProperty] + public DateTime? PTRDate { get; set; } + + [BindProperty] + public string? PTRLocation { get; set; } + + [BindProperty] + public string? CommissionNumber { get; set; } + + [BindProperty] + public DateTime? CommissionExpiration { get; set; } + + [BindProperty] + public string? CommissionLocation { get; set; } + + [BindProperty] + public string? OfficeAddress { get; set; } + + [BindProperty] + public bool IsEighteenYearsOrOlder { get; set; } + } +} \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.js b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.js new file mode 100644 index 0000000..95d994a --- /dev/null +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Register.cshtml.js @@ -0,0 +1,38 @@ +"use strict"; +(function () { + const + control_isNotaryPublic = document.getElementById('IsNotaryPublic'), + control_isPrincipal = document.getElementById('IsPrincipal'), + control_lawyerFields = document.getElementById('LawyerFields'), + control_registerButtonText = document.getElementById('RegisterButtonText'), + control_roleType = document.getElementById('RoleType'), + x = 1; + + function _bindEvents() { + control_isNotaryPublic.addEventListener('change', _roleTypeChanged); + control_isPrincipal.addEventListener('change', _roleTypeChanged); + } + + function _roleTypeChanged(sender) { + let roleType = sender?.target?.value ?? sender?.value ?? 'Principal'; + control_roleType.value = roleType; + control_registerButtonText.textContent = 'Register as ' + roleType; + if (roleType !== 'Principal') { + control_lawyerFields.style.display = 'block'; + let requiredFields = control_lawyerFields.querySelectorAll('[required]'); + requiredFields.forEach(f => f.disabled = false); + } + else { + control_lawyerFields.style.display = 'none'; + let requiredFields = control_lawyerFields.querySelectorAll('[required]'); + requiredFields.forEach(f => f.disabled = true); + } + } + + function _init() { + _roleTypeChanged(control_roleType); + _bindEvents(); + } + + _init(); +})(); \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Layout.cshtml.css b/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Layout.cshtml.css index 4be97d1..5f28270 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Layout.cshtml.css +++ b/EnotaryoPH/EnotaryoPH.Web/Pages/Shared/_Layout.cshtml.css @@ -1,53 +1 @@ -/* Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification -for details on configuring this project to bundle and minify static web assets. */ - -a.navbar-brand { - white-space: normal; - text-align: center; - word-break: break-all; -} - -a { - color: #0077cc; -} - -.btn-primary { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.nav-pills .nav-link.active, .nav-pills .show > .nav-link { - color: #fff; - background-color: #1b6ec2; - border-color: #1861ac; -} - -.border-top { - border-top: 1px solid #e5e5e5; -} - -.border-bottom { - border-bottom: 1px solid #e5e5e5; -} - -.box-shadow { - box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); -} - -button.accept-policy { - font-size: 1rem; - line-height: inherit; -} - -.footer { - position: absolute; - bottom: 0; - width: 100%; - white-space: nowrap; - line-height: 60px; -} - -body { - background-color: red; -} \ No newline at end of file + \ No newline at end of file diff --git a/EnotaryoPH/EnotaryoPH.Web/Program.cs b/EnotaryoPH/EnotaryoPH.Web/Program.cs index 7a2a6b2..94622c8 100644 --- a/EnotaryoPH/EnotaryoPH.Web/Program.cs +++ b/EnotaryoPH/EnotaryoPH.Web/Program.cs @@ -1,4 +1,5 @@ using EnotaryoPH.Data; +using EnotaryoPH.Web.Common.Services; namespace EnotaryoPH.Web { @@ -10,8 +11,8 @@ namespace EnotaryoPH.Web // Add services to the container. builder.Services.AddRazorPages(); - builder.Services.AddDbContext(); + builder.Services.AddTransient(); var app = builder.Build(); diff --git a/EnotaryoPH/EnotaryoPH.Web/wwwroot/css/enotaryo.css b/EnotaryoPH/EnotaryoPH.Web/wwwroot/css/enotaryo.css index 64921ed..ab0fa97 100644 --- a/EnotaryoPH/EnotaryoPH.Web/wwwroot/css/enotaryo.css +++ b/EnotaryoPH/EnotaryoPH.Web/wwwroot/css/enotaryo.css @@ -116,3 +116,14 @@ a span.sidemenu__menuitem__text { max-width: 800px; } +.field-validation-error { + color: var(--bs-danger); + font-size: small; + margin-bottom: 15px; + display: block; + font-size: small; +} + +.input-validation-error { + border: solid 1px var(--bs-danger) +} \ No newline at end of file