FIT5032-Assignment/FIT5032-Assignment/Controllers/HomeController.cs

420 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Data.Entity;
using FIT5032_Assignment.Models;
using System.Security.Cryptography;
using System.Text;
using BCryptNet = BCrypt.Net.BCrypt;
using System.Diagnostics;
using System.Net;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;
using System.IO;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using System.Net.Http.Headers;
using System.Dynamic;
using RestSharp;
using RestSharp.Authenticators;
namespace FIT5032_Assignment.Controllers {
// Endpoint Response
public class PassageUserReply {
public PassageUserReplyUser User { get; set; }
}
public class PassageUserReplyUser {
public string Email { get; set; }
}
public class PassageUserFindReply {
public int Total_Users { get; set; }
public List<PassageUserFindReplyUser> Users { get; set; }
}
public class PassageUserFindReplyUser {
public string Id { 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 DbSet<Patients> Patients { get; set; }
public DbSet<Doctors> Doctors { get; set; }
public DbSet<Appointments> Appointments { get; set; }
public DbSet<Images> Images { get; set; }
public DbSet<Reviews> Reviews { get; set; }
}
public class HomeController : Controller {
public static string GetMd5Hash(string input) {
using (MD5 md5 = MD5.Create()) {
byte[] data = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < data.Length; i++) {
sBuilder.Append(data[i].ToString("x2"));
}
return sBuilder.ToString();
}
}
private static readonly HttpClient httpClient = new HttpClient();
public static RsaSecurityKey LoadRsaSecurityKeyFromPem(string pem) {
TextReader textReader = new StringReader(pem);
PemReader pemReader = new PemReader(textReader);
AsymmetricKeyParameter keyParameter = (AsymmetricKeyParameter)pemReader.ReadObject();
RSAParameters rsaParameters = DotNetUtilities.ToRSAParameters((Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters)keyParameter);
RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
rsa.ImportParameters(rsaParameters);
return new RsaSecurityKey(rsa);
}
private String psgCredentialVerify(string token) {
var jwtHandler = new JwtSecurityTokenHandler();
var jwtToken = jwtHandler.ReadJwtToken(token);
string base64Publickey = "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQTRUWEQwVEh4NnJjNXlQcXM0Skw5M01nVEgvTS95Z2s3V1pYWWsrS01XTTA0bDdzM3owRlMKODlDRE56SFJkbVpJb3RCbDgrcUJ5TUwvck5VcUhXMnJ1Uzg0dmxFaWdza2djK2RsaitCZXFsaGsySFRpQitpegpQcGdCU1FJc2YrZjdSU3dkYktFS2hRQm1La3MxVGF4YWNDUndPVWJKT1VQbjJXZmhVSHhRd0FwZGNCQWdNdHVNCld3QzJZRThGblFRZDhxc3dMTTBGQWhoSzUrdXRXY0s0bHdCVlFxUGJRaUJZYnZmWXkwYVF6UFB2V2NMR1JvR00KUVA1b1JCTmRuRzQ4Sm9Eb2tCSEJkbCt4RzM1L1U2N1BvejFKY0VVSnpWTHdIUFNHa0xyRU1OYlFrbnJSK2tHZwpnS1dWNFpvYWVOSHZVeFE3YVg3SElFMlc1UnIwRmxGUG1RSURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
RsaSecurityKey rsaKey = LoadRsaSecurityKeyFromPem(Encoding.UTF8.GetString(Convert.FromBase64String(base64Publickey)));
// Valid time 3600s
var validationParameters = new TokenValidationParameters() {
ValidIssuer = "https://auth.passage.id/v1/apps/ZHM5whW5xsZEczTn2loffzjN",
ValidateAudience = false,
IssuerSigningKey = rsaKey,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(3600)
};
try {
var claimsPrincipal = jwtHandler.ValidateToken(token, validationParameters, out var rawValidatedToken);
} catch (SecurityTokenExpiredException) {
Trace.WriteLine("Token has expired");
return null;
} catch (SecurityTokenInvalidSignatureException) {
Trace.WriteLine("Token has invalid signature");
return null;
} catch (SecurityTokenInvalidIssuerException) {
Trace.WriteLine("Token has invalid issuer");
return null;
} catch (SecurityTokenInvalidAudienceException) {
Trace.WriteLine("Token has invalid audience");
return null;
} catch (SecurityTokenValidationException) {
Trace.WriteLine("Token failed validation");
return null;
} catch (ArgumentException) {
Trace.WriteLine("Token was empty or null");
return null;
}
string sub = jwtToken.Claims.First(claim => claim.Type == "sub").Value;
return sub;
}
private Users loginInfo(string user) {
var db = new Database1Entities();
var credential = db.Credentials.Where(res => (res.uniqueIdCode == user) && (res.provider == 0));
if (credential.Count() == 0) {
return null;
} else {
var userUuid = credential.First().user;
var dbUser = db.Users.Where(res => res.uuid == userUuid);
if (dbUser.Count() == 0) {
return null;
} else {
return dbUser.First();
}
}
}
private Database1Entities db = new Database1Entities();
private string GenerateRandomString(int length) {
const string validChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var rng = new RNGCryptoServiceProvider();
var bytes = new byte[length];
rng.GetBytes(bytes);
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() {
// If user logged in, show user name
if (Request.Cookies["psg_auth_token"] != null) {
var user = psgCredentialVerify(Request.Cookies["psg_auth_token"].Value);
if (user != null) {
var db = new Database1Entities();
var credential = db.Credentials.Where(res => (res.uniqueIdCode == user) && (res.provider == 0));
if (credential.Count() != 0) {
var userUuid = credential.First().user;
var dbUser = db.Users.Where(res => res.uuid == userUuid);
if (dbUser.Count() != 0) {
ViewBag.displayname = dbUser.First().displayName;
ViewBag.avatar = dbUser.First().avatar;
ViewBag.role = dbUser.First().role == 1 ? "Patient" : "Doctor";
}
}
} else {
// Remove cookies and refresh page
Response.Cookies["psg_auth_token"].Expires = DateTime.Now.AddDays(-1);
return RedirectToAction("Index");
}
}
return View();
}
public ActionResult CreateAccount() {
return View();
}
public ActionResult LoginRedirect() {
// See cookies
var psg_auth_token = Request.Cookies["psg_auth_token"];
Trace.WriteLine(psg_auth_token.Value);
// JWT Verify
string sub = psgCredentialVerify(psg_auth_token.Value);
if (sub == null) {
return RedirectToAction("Login");
} else {
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");
}
}
}
// POST /ComplteteProfile
[HttpPost]
public async Task<ActionResult> CompleteProfile(Models.CompleteProfileForm model) {
if (!ModelState.IsValid) {
ModelState.AddModelError("fullname", "Form not valid");
return View(model);
}
// Verify user is logged in
var psg_auth_token = Request.Cookies["psg_auth_token"];
var user = psgCredentialVerify(psg_auth_token.Value);
if (user == null) {
return RedirectToAction("Login");
}
// Get users email from Passage API
var app_id = "ZHM5whW5xsZEczTn2loffzjN";
var user_id = user;
var url = $"https://api.passage.id/v1/apps/{app_id}/users/{user_id}";
Trace.WriteLine(url);
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
string md5Email = GetMd5Hash(emailaddress);
string avatarUrl = "https://www.gravatar.com/avatar/" + md5Email + "?s=200";
// Create a new credential and a new user
string userUuid = Guid.NewGuid().ToString();
Users newDbUser = new Users {
uuid = userUuid,
displayName = model.fullname,
role = Int16.Parse(model.role),
avatar = avatarUrl
};
Credentials credential = new Credentials {
uuid = Guid.NewGuid().ToString(),
user = userUuid,
uniqueIdCode = user_id,
provider = 0,
};
// Add them into database
db.Users.Add(newDbUser);
db.Credentials.Add(credential);
db.SaveChanges();
if (model.role == "1") {
// Create patient profile
db.Patients.Add(new Patients {
phone = "",
address = "",
user = userUuid,
notes = ""
});
db.SaveChanges();
} else {
db.Doctors.Add(new Doctors {
user = userUuid,
bio = "",
});
db.SaveChanges();
}
return RedirectToAction("Index");
}
public ActionResult InitialPasskey() {
return View();
}
public ActionResult CompleteProfile() {
return View();
}
public ActionResult ImageUpload() {
try {
if (Request.Cookies["psg_auth_token"] == null) {
// Redirect to home page
return RedirectToAction("Index");
}
var user = psgCredentialVerify(Request.Cookies["psg_auth_token"].Value);
if (user == null) {
// Redirect to home page
return RedirectToAction("Index");
} else {
// Detect if user is doctor or patient
var db = new Database1Entities();
var credential = db.Credentials.Where(res => (res.uniqueIdCode == user) && (res.provider == 0));
if (credential.Count() == 0) {
// Redirect to create account
return RedirectToAction("Index");
} else {
// Redirect to image upload
return View();
}
}
} catch (Exception e ) {
Trace.WriteLine(e);
return RedirectToAction("Index");
}
}
[HttpPost]
public async Task<ActionResult> ImageUpload(Models.ImageUploadForm model) {
try {
if (Request.Cookies["psg_auth_token"] == null) {
// Return 401 error
return new HttpStatusCodeResult(HttpStatusCode.Unauthorized);
}
var userCre = psgCredentialVerify(Request.Cookies["psg_auth_token"].Value);
var user = loginInfo(userCre);
if (user.role != 2) {
// Return 403 error if user is not doctor
return new HttpStatusCodeResult(HttpStatusCode.Forbidden) ;
}
// check recived items
if (!ModelState.IsValid) {
ModelState.AddModelError("patientEmail", "Form not valid");
return View(model);
}
// check uploaded file
if (model.imageFile == null) {
ModelState.AddModelError("imageFile", "Please upload a file");
return View(model);
}
// check format: png, jpg
if (model.imageFile.ContentType != "image/png" && model.imageFile.ContentType != "image/jpeg") {
ModelState.AddModelError("imageFile", "Please upload a png or jpg file");
return View(model);
}
// Check if the email have a patient profile
var dbEntity = new Database1Entities();
// Find the account associated with the email
var app_id = "ZHM5whW5xsZEczTn2loffzjN";
var url = $"https://api.passage.id/v1/apps/{app_id}/users?identifier={model.patientEmail}";
var res = httpClient.GetStringAsync(url).Result;
if (JsonConvert.DeserializeObject<PassageUserFindReply>(res).Total_Users == 0) {
ModelState.AddModelError("patientEmail", "No patient found");
return View(model);
}
var patientId = JsonConvert.DeserializeObject<PassageUserFindReply>(res).Users[0].Id;
var patientCredential = dbEntity.Credentials.Where(c => (c.uniqueIdCode == patientId) && (c.provider == 0));
if (patientCredential.Count() == 0) {
ModelState.AddModelError("patientEmail", "No patient found");
return View(model);
}
var patientUuid = patientCredential.First().user;
var patient = dbEntity.Users.Where(u => u.uuid == patientUuid);
if (patient.Count() == 0 || patient.First().role != 1) {
ModelState.AddModelError("patientEmail", "No patient found");
return View(model);
}
// Store file to server
var fileId = Guid.NewGuid().ToString();
var fileName = fileId + Path.GetExtension(model.imageFile.FileName);
var filePath = Path.Combine(Server.MapPath("~/App_Data/upload_images"), fileName);
model.imageFile.SaveAs(filePath);
// Create image entity to database
var appointmentId = Guid.NewGuid().ToString();
var imageId = Guid.NewGuid().ToString();
var appointment = new Appointments {
uuid = appointmentId,
patient = patientUuid,
responsibleBy = user.uuid,
createdAt = DateTime.Now,
appointmentDate = DateTime.Now,
status = 0,
createdBy = 1
};
var image = new Images {
uuid = imageId,
appointment = appointmentId,
patient = patientUuid,
responsibleBy = user.uuid,
createdAt = DateTime.Now,
file = fileName,
status = 0,
};
db.SaveChanges();
// Send attached email with mailgun
var doctorName = user.displayName;
RestClient client = new RestClient(new RestClientOptions ("https://api.mailgun.net/v3/test.astrian.moe") {
Authenticator = new HttpBasicAuthenticator("api", "365900a7818241eafcbbf82e59cf99e8-5465e583-b4966e64"),
});
var request = new RestRequest("messages", Method.Post);
request.AddParameter("from", "Xpectrum <xpectrum@test.astrian.moe>");
request.AddParameter("to", model.patientEmail);
request.AddParameter("subject", "Xpectrum: New image available");
request.AddParameter("text", $"Hi {patient.First().displayName},\n\nDr. {doctorName} has uploaded a new image for you.\n\nPlease check the attachment.\n\nBest regards,\nXpectrum");
request.AddFile("attachment", filePath);
// Send request
var response = await client.ExecuteAsync(request);
Trace.WriteLine(response.Content);
return View();
} catch (Exception e) {
Trace.WriteLine(e);
return new HttpStatusCodeResult(HttpStatusCode.BadGateway);
}
}
}
}