Complete account creation process

This commit is contained in:
Astrian Zheng 2023-09-15 17:28:39 +10:00
parent 13ed90803c
commit 007d56ee32
6 changed files with 126 additions and 60 deletions

View File

@ -16,11 +16,34 @@ using System.IO;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security; using Org.BouncyCastle.Security;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Net.Http.Headers;
namespace FIT5032_Assignment.Controllers namespace FIT5032_Assignment.Controllers
{ {
// Endpoint Response
public class PassageUserReply
{
public PassageUserReplyUser User { get; set; }
}
public class PassageUserReplyUser
{
public string Email { get; set; }
}
// Database
public class Database1Entities : DbContext
{
public DbSet<Users> Users { get; set; }
public DbSet<Credentials> Credentials { get; set; }
public DbSet<Sessions> Sessions { get; set; }
}
public class HomeController : Controller public class HomeController : Controller
{ {
private static readonly HttpClient httpClient = new HttpClient();
public static RsaSecurityKey LoadRsaSecurityKeyFromPem(string pem) public static RsaSecurityKey LoadRsaSecurityKeyFromPem(string pem)
{ {
TextReader textReader = new StringReader(pem); TextReader textReader = new StringReader(pem);
@ -102,6 +125,15 @@ namespace FIT5032_Assignment.Controllers
return new string(bytes.Select(x => validChars[x % validChars.Length]).ToArray()); return new string(bytes.Select(x => validChars[x % validChars.Length]).ToArray());
} }
public HomeController()
{
// if auth token setted, ignore
if (httpClient.DefaultRequestHeaders.Authorization == null)
{
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vF4ch1wUf8.1cqOms9JMmUbqMGohlkJLzGDVlbF51D03fJnLfxwkn8kyAaVVjfvySufW9vXb3p3");
}
}
public ActionResult Index() public ActionResult Index()
{ {
return View(); return View();
@ -125,41 +157,51 @@ namespace FIT5032_Assignment.Controllers
} }
else else
{ {
Trace.WriteLine(sub); var db = new Database1Entities();
var credential = db.Credentials.Where(res => (res.uniqueIdCode == sub) && (res.provider == 0));
// Register: if no credential, redirect to create account
if (credential.Count() == 0)
{
return RedirectToAction("CompleteProfile");
}
else
{
// Successful login
return RedirectToAction("Index");
}
} }
return View();
} }
// POST /CreateAccount // POST /ComplteteProfile
[HttpPost] [HttpPost]
public ActionResult CreateAccount(Models.CreateAccountForm model) public async Task<ActionResult> CompleteProfile(Models.CompleteProfileForm model)
{ {
if (!ModelState.IsValid) if (!ModelState.IsValid)
{ {
ModelState.AddModelError("emailaddress", "Form not valid"); ModelState.AddModelError("fullname", "Form not valid");
return View(model); return View(model);
} }
// Email address is not valid // Verify user is logged in
// Use regular expression to verify email address var psg_auth_token = Request.Cookies["psg_auth_token"];
if (!System.Text.RegularExpressions.Regex.IsMatch(model.emailaddress, @"^([a-zA-Z0-9]|\-|_|\.){1,}@([a-zA-Z0-9]|\-|_|){1,}(\.([a-zA-Z0-9]|\-|_){1,}){1,}$")) var user = loginVerify(psg_auth_token.Value);
if (user == null)
{ {
ModelState.AddModelError("emailaddress", "Email address is not valid"); return RedirectToAction("Login");
return View(model);
} }
// If email address existed in database // Get users email from Passage API
var users = db.Credentials.Where(res => (res.uniqueIdCode == model.emailaddress) && (res.provider == 0)); var app_id = "ZHM5whW5xsZEczTn2loffzjN";
if (users.Count() > 0) var user_id = user;
{ var url = $"https://api.passage.id/v1/apps/{app_id}/users/{user_id}";
ModelState.AddModelError("emailaddress", "Email address existed"); Trace.WriteLine(url);
return View(model); Trace.WriteLine(httpClient.DefaultRequestHeaders.Authorization);
} var res = await httpClient.GetStringAsync(url);
string emailaddress = JsonConvert.DeserializeObject<PassageUserReply>(res).User.Email;
// MD5 hash email to get avatar from gravatar // MD5 hash email to get avatar from gravatar
var md5 = MD5.Create(); var md5 = MD5.Create();
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(model.emailaddress); byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(emailaddress);
byte[] hash = md5.ComputeHash(inputBytes); byte[] hash = md5.ComputeHash(inputBytes);
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (int i = 0; i < hash.Length; i++) for (int i = 0; i < hash.Length; i++)
@ -170,7 +212,7 @@ namespace FIT5032_Assignment.Controllers
// Create a new credential and a new user // Create a new credential and a new user
string userUuid = Guid.NewGuid().ToString(); string userUuid = Guid.NewGuid().ToString();
Users user = new Users Users newDbUser = new Users
{ {
uuid = userUuid, uuid = userUuid,
displayName = model.fullname, displayName = model.fullname,
@ -181,47 +223,27 @@ namespace FIT5032_Assignment.Controllers
{ {
uuid = Guid.NewGuid().ToString(), uuid = Guid.NewGuid().ToString(),
user = userUuid, user = userUuid,
uniqueIdCode = model.emailaddress, uniqueIdCode = user_id,
provider = 0, provider = 0,
}; };
// Assign a new session for user
// Generate 32-bit random string as session token
string token = GenerateRandomString(32);
// Use bcrypt to hash token
string hashedToken = BCryptNet.HashPassword(token);
string sessionUuid = Guid.NewGuid().ToString();
Sessions session = new Sessions
{
uuid = sessionUuid,
user = userUuid,
token = token,
alias = "Web",
validFrom = DateTime.Now,
validTo = DateTime.Now.AddDays(7)
};
// Add them into database // Add them into database
db.Users.Add(user); db.Users.Add(newDbUser);
db.Credentials.Add(credential); db.Credentials.Add(credential);
db.Sessions.Add(session);
db.SaveChanges(); db.SaveChanges();
// write cookie (`session`) return RedirectToAction("Index");
HttpCookie cookie1 = new HttpCookie("session", sessionUuid + ":" + token);
cookie1.Expires = DateTime.Now.AddDays(7);
Response.Cookies.Add(cookie1);
return RedirectToAction("InitialPasskey");
} }
public ActionResult InitialPasskey() public ActionResult InitialPasskey()
{ {
return View(); return View();
} }
public ActionResult CompleteProfile()
{
return View();
}
} }
} }

View File

@ -188,7 +188,7 @@
<Compile Include="Models\Appointments.cs"> <Compile Include="Models\Appointments.cs">
<DependentUpon>FIT5032-Assignment.tt</DependentUpon> <DependentUpon>FIT5032-Assignment.tt</DependentUpon>
</Compile> </Compile>
<Compile Include="Models\CreateAccountForm.cs" /> <Compile Include="Models\CompleteProfileForm.cs" />
<Compile Include="Models\Credentials.cs"> <Compile Include="Models\Credentials.cs">
<DependentUpon>FIT5032-Assignment.tt</DependentUpon> <DependentUpon>FIT5032-Assignment.tt</DependentUpon>
</Compile> </Compile>
@ -308,6 +308,7 @@
<Content Include="Views\Test.cshtml" /> <Content Include="Views\Test.cshtml" />
<Content Include="Views\Home\InitialPasskey.cshtml" /> <Content Include="Views\Home\InitialPasskey.cshtml" />
<Content Include="Views\Home\LoginRedirect.cshtml" /> <Content Include="Views\Home\LoginRedirect.cshtml" />
<Content Include="Views\Home\CompleteProfile.cshtml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="Views\Test\" /> <Folder Include="Views\Test\" />

View File

@ -14,9 +14,9 @@
<WebStackScaffolding_ControllerDialogWidth>600</WebStackScaffolding_ControllerDialogWidth> <WebStackScaffolding_ControllerDialogWidth>600</WebStackScaffolding_ControllerDialogWidth>
<WebStackScaffolding_LayoutPageFile> <WebStackScaffolding_LayoutPageFile>
</WebStackScaffolding_LayoutPageFile> </WebStackScaffolding_LayoutPageFile>
<WebStackScaffolding_IsLayoutPageSelected>False</WebStackScaffolding_IsLayoutPageSelected> <WebStackScaffolding_IsLayoutPageSelected>True</WebStackScaffolding_IsLayoutPageSelected>
<WebStackScaffolding_IsPartialViewSelected>False</WebStackScaffolding_IsPartialViewSelected> <WebStackScaffolding_IsPartialViewSelected>False</WebStackScaffolding_IsPartialViewSelected>
<WebStackScaffolding_IsReferencingScriptLibrariesSelected>False</WebStackScaffolding_IsReferencingScriptLibrariesSelected> <WebStackScaffolding_IsReferencingScriptLibrariesSelected>True</WebStackScaffolding_IsReferencingScriptLibrariesSelected>
<WebStackScaffolding_DbContextTypeFullName>FIT5032_Assignment.Models.Database1Entities</WebStackScaffolding_DbContextTypeFullName> <WebStackScaffolding_DbContextTypeFullName>FIT5032_Assignment.Models.Database1Entities</WebStackScaffolding_DbContextTypeFullName>
<WebStackScaffolding_IsViewGenerationSelected>False</WebStackScaffolding_IsViewGenerationSelected> <WebStackScaffolding_IsViewGenerationSelected>False</WebStackScaffolding_IsViewGenerationSelected>
<WebStackScaffolding_IsAsyncSelected>False</WebStackScaffolding_IsAsyncSelected> <WebStackScaffolding_IsAsyncSelected>False</WebStackScaffolding_IsAsyncSelected>

View File

@ -6,12 +6,8 @@ using System.ComponentModel.DataAnnotations;
namespace FIT5032_Assignment.Models namespace FIT5032_Assignment.Models
{ {
public class CreateAccountForm public class CompleteProfileForm
{ {
[Required]
[Display(Name = "Email address")]
public string emailaddress { get; set; }
[Required] [Required]
[Display(Name = "Full name")] [Display(Name = "Full name")]
public string fullname { get; set; } public string fullname { get; set; }

View File

@ -0,0 +1,49 @@
@model FIT5032_Assignment.Models.CompleteProfileForm
@{
ViewBag.Title = "CompleteProfile";
}
<h2>CompleteProfile</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>CompleteProfileForm</h4>
<hr />
@Html.ValidationSummary(true, "", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(model => model.fullname, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.fullname, new { htmlAttributes = new { @class = "form-control" } })
@Html.ValidationMessageFor(model => model.fullname, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.role, htmlAttributes: new { @class = "control-label col-md-2" })
<div class="col-md-10">
<!--Dropdown Menu-->
@Html.DropDownListFor(model => model.role, new List<SelectListItem>
{
new SelectListItem{ Text="Patient", Value = "1"},
new SelectListItem{ Text="Doctor", Value = "2"}
}, "Select Role", new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.role, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}

View File

@ -1,6 +1,4 @@
@model FIT5032_Assignment.Models.CreateAccountForm @{
@{
ViewBag.Title = "CreateAccount"; ViewBag.Title = "CreateAccount";
Layout = "~/Views/Shared/_Layout.cshtml"; Layout = "~/Views/Shared/_Layout.cshtml";
} }