register user and lawyers

This commit is contained in:
jojo aquino 2024-12-14 22:29:16 +00:00
parent 87ab6a874e
commit f1b13b2877
16 changed files with 460 additions and 54 deletions

View File

@ -52,5 +52,8 @@ namespace EnotaryoPH.Data.Entities
[Column("Status")] [Column("Status")]
public string? Status { get; set; } public string? Status { get; set; }
[ForeignKey("UserID")]
public User User { get; set; }
} }
} }

View File

@ -13,5 +13,6 @@ namespace EnotaryoPH.Data
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(_configuration.GetConnectionString("NotaryoDatabase")); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.UseNpgsql(_configuration.GetConnectionString("NotaryoDatabase"));
public DbSet<User>? Users { get; set; } public DbSet<User>? Users { get; set; }
public DbSet<Lawyer>? Lawyers { get; set; }
} }
} }

View File

@ -0,0 +1,9 @@
namespace EnotaryoPH.Web.Common.Services
{
public interface IPasswordService
{
string HashPassword(string password);
bool VerifyHashedPassword(string hashedPassword, string providedPassword);
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
@page
@model EnotaryoPH.Web.Pages.ForgotPasswordModel
@{
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EnotaryoPH.Web.Pages
{
public class ForgotPasswordModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,29 @@
@page
@model EnotaryoPH.Web.Pages.LoginModel
@{
}
<div class="container">
<h1 class="my-4">Login</h1>
<div class="row">
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
<form>
<div class="row">
<div class="col"><label class="form-label">Email</label><input id="Email" class="form-control mb-3" type="email" /></div>
</div>
<div class="row">
<div class="col"><label class="form-label">Password</label><input id="Password-2" class="form-control mb-3" type="password" /></div>
</div>
<div class="row mt-3 mb-0">
<div class="col">
<button class="btn btn-primary btn-lg" type="button">Login</button>
<a href="/Register" class="btn btn-outline-primary btn-lg ms-1" type="button">Register </a>
</div>
</div>
<div class="row mb-5 mt-2">
<div class="col"><a href="#">Forgot password?</a></div>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace EnotaryoPH.Web.Pages
{
public class LoginModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -0,0 +1,4 @@
@page
@model EnotaryoPH.Web.Pages.Principal.DashboardModel
@{
}

View File

@ -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()
{
}
}
}

View File

@ -0,0 +1,72 @@
@page
@model EnotaryoPH.Web.Pages.RegisterModel
@{
}
<div class="container">
<h1 class="my-4">Sign Up</h1>
<div class="row">
<div class="col-md-9 col-lg-12">
<form method="post" asp-action="Register">
<div class="row">
<div class="col-12 col-lg-3">
<label class="form-label" for="Email">Email</label>
<input id="Email" class="form-control" type="email" required asp-for="Email" />
@Html.ValidationMessageFor(x => x.Email)
</div>
<div class="col-12 col-lg-3"><label class="form-label" for="Password">Password</label><input id="Password" class="form-control" type="password" required asp-for="Password" />
@Html.ValidationMessageFor(x => x.Password)
</div>
<div class="col-12 col-lg-3"><label class="form-label" for="ConfirmPassword">Confirm Password</label><input id="ConfirmPassword" class="form-control" type="password" required asp-for="ConfirmPassword" />
@Html.ValidationMessageFor(x => x.ConfirmPassword)
</div>
</div>
<div class="row">
<div class="col-12 col-lg-3"><label class="form-label" for="PhoneNumber">Phone number</label><input id="PhoneNumber" class="form-control mb-3" type="text" asp-for="PhoneNumber" /></div>
<div class="col-12 col-lg-3"><label class="form-label" for="BirthDate">Birthdate</label><input id="BirthDate" class="form-control mb-3" type="date" required asp-for="BirthDate" /></div>
</div>
<div class="row mt-3">
<div class="col">
<label class="form-label">Register As</label>
<div>
<div class="form-check form-check-inline"><input id="IsPrincipal" class="form-check-input" type="radio" name="RoleType" checked value="Principal" asp-for="RoleType" /><label class="form-check-label" for="IsPrincipal">Principal</label></div>
<div class="form-check form-check-inline"><input id="IsNotaryPublic" class="form-check-input" type="radio" name="RoleType" value="Notary Public" asp-for="RoleType" /><label class="form-check-label" for="IsNotaryPublic">Notary Public </label></div>
</div>
</div>
</div>
<div id="LawyerFields">
<div class="row mt-4">
<div class="col-12 col-lg-3"><label class="form-label" for="RollNumber">Roll Number</label><input id="RollNumber" class="form-control mb-3" type="text" required asp-for=RollNumber /></div>
<div class="col-12 col-lg-3"><label class="form-label">IBP Number</label><input id="IBPNumber" class="form-control mb-3" type="text" required asp-for="IBPNumber" /></div>
<div class="col-12 col-lg-3"><label class="form-label" for="MCLEComplianceNumber">MCLE Compliance Number</label><input id="MCLEComplianceNumber" class="form-control mb-3" type="text" required asp-for="MCLEComplianceNumber" /></div>
<div class="col-12 col-lg-3"><label class="form-label" for="MCLEDate">MCLE Date</label><input id="MCLEDate" class="form-control mb-3" type="date" required asp-for="MCLEDate" /></div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-3"><label class="form-label" for="PTRNumber">PTR Number</label><input id="PTRNumber" class="form-control mb-3" type="text" required asp-for="PTRNumber" /></div>
<div class="col-12 col-lg-3"><label class="form-label" for="PTRDate">PTR Date</label><input id="PTRDate" class="form-control mb-3" type="date" required asp-for="PTRDate" /></div>
<div class="col"><label class="form-label" for="PTRLocation">PTR Location</label><input id="PTRLocation" class="form-control mb-3" type="text" required asp-for="PTRLocation" /></div>
</div>
<div class="row mt-3">
<div class="col-12 col-lg-3"><label class="form-label">Commission Number</label><input id="CommissionNumber" class="form-control mb-3" type="text" required asp-for="CommissionNumber" /></div>
<div class="col-12 col-lg-3"><label class="form-label">Commision Expiration</label><input id="CommisionExpiration" class="form-control mb-3" type="date" required asp-for="CommissionExpiration" /></div>
<div class="col"><label class="form-label" for="CommissionLocation">Commission Location</label><input id="CommissionLocation" class="form-control mb-3" type="text" required asp-for="CommissionLocation" /></div>
</div>
<div class="row mt-3">
<div class="col"><label class="form-label" for="OfficeAddress">Office Address</label><input id="OfficeAddress" class="form-control mb-3" type="text" required asp-for="OfficeAddress" /></div>
</div>
</div>
<div class="row mt-5">
<div class="col">
<div class="form-check"><input id="EighteenYearsOrOlder" class="form-check-input" type="checkbox" required asp-for="IsEighteenYearsOrOlder" /><label class="form-check-label" for="EighteenYearsOrOlder">I am 18 years old or older</label></div>
<button id="RegisterButton" class="btn btn-primary btn-lg" type="submit"><i class="fas fa-user-plus me-2"></i><span id="RegisterButtonText">Register as</span></button>
</div>
</div>
<input type="hidden" asp-for="RoleType" />
</form>
</div>
</div>
</div>
@section Scripts {
<script src="~/Pages/Register.cshtml.js"></script>
}

View File

@ -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; }
}
}

View File

@ -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();
})();

View File

@ -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;
}

View File

@ -1,4 +1,5 @@
using EnotaryoPH.Data; using EnotaryoPH.Data;
using EnotaryoPH.Web.Common.Services;
namespace EnotaryoPH.Web namespace EnotaryoPH.Web
{ {
@ -10,8 +11,8 @@ namespace EnotaryoPH.Web
// Add services to the container. // Add services to the container.
builder.Services.AddRazorPages(); builder.Services.AddRazorPages();
builder.Services.AddDbContext<NotaryoDBContext>(); builder.Services.AddDbContext<NotaryoDBContext>();
builder.Services.AddTransient<IPasswordService, PasswordService>();
var app = builder.Build(); var app = builder.Build();

View File

@ -116,3 +116,14 @@ a span.sidemenu__menuitem__text {
max-width: 800px; 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)
}