added token based registration.
This commit is contained in:
31
Controllers/AdminController.cs
Normal file
31
Controllers/AdminController.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using CarCareTracker.Logic;
|
||||||
|
using CarCareTracker.Models;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace CarCareTracker.Controllers
|
||||||
|
{
|
||||||
|
[Authorize(Roles = nameof(UserData.IsAdmin))]
|
||||||
|
public class AdminController : Controller
|
||||||
|
{
|
||||||
|
private ILoginLogic _loginLogic;
|
||||||
|
public AdminController(ILoginLogic loginLogic)
|
||||||
|
{
|
||||||
|
_loginLogic = loginLogic;
|
||||||
|
}
|
||||||
|
public IActionResult Index()
|
||||||
|
{
|
||||||
|
var viewModel = new AdminViewModel
|
||||||
|
{
|
||||||
|
Users = _loginLogic.GetAllUsers(),
|
||||||
|
Tokens = _loginLogic.GetAllTokens()
|
||||||
|
};
|
||||||
|
return View(viewModel);
|
||||||
|
}
|
||||||
|
public IActionResult GenerateNewToken()
|
||||||
|
{
|
||||||
|
var result = _loginLogic.GenerateUserToken();
|
||||||
|
return Json(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using CarCareTracker.Helper;
|
using CarCareTracker.Helper;
|
||||||
|
using CarCareTracker.Logic;
|
||||||
using CarCareTracker.Models;
|
using CarCareTracker.Models;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
@@ -13,22 +14,26 @@ namespace CarCareTracker.Controllers
|
|||||||
public class LoginController : Controller
|
public class LoginController : Controller
|
||||||
{
|
{
|
||||||
private IDataProtector _dataProtector;
|
private IDataProtector _dataProtector;
|
||||||
private ILoginHelper _loginHelper;
|
private ILoginLogic _loginLogic;
|
||||||
private readonly ILogger<LoginController> _logger;
|
private readonly ILogger<LoginController> _logger;
|
||||||
public LoginController(
|
public LoginController(
|
||||||
ILogger<LoginController> logger,
|
ILogger<LoginController> logger,
|
||||||
IDataProtectionProvider securityProvider,
|
IDataProtectionProvider securityProvider,
|
||||||
ILoginHelper loginHelper
|
ILoginLogic loginLogic
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_dataProtector = securityProvider.CreateProtector("login");
|
_dataProtector = securityProvider.CreateProtector("login");
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_loginHelper = loginHelper;
|
_loginLogic = loginLogic;
|
||||||
}
|
}
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
return View();
|
return View();
|
||||||
}
|
}
|
||||||
|
public IActionResult Registration()
|
||||||
|
{
|
||||||
|
return View();
|
||||||
|
}
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public IActionResult Login(LoginModel credentials)
|
public IActionResult Login(LoginModel credentials)
|
||||||
{
|
{
|
||||||
@@ -40,13 +45,12 @@ namespace CarCareTracker.Controllers
|
|||||||
//compare it against hashed credentials
|
//compare it against hashed credentials
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var loginIsValid = _loginHelper.ValidateUserCredentials(credentials);
|
var userData = _loginLogic.ValidateUserCredentials(credentials);
|
||||||
if (loginIsValid)
|
if (userData.Id != default)
|
||||||
{
|
{
|
||||||
AuthCookie authCookie = new AuthCookie
|
AuthCookie authCookie = new AuthCookie
|
||||||
{
|
{
|
||||||
Id = 1, //this is hardcoded for now
|
UserData = userData,
|
||||||
UserName = credentials.UserName,
|
|
||||||
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
|
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
|
||||||
};
|
};
|
||||||
var serializedCookie = JsonSerializer.Serialize(authCookie);
|
var serializedCookie = JsonSerializer.Serialize(authCookie);
|
||||||
@@ -61,26 +65,21 @@ namespace CarCareTracker.Controllers
|
|||||||
}
|
}
|
||||||
return Json(false);
|
return Json(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public IActionResult Register(LoginModel credentials)
|
||||||
|
{
|
||||||
|
var result = _loginLogic.RegisterNewUser(credentials);
|
||||||
|
return Json(result);
|
||||||
|
}
|
||||||
[Authorize] //User must already be logged in to do this.
|
[Authorize] //User must already be logged in to do this.
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public IActionResult CreateLoginCreds(LoginModel credentials)
|
public IActionResult CreateLoginCreds(LoginModel credentials)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
|
var result = _loginLogic.CreateRootUserCredentials(credentials);
|
||||||
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
return Json(result);
|
||||||
if (existingUserConfig is not null)
|
|
||||||
{
|
|
||||||
//create hashes of the login credentials.
|
|
||||||
var hashedUserName = Sha256_hash(credentials.UserName);
|
|
||||||
var hashedPassword = Sha256_hash(credentials.Password);
|
|
||||||
//copy over settings that are off limits on the settings page.
|
|
||||||
existingUserConfig.EnableAuth = true;
|
|
||||||
existingUserConfig.UserNameHash = hashedUserName;
|
|
||||||
existingUserConfig.UserPasswordHash = hashedPassword;
|
|
||||||
}
|
|
||||||
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
|
||||||
return Json(true);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -94,19 +93,13 @@ namespace CarCareTracker.Controllers
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
|
var result = _loginLogic.DeleteRootUserCredentials();
|
||||||
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
|
||||||
if (existingUserConfig is not null)
|
|
||||||
{
|
|
||||||
//copy over settings that are off limits on the settings page.
|
|
||||||
existingUserConfig.EnableAuth = false;
|
|
||||||
existingUserConfig.UserNameHash = string.Empty;
|
|
||||||
existingUserConfig.UserPasswordHash = string.Empty;
|
|
||||||
}
|
|
||||||
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
|
||||||
//destroy any login cookies.
|
//destroy any login cookies.
|
||||||
Response.Cookies.Delete("ACCESS_TOKEN");
|
if (result)
|
||||||
return Json(true);
|
{
|
||||||
|
Response.Cookies.Delete("ACCESS_TOKEN");
|
||||||
|
}
|
||||||
|
return Json(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -121,20 +114,5 @@ namespace CarCareTracker.Controllers
|
|||||||
Response.Cookies.Delete("ACCESS_TOKEN");
|
Response.Cookies.Delete("ACCESS_TOKEN");
|
||||||
return Json(true);
|
return Json(true);
|
||||||
}
|
}
|
||||||
private static string Sha256_hash(string value)
|
|
||||||
{
|
|
||||||
StringBuilder Sb = new StringBuilder();
|
|
||||||
|
|
||||||
using (var hash = SHA256.Create())
|
|
||||||
{
|
|
||||||
Encoding enc = Encoding.UTF8;
|
|
||||||
byte[] result = hash.ComputeHash(enc.GetBytes(value));
|
|
||||||
|
|
||||||
foreach (byte b in result)
|
|
||||||
Sb.Append(b.ToString("x2"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
External/Implementations/TokenRecordDataAccess.cs
vendored
Normal file
48
External/Implementations/TokenRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
using CarCareTracker.External.Interfaces;
|
||||||
|
using CarCareTracker.Helper;
|
||||||
|
using CarCareTracker.Models;
|
||||||
|
using LiteDB;
|
||||||
|
|
||||||
|
namespace CarCareTracker.External.Implementations
|
||||||
|
{
|
||||||
|
public class TokenRecordDataAccess : ITokenRecordDataAccess
|
||||||
|
{
|
||||||
|
private static string dbName = StaticHelper.DbName;
|
||||||
|
private static string tableName = "tokenrecords";
|
||||||
|
public List<Token> GetTokens()
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<Token>(tableName);
|
||||||
|
return table.FindAll().ToList();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public Token GetTokenRecordByBody(string tokenBody)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<Token>(tableName);
|
||||||
|
var tokenRecord = table.FindOne(Query.EQ(nameof(Token.Body), tokenBody));
|
||||||
|
return tokenRecord ?? new Token();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public bool CreateNewToken(Token token)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<Token>(tableName);
|
||||||
|
table.Insert(token);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public bool DeleteToken(int tokenId)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<Token>(tableName);
|
||||||
|
table.Delete(tokenId);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
External/Implementations/UserRecordDataAccess.cs
vendored
Normal file
57
External/Implementations/UserRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using CarCareTracker.External.Interfaces;
|
||||||
|
using CarCareTracker.Helper;
|
||||||
|
using CarCareTracker.Models;
|
||||||
|
using LiteDB;
|
||||||
|
|
||||||
|
namespace CarCareTracker.External.Implementations
|
||||||
|
{
|
||||||
|
public class UserRecordDataAccess : IUserRecordDataAccess
|
||||||
|
{
|
||||||
|
private static string dbName = StaticHelper.DbName;
|
||||||
|
private static string tableName = "userrecords";
|
||||||
|
public List<UserData> GetUsers()
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<UserData>(tableName);
|
||||||
|
return table.FindAll().ToList();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public UserData GetUserRecordByUserName(string userName)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<UserData>(tableName);
|
||||||
|
var userRecord = table.FindOne(Query.EQ(nameof(UserData.UserName), userName));
|
||||||
|
return userRecord ?? new UserData();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public UserData GetUserRecordById(int userId)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<UserData>(tableName);
|
||||||
|
var userRecord = table.FindById(userId);
|
||||||
|
return userRecord ?? new UserData();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public bool SaveUserRecord(UserData userRecord)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<UserData>(tableName);
|
||||||
|
table.Upsert(userRecord);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
public bool DeleteUserRecord(int userId)
|
||||||
|
{
|
||||||
|
using (var db = new LiteDatabase(dbName))
|
||||||
|
{
|
||||||
|
var table = db.GetCollection<UserData>(tableName);
|
||||||
|
table.Delete(userId);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
External/Interfaces/ITokenRecordDataAccess.cs
vendored
Normal file
12
External/Interfaces/ITokenRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using CarCareTracker.Models;
|
||||||
|
|
||||||
|
namespace CarCareTracker.External.Interfaces
|
||||||
|
{
|
||||||
|
public interface ITokenRecordDataAccess
|
||||||
|
{
|
||||||
|
public List<Token> GetTokens();
|
||||||
|
public Token GetTokenRecordByBody(string tokenBody);
|
||||||
|
public bool CreateNewToken(Token token);
|
||||||
|
public bool DeleteToken(int tokenId);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
External/Interfaces/IUserRecordDataAccess.cs
vendored
Normal file
13
External/Interfaces/IUserRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using CarCareTracker.Models;
|
||||||
|
|
||||||
|
namespace CarCareTracker.External.Interfaces
|
||||||
|
{
|
||||||
|
public interface IUserRecordDataAccess
|
||||||
|
{
|
||||||
|
public List<UserData> GetUsers();
|
||||||
|
public UserData GetUserRecordByUserName(string userName);
|
||||||
|
public UserData GetUserRecordById(int userId);
|
||||||
|
public bool SaveUserRecord(UserData userRecord);
|
||||||
|
public bool DeleteUserRecord(int userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
using CarCareTracker.Models;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace CarCareTracker.Helper
|
|
||||||
{
|
|
||||||
public interface ILoginHelper
|
|
||||||
{
|
|
||||||
bool ValidateUserCredentials(LoginModel credentials);
|
|
||||||
}
|
|
||||||
public class LoginHelper: ILoginHelper
|
|
||||||
{
|
|
||||||
public bool ValidateUserCredentials(LoginModel credentials)
|
|
||||||
{
|
|
||||||
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
|
|
||||||
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
|
||||||
if (existingUserConfig is not null)
|
|
||||||
{
|
|
||||||
//create hashes of the login credentials.
|
|
||||||
var hashedUserName = Sha256_hash(credentials.UserName);
|
|
||||||
var hashedPassword = Sha256_hash(credentials.Password);
|
|
||||||
//compare against stored hash.
|
|
||||||
if (hashedUserName == existingUserConfig.UserNameHash &&
|
|
||||||
hashedPassword == existingUserConfig.UserPasswordHash)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
private static string Sha256_hash(string value)
|
|
||||||
{
|
|
||||||
StringBuilder Sb = new StringBuilder();
|
|
||||||
|
|
||||||
using (var hash = SHA256.Create())
|
|
||||||
{
|
|
||||||
Encoding enc = Encoding.UTF8;
|
|
||||||
byte[] result = hash.ComputeHash(enc.GetBytes(value));
|
|
||||||
|
|
||||||
foreach (byte b in result)
|
|
||||||
Sb.Append(b.ToString("x2"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return Sb.ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
201
Logic/LoginLogic.cs
Normal file
201
Logic/LoginLogic.cs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
using CarCareTracker.External.Interfaces;
|
||||||
|
using CarCareTracker.Helper;
|
||||||
|
using CarCareTracker.Models;
|
||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CarCareTracker.Logic
|
||||||
|
{
|
||||||
|
public interface ILoginLogic
|
||||||
|
{
|
||||||
|
bool GenerateUserToken();
|
||||||
|
OperationResponse RegisterNewUser(LoginModel credentials);
|
||||||
|
OperationResponse ResetUserPassword(LoginModel credentials);
|
||||||
|
UserData ValidateUserCredentials(LoginModel credentials);
|
||||||
|
bool CreateRootUserCredentials(LoginModel credentials);
|
||||||
|
bool DeleteRootUserCredentials();
|
||||||
|
List<UserData> GetAllUsers();
|
||||||
|
List<Token> GetAllTokens();
|
||||||
|
|
||||||
|
}
|
||||||
|
public class LoginLogic : ILoginLogic
|
||||||
|
{
|
||||||
|
private readonly IUserRecordDataAccess _userData;
|
||||||
|
private readonly ITokenRecordDataAccess _tokenData;
|
||||||
|
public LoginLogic(IUserRecordDataAccess userData, ITokenRecordDataAccess tokenData)
|
||||||
|
{
|
||||||
|
_userData = userData;
|
||||||
|
_tokenData = tokenData;
|
||||||
|
}
|
||||||
|
public OperationResponse RegisterNewUser(LoginModel credentials)
|
||||||
|
{
|
||||||
|
//validate their token.
|
||||||
|
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
|
||||||
|
if (existingToken.Id == default)
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Invalid Token" };
|
||||||
|
}
|
||||||
|
//token is valid, check if username and password is acceptable and that username is unique.
|
||||||
|
if (string.IsNullOrWhiteSpace(credentials.UserName) || string.IsNullOrWhiteSpace(credentials.Password))
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Neither username nor password can be blank" };
|
||||||
|
}
|
||||||
|
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
|
||||||
|
if (existingUser.Id != default)
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Username already taken" };
|
||||||
|
}
|
||||||
|
//username is unique then we delete the token and create the user.
|
||||||
|
_tokenData.DeleteToken(existingToken.Id);
|
||||||
|
var newUser = new UserData()
|
||||||
|
{
|
||||||
|
UserName = credentials.UserName,
|
||||||
|
Password = GetHash(credentials.Password)
|
||||||
|
};
|
||||||
|
var result = _userData.SaveUserRecord(newUser);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = true, Message = "You will be redirected to the login page briefly." };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// Returns an empty user if can't auth against neither root nor db user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">credentials from login page</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public UserData ValidateUserCredentials(LoginModel credentials)
|
||||||
|
{
|
||||||
|
if (UserIsRoot(credentials))
|
||||||
|
{
|
||||||
|
return new UserData()
|
||||||
|
{
|
||||||
|
Id = -1,
|
||||||
|
UserName = credentials.UserName,
|
||||||
|
IsAdmin = true
|
||||||
|
};
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
//authenticate via DB.
|
||||||
|
var result = _userData.GetUserRecordByUserName(credentials.UserName);
|
||||||
|
if (GetHash(credentials.Password) == result.Password)
|
||||||
|
{
|
||||||
|
result.Password = string.Empty;
|
||||||
|
return result;
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return new UserData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#region "Admin Functions"
|
||||||
|
public List<UserData> GetAllUsers()
|
||||||
|
{
|
||||||
|
var result = _userData.GetUsers();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public List<Token> GetAllTokens()
|
||||||
|
{
|
||||||
|
var result = _tokenData.GetTokens();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public bool GenerateUserToken()
|
||||||
|
{
|
||||||
|
var token = new Token()
|
||||||
|
{
|
||||||
|
Body = Guid.NewGuid().ToString().Substring(0, 8)
|
||||||
|
};
|
||||||
|
var result = _tokenData.CreateNewToken(token);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public OperationResponse ResetUserPassword(LoginModel credentials)
|
||||||
|
{
|
||||||
|
//user might have forgotten their password.
|
||||||
|
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
|
||||||
|
if (existingUser.Id == default)
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Unable to find user" };
|
||||||
|
}
|
||||||
|
var newPassword = Guid.NewGuid().ToString().Substring(0, 8);
|
||||||
|
existingUser.Password = GetHash(newPassword);
|
||||||
|
var result = _userData.SaveUserRecord(existingUser);
|
||||||
|
if (result)
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = true, Message = newPassword };
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
return new OperationResponse { Success = false, Message = "Something went wrong, please try again later." };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
#region "Root User"
|
||||||
|
public bool CreateRootUserCredentials(LoginModel credentials)
|
||||||
|
{
|
||||||
|
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
|
||||||
|
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||||
|
if (existingUserConfig is not null)
|
||||||
|
{
|
||||||
|
//create hashes of the login credentials.
|
||||||
|
var hashedUserName = GetHash(credentials.UserName);
|
||||||
|
var hashedPassword = GetHash(credentials.Password);
|
||||||
|
//copy over settings that are off limits on the settings page.
|
||||||
|
existingUserConfig.EnableAuth = true;
|
||||||
|
existingUserConfig.UserNameHash = hashedUserName;
|
||||||
|
existingUserConfig.UserPasswordHash = hashedPassword;
|
||||||
|
}
|
||||||
|
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public bool DeleteRootUserCredentials() {
|
||||||
|
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
|
||||||
|
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||||
|
if (existingUserConfig is not null)
|
||||||
|
{
|
||||||
|
//copy over settings that are off limits on the settings page.
|
||||||
|
existingUserConfig.EnableAuth = false;
|
||||||
|
existingUserConfig.UserNameHash = string.Empty;
|
||||||
|
existingUserConfig.UserPasswordHash = string.Empty;
|
||||||
|
}
|
||||||
|
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
private bool UserIsRoot(LoginModel credentials)
|
||||||
|
{
|
||||||
|
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
|
||||||
|
var existingUserConfig = JsonSerializer.Deserialize<UserConfig>(configFileContents);
|
||||||
|
if (existingUserConfig is not null)
|
||||||
|
{
|
||||||
|
//create hashes of the login credentials.
|
||||||
|
var hashedUserName = GetHash(credentials.UserName);
|
||||||
|
var hashedPassword = GetHash(credentials.Password);
|
||||||
|
//compare against stored hash.
|
||||||
|
if (hashedUserName == existingUserConfig.UserNameHash &&
|
||||||
|
hashedPassword == existingUserConfig.UserPasswordHash)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
private static string GetHash(string value)
|
||||||
|
{
|
||||||
|
StringBuilder Sb = new StringBuilder();
|
||||||
|
|
||||||
|
using (var hash = SHA256.Create())
|
||||||
|
{
|
||||||
|
Encoding enc = Encoding.UTF8;
|
||||||
|
byte[] result = hash.ComputeHash(enc.GetBytes(value));
|
||||||
|
|
||||||
|
foreach (byte b in result)
|
||||||
|
Sb.Append(b.ToString("x2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Sb.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using CarCareTracker.Helper;
|
using CarCareTracker.Logic;
|
||||||
using CarCareTracker.Models;
|
using CarCareTracker.Models;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
@@ -15,20 +15,20 @@ namespace CarCareTracker.Middleware
|
|||||||
{
|
{
|
||||||
private IHttpContextAccessor _httpContext;
|
private IHttpContextAccessor _httpContext;
|
||||||
private IDataProtector _dataProtector;
|
private IDataProtector _dataProtector;
|
||||||
private ILoginHelper _loginHelper;
|
private ILoginLogic _loginLogic;
|
||||||
private bool enableAuth;
|
private bool enableAuth;
|
||||||
public Authen(
|
public Authen(
|
||||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||||
UrlEncoder encoder,
|
UrlEncoder encoder,
|
||||||
ILoggerFactory logger,
|
ILoggerFactory logger,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILoginHelper loginHelper,
|
ILoginLogic loginLogic,
|
||||||
IDataProtectionProvider securityProvider,
|
IDataProtectionProvider securityProvider,
|
||||||
IHttpContextAccessor httpContext) : base(options, logger, encoder)
|
IHttpContextAccessor httpContext) : base(options, logger, encoder)
|
||||||
{
|
{
|
||||||
_httpContext = httpContext;
|
_httpContext = httpContext;
|
||||||
_dataProtector = securityProvider.CreateProtector("login");
|
_dataProtector = securityProvider.CreateProtector("login");
|
||||||
_loginHelper = loginHelper;
|
_loginLogic = loginLogic;
|
||||||
enableAuth = bool.Parse(configuration["EnableAuth"]);
|
enableAuth = bool.Parse(configuration["EnableAuth"]);
|
||||||
}
|
}
|
||||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||||
@@ -66,14 +66,18 @@ namespace CarCareTracker.Middleware
|
|||||||
return AuthenticateResult.Fail("Invalid credentials");
|
return AuthenticateResult.Fail("Invalid credentials");
|
||||||
} else
|
} else
|
||||||
{
|
{
|
||||||
var validUser = _loginHelper.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
|
var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
|
||||||
if (validUser)
|
if (userData.Id != default)
|
||||||
{
|
{
|
||||||
var appIdentity = new ClaimsIdentity("Custom");
|
var appIdentity = new ClaimsIdentity("Custom");
|
||||||
var userIdentity = new List<Claim>
|
var userIdentity = new List<Claim>
|
||||||
{
|
{
|
||||||
new(ClaimTypes.Name, splitString[0])
|
new(ClaimTypes.Name, splitString[0])
|
||||||
};
|
};
|
||||||
|
if (userData.IsAdmin)
|
||||||
|
{
|
||||||
|
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
|
||||||
|
}
|
||||||
appIdentity.AddClaims(userIdentity);
|
appIdentity.AddClaims(userIdentity);
|
||||||
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
||||||
return AuthenticateResult.Success(ticket);
|
return AuthenticateResult.Success(ticket);
|
||||||
@@ -82,33 +86,44 @@ namespace CarCareTracker.Middleware
|
|||||||
}
|
}
|
||||||
else if (!string.IsNullOrWhiteSpace(access_token))
|
else if (!string.IsNullOrWhiteSpace(access_token))
|
||||||
{
|
{
|
||||||
//decrypt the access token.
|
try
|
||||||
var decryptedCookie = _dataProtector.Unprotect(access_token);
|
|
||||||
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
|
|
||||||
if (authCookie != null)
|
|
||||||
{
|
{
|
||||||
//validate auth cookie
|
//decrypt the access token.
|
||||||
if (authCookie.ExpiresOn < DateTime.Now)
|
var decryptedCookie = _dataProtector.Unprotect(access_token);
|
||||||
|
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
|
||||||
|
if (authCookie != null)
|
||||||
{
|
{
|
||||||
//if cookie is expired
|
//validate auth cookie
|
||||||
return AuthenticateResult.Fail("Expired credentials");
|
if (authCookie.ExpiresOn < DateTime.Now)
|
||||||
}
|
|
||||||
else if (authCookie.Id == default || string.IsNullOrWhiteSpace(authCookie.UserName))
|
|
||||||
{
|
|
||||||
return AuthenticateResult.Fail("Corrupted credentials");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var appIdentity = new ClaimsIdentity("Custom");
|
|
||||||
var userIdentity = new List<Claim>
|
|
||||||
{
|
{
|
||||||
new(ClaimTypes.Name, authCookie.UserName)
|
//if cookie is expired
|
||||||
|
return AuthenticateResult.Fail("Expired credentials");
|
||||||
|
}
|
||||||
|
else if (authCookie.UserData.Id == default || string.IsNullOrWhiteSpace(authCookie.UserData.UserName))
|
||||||
|
{
|
||||||
|
return AuthenticateResult.Fail("Corrupted credentials");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var appIdentity = new ClaimsIdentity("Custom");
|
||||||
|
var userIdentity = new List<Claim>
|
||||||
|
{
|
||||||
|
new(ClaimTypes.Name, authCookie.UserData.UserName)
|
||||||
};
|
};
|
||||||
appIdentity.AddClaims(userIdentity);
|
if (authCookie.UserData.IsAdmin)
|
||||||
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
{
|
||||||
return AuthenticateResult.Success(ticket);
|
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
|
||||||
|
}
|
||||||
|
appIdentity.AddClaims(userIdentity);
|
||||||
|
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
|
||||||
|
return AuthenticateResult.Success(ticket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return AuthenticateResult.Fail("Corrupted credentials");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return AuthenticateResult.Fail("Invalid credentials");
|
return AuthenticateResult.Fail("Invalid credentials");
|
||||||
}
|
}
|
||||||
|
|||||||
8
Models/Admin/AdminViewModel.cs
Normal file
8
Models/Admin/AdminViewModel.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public class AdminViewModel
|
||||||
|
{
|
||||||
|
public List<UserData> Users { get; set; }
|
||||||
|
public List<Token> Tokens { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
{
|
{
|
||||||
public class AuthCookie
|
public class AuthCookie
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public UserData UserData { get; set; }
|
||||||
public string UserName { get; set; }
|
|
||||||
public DateTime ExpiresOn { get; set; }
|
public DateTime ExpiresOn { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
{
|
{
|
||||||
public string UserName { get; set; }
|
public string UserName { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
public bool IsPersistent { get; set; } = false;
|
public bool IsPersistent { get; set; } = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Models/Login/Token.cs
Normal file
8
Models/Login/Token.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public class Token
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Body { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Models/Login/UserData.cs
Normal file
10
Models/Login/UserData.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public class UserData
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Models/OperationResponse.cs
Normal file
8
Models/OperationResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace CarCareTracker.Models
|
||||||
|
{
|
||||||
|
public class OperationResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CarCareTracker.External.Implementations;
|
using CarCareTracker.External.Implementations;
|
||||||
using CarCareTracker.External.Interfaces;
|
using CarCareTracker.External.Interfaces;
|
||||||
using CarCareTracker.Helper;
|
using CarCareTracker.Helper;
|
||||||
|
using CarCareTracker.Logic;
|
||||||
using CarCareTracker.Middleware;
|
using CarCareTracker.Middleware;
|
||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -17,14 +18,18 @@ builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAcc
|
|||||||
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
|
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
|
||||||
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
|
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
|
||||||
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
|
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
|
||||||
|
builder.Services.AddSingleton<IUserRecordDataAccess, UserRecordDataAccess>();
|
||||||
|
builder.Services.AddSingleton<ITokenRecordDataAccess, TokenRecordDataAccess>();
|
||||||
|
|
||||||
//configure helpers
|
//configure helpers
|
||||||
builder.Services.AddSingleton<IFileHelper, FileHelper>();
|
builder.Services.AddSingleton<IFileHelper, FileHelper>();
|
||||||
builder.Services.AddSingleton<IGasHelper, GasHelper>();
|
builder.Services.AddSingleton<IGasHelper, GasHelper>();
|
||||||
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
|
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
|
||||||
builder.Services.AddSingleton<ILoginHelper, LoginHelper>();
|
|
||||||
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
|
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
|
||||||
|
|
||||||
|
//configur logic
|
||||||
|
builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
|
||||||
|
|
||||||
if (!Directory.Exists("data"))
|
if (!Directory.Exists("data"))
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory("data");
|
Directory.CreateDirectory("data");
|
||||||
|
|||||||
62
Views/Admin/Index.cshtml
Normal file
62
Views/Admin/Index.cshtml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@model AdminViewModel
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-3">
|
||||||
|
<div class="row">
|
||||||
|
<button onclick="generateNewToken()" class="btn btn-primary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Generate User Token</button>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="d-flex">
|
||||||
|
<th scope="col" class="col-8">Token</th>
|
||||||
|
<th scope="col" class="col-4">Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (Token token in Model.Tokens)
|
||||||
|
{
|
||||||
|
<tr class="d-flex" style="cursor:pointer;">
|
||||||
|
<td class="col-8">@token.Body</td>
|
||||||
|
<td class="col-4">@token.Id</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr class="d-flex">
|
||||||
|
<th scope="col" class="col-4">UserName</th>
|
||||||
|
<th scope="col" class="col-2">Is Admin</th>
|
||||||
|
<th scope="col" class="col-4">Reset Password</th>
|
||||||
|
<th scope="col" class="col-2">Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (UserData userData in Model.Users)
|
||||||
|
{
|
||||||
|
<tr class="d-flex" style="cursor:pointer;">
|
||||||
|
<td class="col-4">@userData.UserName</td>
|
||||||
|
<td class="col-2">@userData.Id</td>
|
||||||
|
<td class="col-4">@userData.Id</td>
|
||||||
|
<td class="col-2">@userData.Id</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function reloadPage() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
function generateNewToken(){
|
||||||
|
$.get('/Admin/GenerateNewToken', function (data) {
|
||||||
|
if (data) {
|
||||||
|
reloadPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -23,6 +23,9 @@
|
|||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>Login</button>
|
<button type="button" class="btn btn-warning mt-2" onclick="performLogin()"><i class="bi bi-box-arrow-in-right me-2"></i>Login</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="/Login/Registration" class="btn btn-link mt-2">Register</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
31
Views/Login/Registration.cshtml
Normal file
31
Views/Login/Registration.cshtml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
@{
|
||||||
|
ViewData["Title"] = "LubeLogger - Register";
|
||||||
|
}
|
||||||
|
@section Scripts {
|
||||||
|
<script src="~/js/login.js" asp-append-version="true"></script>
|
||||||
|
}
|
||||||
|
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<img src="/defaults/lubelogger_logo.png" />
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputToken">Token</label>
|
||||||
|
<input type="text" id="inputToken" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputUserName">Username</label>
|
||||||
|
<input type="text" id="inputUserName" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="inputUserPassword">Password</label>
|
||||||
|
<input type="password" id="inputUserPassword" onkeyup="handlePasswordKeyPress(event)" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<button type="button" class="btn btn-warning mt-2" onclick="performRegistration()"><i class="bi bi-box-arrow-in-right me-2"></i>Register</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-grid">
|
||||||
|
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,19 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
function performRegistration() {
|
||||||
|
var token = $("#inputToken").val();
|
||||||
|
var userName = $("#inputUserName").val();
|
||||||
|
var userPassword = $("#inputUserPassword").val();
|
||||||
|
$.post('/Login/Register', { userName: userName, password: userPassword, token: token }, function (data) {
|
||||||
|
if (data.success) {
|
||||||
|
successToast(data.message);
|
||||||
|
setTimeout(function () { window.location.href = '/Login/Index' }, 500);
|
||||||
|
} else {
|
||||||
|
errorToast(data.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
function handlePasswordKeyPress(event) {
|
function handlePasswordKeyPress(event) {
|
||||||
if (event.keyCode == 13) {
|
if (event.keyCode == 13) {
|
||||||
performLogin();
|
performLogin();
|
||||||
|
|||||||
Reference in New Issue
Block a user