Add image upload page
This commit is contained in:
parent
fbb1fcb566
commit
35c458b236
|
@ -21,283 +21,242 @@ 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; }
|
||||
// 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 DbSet<Patients> Patients { get; set; }
|
||||
public DbSet<Doctors> Doctors { 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 loginVerify(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;
|
||||
}
|
||||
|
||||
public class PassageUserReplyUser
|
||||
{
|
||||
public string Email { get; set; }
|
||||
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());
|
||||
}
|
||||
|
||||
// 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 HomeController() {
|
||||
// if auth token setted, ignore
|
||||
if (httpClient.DefaultRequestHeaders.Authorization == null) {
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "vF4ch1wUf8.1cqOms9JMmUbqMGohlkJLzGDVlbF51D03fJnLfxwkn8kyAaVVjfvySufW9vXb3p3");
|
||||
}
|
||||
}
|
||||
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();
|
||||
public ActionResult Index() {
|
||||
// If user logged in, show user name
|
||||
if (Request.Cookies["psg_auth_token"] != null) {
|
||||
var user = loginVerify(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";
|
||||
}
|
||||
}
|
||||
}
|
||||
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 loginVerify(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 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 = loginVerify(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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = loginVerify(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 = loginVerify(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();
|
||||
}
|
||||
}
|
||||
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 = loginVerify(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 = loginVerify(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() {
|
||||
var user = loginVerify(Request.Cookies["psg_auth_token"].Value);
|
||||
if (user != null) {
|
||||
// Redirect to home page
|
||||
return RedirectToAction("Index");
|
||||
} else {
|
||||
return View();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -310,6 +310,7 @@
|
|||
<Content Include="Views\Home\LoginRedirect.cshtml" />
|
||||
<Content Include="Views\Home\CompleteProfile.cshtml" />
|
||||
<Content Include="Views\Home\View.cshtml" />
|
||||
<Content Include="Views\Home\ImageUpload.cshtml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Views\Test\" />
|
||||
|
|
8
FIT5032-Assignment/Views/Home/ImageUpload.cshtml
Normal file
8
FIT5032-Assignment/Views/Home/ImageUpload.cshtml
Normal file
|
@ -0,0 +1,8 @@
|
|||
@{
|
||||
Layout = "~/Views/Shared/_Layout.cshtml";
|
||||
}
|
||||
|
||||
|
||||
<main aria-labelledby="title">
|
||||
image upload
|
||||
</main>
|
Loading…
Reference in New Issue
Block a user