Merge pull request #84 from hargata/Hargata/users

Users Management
This commit is contained in:
Hargata Softworks
2024-01-14 13:14:33 -07:00
committed by GitHub
62 changed files with 2049 additions and 316 deletions

View File

@@ -1,8 +1,11 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Filter;
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace CarCareTracker.Controllers
{
@@ -19,6 +22,7 @@ namespace CarCareTracker.Controllers
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly IGasHelper _gasHelper;
private readonly IUserLogic _userLogic;
public APIController(IVehicleDataAccess dataAccess,
IGasHelper gasHelper,
IReminderHelper reminderHelper,
@@ -28,7 +32,8 @@ namespace CarCareTracker.Controllers
ICollisionRecordDataAccess collisionRecordDataAccess,
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess)
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IUserLogic userLogic)
{
_dataAccess = dataAccess;
_noteDataAccess = noteDataAccess;
@@ -40,19 +45,28 @@ namespace CarCareTracker.Controllers
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_gasHelper = gasHelper;
_reminderHelper = reminderHelper;
_userLogic = userLogic;
}
public IActionResult Index()
{
return View();
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
[HttpGet]
[Route("/api/vehicles")]
public IActionResult Vehicles()
{
var result = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
result = _userLogic.FilterUserVehicles(result, GetUserID());
}
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/servicerecords")]
public IActionResult ServiceRecords(int vehicleId)
@@ -61,6 +75,7 @@ namespace CarCareTracker.Controllers
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/repairrecords")]
public IActionResult RepairRecords(int vehicleId)
@@ -69,6 +84,7 @@ namespace CarCareTracker.Controllers
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/upgraderecords")]
public IActionResult UpgradeRecords(int vehicleId)
@@ -77,6 +93,7 @@ namespace CarCareTracker.Controllers
var result = vehicleRecords.Select(x => new ServiceRecordExportModel { Date = x.Date.ToShortDateString(), Description = x.Description, Cost = x.Cost.ToString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/taxrecords")]
public IActionResult TaxRecords(int vehicleId)
@@ -84,6 +101,7 @@ namespace CarCareTracker.Controllers
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/gasrecords")]
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
@@ -92,6 +110,7 @@ namespace CarCareTracker.Controllers
var result = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG).Select(x => new GasRecordExportModel { Date = x.Date, Odometer = x.Mileage.ToString(), Cost = x.Cost.ToString(), FuelConsumed = x.Gallons.ToString(), FuelEconomy = x.MilesPerGallon.ToString()});
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
[Route("/api/vehicle/reminders")]
public IActionResult Reminders(int vehicleId)

View File

@@ -0,0 +1,56 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Net;
using System.Net.Mail;
namespace CarCareTracker.Controllers
{
[Authorize(Roles = nameof(UserData.IsAdmin))]
public class AdminController : Controller
{
private ILoginLogic _loginLogic;
private IUserLogic _userLogic;
private IConfigHelper _configHelper;
public AdminController(ILoginLogic loginLogic, IUserLogic userLogic, IConfigHelper configHelper)
{
_loginLogic = loginLogic;
_userLogic = userLogic;
_configHelper = configHelper;
}
public IActionResult Index()
{
var viewModel = new AdminViewModel
{
Users = _loginLogic.GetAllUsers(),
Tokens = _loginLogic.GetAllTokens()
};
return View(viewModel);
}
public IActionResult GenerateNewToken(string emailAddress, bool autoNotify)
{
var result = _loginLogic.GenerateUserToken(emailAddress, autoNotify);
return Json(result);
}
[HttpPost]
public IActionResult DeleteToken(int tokenId)
{
var result = _loginLogic.DeleteUserToken(tokenId);
return Json(result);
}
[HttpPost]
public IActionResult DeleteUser(int userId)
{
var result =_userLogic.DeleteAllAccessToUser(userId) && _configHelper.DeleteUserConfig(userId) && _loginLogic.DeleteUser(userId);
return Json(result);
}
[HttpPost]
public IActionResult UpdateUserAdminStatus(int userId, bool isAdmin)
{
var result = _loginLogic.MakeUserAdmin(userId, isAdmin);
return Json(result);
}
}
}

12
Controllers/Error.cs Normal file
View File

@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Mvc;
namespace CarCareTracker.Controllers
{
public class ErrorController : Controller
{
public IActionResult Unauthorized()
{
return View("401");
}
}
}

View File

@@ -1,14 +1,11 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
using static System.Net.Mime.MediaTypeNames;
using System.Drawing;
using System.Linq.Expressions;
using Microsoft.Extensions.Logging;
using CarCareTracker.Helper;
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using CarCareTracker.Logic;
namespace CarCareTracker.Controllers
{
@@ -17,17 +14,23 @@ namespace CarCareTracker.Controllers
{
private readonly ILogger<HomeController> _logger;
private readonly IVehicleDataAccess _dataAccess;
private readonly IFileHelper _fileHelper;
private readonly IConfiguration _config;
private readonly IUserLogic _userLogic;
private readonly IConfigHelper _config;
public HomeController(ILogger<HomeController> logger, IVehicleDataAccess dataAccess, IFileHelper fileHelper, IConfiguration configuration)
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
IConfigHelper configuration)
{
_logger = logger;
_dataAccess = dataAccess;
_fileHelper = fileHelper;
_config = configuration;
_userLogic = userLogic;
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
public IActionResult Index(string tab = "garage")
{
return View(model: tab);
@@ -35,53 +38,22 @@ namespace CarCareTracker.Controllers
public IActionResult Garage()
{
var vehiclesStored = _dataAccess.GetVehicles();
if (!User.IsInRole(nameof(UserData.IsRootUser)))
{
vehiclesStored = _userLogic.FilterUserVehicles(vehiclesStored, GetUserID());
}
return PartialView("_GarageDisplay", vehiclesStored);
}
public IActionResult Settings()
{
var userConfig = new UserConfig
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
};
var userConfig = _config.GetUserConfig(User);
return PartialView("_Settings", userConfig);
}
[HttpPost]
public IActionResult WriteToSettings(UserConfig userConfig)
{
try
{
if (!System.IO.File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
userConfig.EnableAuth = existingUserConfig.EnableAuth;
userConfig.UserNameHash = existingUserConfig.UserNameHash;
userConfig.UserPasswordHash = existingUserConfig.UserPasswordHash;
} else
{
userConfig.EnableAuth = false;
userConfig.UserNameHash = string.Empty;
userConfig.UserPasswordHash = string.Empty;
}
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(userConfig));
return Json(true);
} catch (Exception ex)
{
_logger.LogError(ex, "Error on saving config file.");
}
return Json(false);
var result = _config.SaveUserConfig(User, userConfig);
return Json(result);
}
public IActionResult Privacy()
{

View File

@@ -1,4 +1,5 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.DataProtection;
@@ -13,22 +14,34 @@ namespace CarCareTracker.Controllers
public class LoginController : Controller
{
private IDataProtector _dataProtector;
private ILoginHelper _loginHelper;
private ILoginLogic _loginLogic;
private readonly ILogger<LoginController> _logger;
public LoginController(
ILogger<LoginController> logger,
IDataProtectionProvider securityProvider,
ILoginHelper loginHelper
ILoginLogic loginLogic
)
{
_dataProtector = securityProvider.CreateProtector("login");
_logger = logger;
_loginHelper = loginHelper;
_loginLogic = loginLogic;
}
public IActionResult Index()
{
return View();
}
public IActionResult Registration()
{
return View();
}
public IActionResult ForgotPassword()
{
return View();
}
public IActionResult ResetPassword()
{
return View();
}
[HttpPost]
public IActionResult Login(LoginModel credentials)
{
@@ -40,13 +53,12 @@ namespace CarCareTracker.Controllers
//compare it against hashed credentials
try
{
var loginIsValid = _loginHelper.ValidateUserCredentials(credentials);
if (loginIsValid)
var userData = _loginLogic.ValidateUserCredentials(credentials);
if (userData.Id != default)
{
AuthCookie authCookie = new AuthCookie
{
Id = 1, //this is hardcoded for now
UserName = credentials.UserName,
UserData = userData,
ExpiresOn = DateTime.Now.AddDays(credentials.IsPersistent ? 30 : 1)
};
var serializedCookie = JsonSerializer.Serialize(authCookie);
@@ -61,26 +73,33 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[HttpPost]
public IActionResult Register(LoginModel credentials)
{
var result = _loginLogic.RegisterNewUser(credentials);
return Json(result);
}
[HttpPost]
public IActionResult RequestResetPassword(LoginModel credentials)
{
var result = _loginLogic.RequestResetPassword(credentials);
return Json(result);
}
[HttpPost]
public IActionResult PerformPasswordReset(LoginModel credentials)
{
var result = _loginLogic.ResetPasswordByUser(credentials);
return Json(result);
}
[Authorize] //User must already be logged in to do this.
[HttpPost]
public IActionResult CreateLoginCreds(LoginModel credentials)
{
try
{
var configFileContents = System.IO.File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = 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);
//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);
var result = _loginLogic.CreateRootUserCredentials(credentials);
return Json(result);
}
catch (Exception ex)
{
@@ -94,19 +113,13 @@ namespace CarCareTracker.Controllers
{
try
{
var configFileContents = System.IO.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;
}
System.IO.File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(existingUserConfig));
var result = _loginLogic.DeleteRootUserCredentials();
//destroy any login cookies.
Response.Cookies.Delete("ACCESS_TOKEN");
return Json(true);
if (result)
{
Response.Cookies.Delete("ACCESS_TOKEN");
}
return Json(result);
}
catch (Exception ex)
{
@@ -121,20 +134,5 @@ namespace CarCareTracker.Controllers
Response.Cookies.Delete("ACCESS_TOKEN");
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();
}
}
}

View File

@@ -7,6 +7,9 @@ using CsvHelper;
using System.Globalization;
using Microsoft.AspNetCore.Authorization;
using CarCareTracker.MapProfile;
using System.Security.Claims;
using CarCareTracker.Logic;
using CarCareTracker.Filter;
namespace CarCareTracker.Controllers
{
@@ -24,11 +27,12 @@ namespace CarCareTracker.Controllers
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
private readonly IWebHostEnvironment _webEnv;
private readonly bool _useDescending;
private readonly IConfiguration _config;
private readonly IConfigHelper _config;
private readonly IFileHelper _fileHelper;
private readonly IGasHelper _gasHelper;
private readonly IReminderHelper _reminderHelper;
private readonly IReportHelper _reportHelper;
private readonly IUserLogic _userLogic;
public VehicleController(ILogger<VehicleController> logger,
IFileHelper fileHelper,
@@ -43,8 +47,9 @@ namespace CarCareTracker.Controllers
ITaxRecordDataAccess taxRecordDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IUpgradeRecordDataAccess upgradeRecordDataAccess,
IUserLogic userLogic,
IWebHostEnvironment webEnv,
IConfiguration config)
IConfigHelper config)
{
_logger = logger;
_dataAccess = dataAccess;
@@ -59,10 +64,16 @@ namespace CarCareTracker.Controllers
_taxRecordDataAccess = taxRecordDataAccess;
_reminderRecordDataAccess = reminderRecordDataAccess;
_upgradeRecordDataAccess = upgradeRecordDataAccess;
_userLogic = userLogic;
_webEnv = webEnv;
_config = config;
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
_useDescending = config.GetUserConfig(User).UseDescending;
}
private int GetUserID()
{
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult Index(int vehicleId)
{
@@ -74,6 +85,7 @@ namespace CarCareTracker.Controllers
{
return PartialView("_VehicleModal", new Vehicle());
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetEditVehiclePartialViewById(int vehicleId)
{
@@ -85,10 +97,22 @@ namespace CarCareTracker.Controllers
{
try
{
bool isNewAddition = vehicleInput.Id == default;
if (!isNewAddition)
{
if (!_userLogic.UserCanEditVehicle(GetUserID(), vehicleInput.Id))
{
return View("401");
}
}
//move image from temp folder to images folder.
vehicleInput.ImageLocation = _fileHelper.MoveFileFromTemp(vehicleInput.ImageLocation, "images/");
//save vehicle.
var result = _dataAccess.SaveVehicle(vehicleInput);
if (isNewAddition)
{
_userLogic.AddUserAccessToVehicle(GetUserID(), vehicleInput.Id);
}
return Json(result);
}
catch (Exception ex)
@@ -97,6 +121,7 @@ namespace CarCareTracker.Controllers
return Json(false);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult DeleteVehicle(int vehicleId)
{
@@ -108,6 +133,7 @@ namespace CarCareTracker.Controllers
_noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
_userLogic.DeleteAllAccessToVehicle(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId);
return Json(result);
}
@@ -117,6 +143,7 @@ namespace CarCareTracker.Controllers
{
return PartialView("_BulkDataImporter", mode);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
{
@@ -204,8 +231,8 @@ namespace CarCareTracker.Controllers
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
vehicleRecords = vehicleRecords.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
var convertedRecords = _gasHelper.GetGasRecordViewModels(vehicleRecords, useMPG, useUKMPG);
var exportData = convertedRecords.Select(x => new GasRecordExportModel { Date = x.Date.ToString(), Cost = x.Cost.ToString(), FuelConsumed = x.Gallons.ToString(), FuelEconomy = x.MilesPerGallon.ToString(), Odometer = x.Mileage.ToString() });
@@ -220,6 +247,7 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
{
@@ -353,6 +381,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Gas Records"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
{
@@ -360,8 +389,8 @@ namespace CarCareTracker.Controllers
//need it in ascending order to perform computation.
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
//check if the user uses MPG or Liters per 100km.
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG);
if (_useDescending)
{
@@ -419,6 +448,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Service Records"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetServiceRecordsByVehicleId(int vehicleId)
{
@@ -472,6 +502,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Collision Records"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCollisionRecordsByVehicleId(int vehicleId)
{
@@ -525,6 +556,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Tax Records"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetTaxRecordsByVehicleId(int vehicleId)
{
@@ -577,6 +609,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Reports"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetReportPartialView(int vehicleId)
{
@@ -642,8 +675,43 @@ namespace CarCareTracker.Controllers
{
viewModel.Years.Add(DateTime.Now.AddYears(i * -1).Year);
}
//get collaborators
var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId);
viewModel.Collaborators = collaborators;
//get MPG per month.
var userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x=>x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Average(y=>y.MilesPerGallon)
}).ToList();
viewModel.FuelMileageForVehicleByMonth = monthlyMileageData;
return PartialView("_Report", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCollaboratorsForVehicle(int vehicleId)
{
var result = _userLogic.GetCollaboratorsForVehicle(vehicleId);
return PartialView("_Collaborators", result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult AddCollaboratorsToVehicle(int vehicleId, string username)
{
var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{
var result = _userLogic.DeleteCollaboratorFromVehicle(userId, vehicleId);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetCostMakeUpForVehicle(int vehicleId, int year = 0)
{
@@ -670,6 +738,7 @@ namespace CarCareTracker.Controllers
};
return PartialView("_CostMakeUpReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetReminderMakeUpByVehicle(int vehicleId, int daysToAdd)
{
var reminders = GetRemindersAndUrgency(vehicleId, DateTime.Now.AddDays(daysToAdd));
@@ -682,6 +751,7 @@ namespace CarCareTracker.Controllers
};
return PartialView("_ReminderMakeUpReport", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
public IActionResult GetVehicleHistory(int vehicleId)
{
var vehicleHistory = new VehicleHistoryViewModel();
@@ -693,8 +763,8 @@ namespace CarCareTracker.Controllers
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
bool useMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]);
bool useUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)]);
bool useMPG = _config.GetUserConfig(User).UseMPG;
bool useUKMPG = _config.GetUserConfig(User).UseUKMPG;
vehicleHistory.TotalGasCost = gasRecords.Sum(x => x.Cost);
vehicleHistory.TotalCost = serviceRecords.Sum(x => x.Cost) + repairRecords.Sum(x => x.Cost) + upgradeRecords.Sum(x => x.Cost) + taxRecords.Sum(x => x.Cost);
var averageMPG = 0.00M;
@@ -745,6 +815,26 @@ namespace CarCareTracker.Controllers
vehicleHistory.VehicleHistory = reportData.OrderBy(x=>x.Date).ThenBy(x=>x.Odometer).ToList();
return PartialView("_VehicleHistory", vehicleHistory);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetMonthMPGByVehicle(int vehicleId, int year = 0)
{
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
var userConfig = _config.GetUserConfig(User);
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, userConfig.UseUKMPG);
if (year != 0)
{
mileageData.RemoveAll(x => DateTime.Parse(x.Date).Year != year);
}
mileageData.RemoveAll(x => x.MilesPerGallon == default);
var monthlyMileageData = mileageData.GroupBy(x => x.MonthId).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
{
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
Cost = x.Average(y => y.MilesPerGallon)
}).ToList();
return PartialView("_MPGByMonthReport", monthlyMileageData);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
public IActionResult GetCostByMonthByVehicle(int vehicleId, List<ImportMode> selectedMetrics, int year = 0)
{
@@ -783,6 +873,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Reminders"
[TypeFilter(typeof(CollaboratorFilter))]
private int GetMaxMileage(int vehicleId)
{
var numbersArray = new List<int>();
@@ -815,6 +906,7 @@ namespace CarCareTracker.Controllers
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
return results;
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
{
@@ -825,6 +917,7 @@ namespace CarCareTracker.Controllers
}
return Json(false);
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
{
@@ -875,6 +968,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Upgrade Records"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
{
@@ -928,6 +1022,7 @@ namespace CarCareTracker.Controllers
}
#endregion
#region "Notes"
[TypeFilter(typeof(CollaboratorFilter))]
[HttpGet]
public IActionResult GetNotesByVehicleId(int vehicleId)
{

View File

@@ -0,0 +1,57 @@
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 Token GetTokenRecordByEmailAddress(string emailAddress)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Token>(tableName);
var tokenRecord = table.FindOne(Query.EQ(nameof(Token.EmailAddress), emailAddress));
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;
};
}
}
}

View File

@@ -0,0 +1,88 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
namespace CarCareTracker.External.Implementations
{
public class UserAccessDataAccess : IUserAccessDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "useraccessrecords";
/// <summary>
/// Gets a list of vehicles user have access to.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public List<UserAccess> GetUserAccessByUserId(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x=>x.Id.UserId == userId).ToList();
};
}
public UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId).FirstOrDefault();
};
}
public List<UserAccess> GetUserAccessByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
return table.Find(x => x.Id.VehicleId == vehicleId).ToList();
};
}
public bool SaveUserAccess(UserAccess userAccess)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.Upsert(userAccess);
return true;
};
}
public bool DeleteUserAccess(int userId, int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId);
return true;
};
}
/// <summary>
/// Delete all access records when a vehicle is deleted.
/// </summary>
/// <param name="vehicleId"></param>
/// <returns></returns>
public bool DeleteAllAccessRecordsByVehicleId(int vehicleId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x=>x.Id.VehicleId == vehicleId);
return true;
};
}
/// <summary>
/// Delee all access records when a user is deleted.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>
public bool DeleteAllAccessRecordsByUserId(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserAccess>(tableName);
table.DeleteMany(x => x.Id.UserId == userId);
return true;
};
}
}
}

View File

@@ -0,0 +1,40 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using LiteDB;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace CarCareTracker.External.Implementations
{
public class UserConfigDataAccess: IUserConfigDataAccess
{
private static string dbName = StaticHelper.DbName;
private static string tableName = "userconfigrecords";
public UserConfigData GetUserConfig(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
return table.FindById(userId);
};
}
public bool SaveUserConfig(UserConfigData userConfigData)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
table.Upsert(userConfigData);
return true;
};
}
public bool DeleteUserConfig(int userId)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserConfigData>(tableName);
table.Delete(userId);
return true;
};
}
}
}

View File

@@ -0,0 +1,66 @@
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 GetUserRecordByEmailAddress(string emailAddress)
{
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<UserData>(tableName);
var userRecord = table.FindOne(Query.EQ(nameof(UserData.EmailAddress), emailAddress));
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;
};
}
}
}

View File

@@ -14,7 +14,7 @@ namespace CarCareTracker.External.Implementations
using (var db = new LiteDatabase(dbName))
{
var table = db.GetCollection<Vehicle>(tableName);
table.Upsert(vehicle);
var result = table.Upsert(vehicle);
return true;
};
}

View File

@@ -0,0 +1,13 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface ITokenRecordDataAccess
{
public List<Token> GetTokens();
public Token GetTokenRecordByBody(string tokenBody);
public Token GetTokenRecordByEmailAddress(string emailAddress);
public bool CreateNewToken(Token token);
public bool DeleteToken(int tokenId);
}
}

View File

@@ -0,0 +1,15 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserAccessDataAccess
{
List<UserAccess> GetUserAccessByUserId(int userId);
UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId);
List<UserAccess> GetUserAccessByVehicleId(int vehicleId);
bool SaveUserAccess(UserAccess userAccess);
bool DeleteUserAccess(int userId, int vehicleId);
bool DeleteAllAccessRecordsByVehicleId(int vehicleId);
bool DeleteAllAccessRecordsByUserId(int userId);
}
}

View File

@@ -0,0 +1,11 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserConfigDataAccess
{
public UserConfigData GetUserConfig(int userId);
public bool SaveUserConfig(UserConfigData userConfigData);
public bool DeleteUserConfig(int userId);
}
}

View File

@@ -0,0 +1,14 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserRecordDataAccess
{
public List<UserData> GetUsers();
public UserData GetUserRecordByUserName(string userName);
public UserData GetUserRecordByEmailAddress(string emailAddress);
public UserData GetUserRecordById(int userId);
public bool SaveUserRecord(UserData userRecord);
public bool DeleteUserRecord(int userId);
}
}

View File

@@ -0,0 +1,28 @@
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;
namespace CarCareTracker.Filter
{
public class CollaboratorFilter: ActionFilterAttribute
{
private readonly IUserLogic _userLogic;
public CollaboratorFilter(IUserLogic userLogic) {
_userLogic = userLogic;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.IsInRole(nameof(UserData.IsRootUser)))
{
var vehicleId = int.Parse(filterContext.ActionArguments["vehicleId"].ToString());
var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
if (!_userLogic.UserCanEditVehicle(userId, vehicleId))
{
filterContext.Result = new RedirectResult("/Error/Unauthorized");
}
}
}
}
}

137
Helper/ConfigHelper.cs Normal file
View File

@@ -0,0 +1,137 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Claims;
namespace CarCareTracker.Helper
{
public interface IConfigHelper
{
UserConfig GetUserConfig(ClaimsPrincipal user);
bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData);
public bool DeleteUserConfig(int userId);
}
public class ConfigHelper : IConfigHelper
{
private readonly IConfiguration _config;
private readonly IUserConfigDataAccess _userConfig;
private IMemoryCache _cache;
public ConfigHelper(IConfiguration serverConfig,
IUserConfigDataAccess userConfig,
IMemoryCache memoryCache)
{
_config = serverConfig;
_userConfig = userConfig;
_cache = memoryCache;
}
public bool SaveUserConfig(ClaimsPrincipal user, UserConfig configData)
{
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
int userId = 0;
if (storedUserId != null)
{
userId = int.Parse(storedUserId);
}
bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1;
if (isRootUser)
{
try
{
if (!File.Exists(StaticHelper.UserConfigPath))
{
//if file doesn't exist it might be because it's running on a mounted volume in docker.
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(new UserConfig()));
}
var configFileContents = File.ReadAllText(StaticHelper.UserConfigPath);
var existingUserConfig = System.Text.Json.JsonSerializer.Deserialize<UserConfig>(configFileContents);
if (existingUserConfig is not null)
{
//copy over settings that are off limits on the settings page.
configData.EnableAuth = existingUserConfig.EnableAuth;
configData.UserNameHash = existingUserConfig.UserNameHash;
configData.UserPasswordHash = existingUserConfig.UserPasswordHash;
}
else
{
configData.EnableAuth = false;
configData.UserNameHash = string.Empty;
configData.UserPasswordHash = string.Empty;
}
File.WriteAllText(StaticHelper.UserConfigPath, System.Text.Json.JsonSerializer.Serialize(configData));
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return true;
}
catch (Exception ex)
{
return false;
}
} else
{
var userConfig = new UserConfigData()
{
Id = userId,
UserConfig = configData
};
var result = _userConfig.SaveUserConfig(userConfig);
_cache.Set<UserConfig>($"userConfig_{userId}", configData);
return result;
}
}
public bool DeleteUserConfig(int userId)
{
_cache.Remove($"userConfig_{userId}");
var result = _userConfig.DeleteUserConfig(userId);
return result;
}
public UserConfig GetUserConfig(ClaimsPrincipal user)
{
var serverConfig = new UserConfig
{
EnableCsvImports = bool.Parse(_config[nameof(UserConfig.EnableCsvImports)]),
UseDarkMode = bool.Parse(_config[nameof(UserConfig.UseDarkMode)]),
UseMPG = bool.Parse(_config[nameof(UserConfig.UseMPG)]),
UseDescending = bool.Parse(_config[nameof(UserConfig.UseDescending)]),
EnableAuth = bool.Parse(_config[nameof(UserConfig.EnableAuth)]),
HideZero = bool.Parse(_config[nameof(UserConfig.HideZero)]),
UseUKMPG = bool.Parse(_config[nameof(UserConfig.UseUKMPG)])
};
int userId = 0;
if (user != null)
{
var storedUserId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (storedUserId != null)
{
userId = int.Parse(storedUserId);
}
} else
{
return serverConfig;
}
return _cache.GetOrCreate<UserConfig>($"userConfig_{userId}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromHours(1);
if (!user.Identity.IsAuthenticated)
{
return serverConfig;
}
bool isRootUser = user.IsInRole(nameof(UserData.IsRootUser)) || userId == -1;
if (isRootUser)
{
return serverConfig;
}
else
{
var result = _userConfig.GetUserConfig(userId);
if (result == null)
{
return serverConfig;
}
else
{
return result.UserConfig;
}
}
});
}
}
}

View File

@@ -36,6 +36,7 @@ namespace CarCareTracker.Helper
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
MonthId = currentObject.Date.Month,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = convertedConsumption,
@@ -73,6 +74,7 @@ namespace CarCareTracker.Helper
{
Id = currentObject.Id,
VehicleId = currentObject.VehicleId,
MonthId = currentObject.Date.Month,
Date = currentObject.Date.ToShortDateString(),
Mileage = currentObject.Mileage,
Gallons = convertedConsumption,

View File

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

85
Helper/MailHelper.cs Normal file
View File

@@ -0,0 +1,85 @@
using CarCareTracker.Models;
using System.Net.Mail;
using System.Net;
namespace CarCareTracker.Helper
{
public interface IMailHelper
{
OperationResponse NotifyUserForRegistration(string emailAddress, string token);
OperationResponse NotifyUserForPasswordReset(string emailAddress, string token);
}
public class MailHelper : IMailHelper
{
private readonly MailConfig mailConfig;
public MailHelper(
IConfiguration config
) {
//load mailConfig from Configuration
mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
}
public OperationResponse NotifyUserForRegistration(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token)) {
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
}
string emailSubject = "Your Registration Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please complete your registration for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public OperationResponse NotifyUserForPasswordReset(string emailAddress, string token)
{
if (string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
return new OperationResponse { Success = false, Message = "SMTP Server Not Setup" };
}
if (string.IsNullOrWhiteSpace(emailAddress) || string.IsNullOrWhiteSpace(token))
{
return new OperationResponse { Success = false, Message = "Email Address or Token is invalid" };
}
string emailSubject = "Your Password Reset Token for LubeLogger";
string emailBody = $"A token has been generated on your behalf, please reset your password for LubeLogger using the token: {token}";
var result = SendEmail(emailAddress, emailSubject, emailBody);
if (result)
{
return new OperationResponse { Success = true, Message = "Email Sent!" };
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
private bool SendEmail(string emailTo, string emailSubject, string emailBody) {
string to = emailTo;
string from = mailConfig.EmailFrom;
var server = mailConfig.EmailServer;
MailMessage message = new MailMessage(from, to);
message.Subject = emailSubject;
message.Body = emailBody;
SmtpClient client = new SmtpClient(server);
client.EnableSsl = mailConfig.UseSSL;
client.Port = mailConfig.Port;
client.Credentials = new NetworkCredential(mailConfig.Username, mailConfig.Password);
try
{
client.Send(message);
return true;
}
catch (Exception ex)
{
return false;
}
}
}
}

View File

@@ -7,6 +7,7 @@
{
public static string DbName = "data/cartracker.db";
public static string UserConfigPath = "config/userConfig.json";
public static string GenericErrorMessage = "An error occurred, please try again later";
public static string TruncateStrings(string input, int maxLength = 25)
{

354
Logic/LoginLogic.cs Normal file
View File

@@ -0,0 +1,354 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Net;
using System.Net.Mail;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace CarCareTracker.Logic
{
public interface ILoginLogic
{
bool MakeUserAdmin(int userId, bool isAdmin);
OperationResponse GenerateUserToken(string emailAddress, bool autoNotify);
bool DeleteUserToken(int tokenId);
bool DeleteUser(int userId);
OperationResponse RegisterNewUser(LoginModel credentials);
OperationResponse RequestResetPassword(LoginModel credentials);
OperationResponse ResetPasswordByUser(LoginModel credentials);
OperationResponse ResetUserPassword(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials);
bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials);
bool DeleteRootUserCredentials();
List<UserData> GetAllUsers();
List<Token> GetAllTokens();
}
public class LoginLogic : ILoginLogic
{
private readonly IUserRecordDataAccess _userData;
private readonly ITokenRecordDataAccess _tokenData;
private readonly IMailHelper _mailHelper;
private IMemoryCache _cache;
public LoginLogic(IUserRecordDataAccess userData,
ITokenRecordDataAccess tokenData,
IMailHelper mailHelper,
IMemoryCache memoryCache)
{
_userData = userData;
_tokenData = tokenData;
_mailHelper = mailHelper;
_cache = memoryCache;
}
public bool CheckIfUserIsValid(int userId)
{
if (userId == -1)
{
return true;
}
var result = _userData.GetUserRecordById(userId);
if (result == null)
{
return false;
} else
{
return result.Id != 0;
}
}
//handles user registration
public OperationResponse RegisterNewUser(LoginModel credentials)
{
//validate their token.
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
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.EmailAddress) || 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" };
}
var existingUserWithEmail = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUserWithEmail.Id != default)
{
return new OperationResponse { Success = false, Message = "A user with that email already exists" };
}
//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),
EmailAddress = credentials.EmailAddress
};
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>
/// Generates a token and notifies user via email so they can reset their password.
/// </summary>
/// <param name="credentials"></param>
/// <returns></returns>
public OperationResponse RequestResetPassword(LoginModel credentials)
{
var existingUser = _userData.GetUserRecordByUserName(credentials.UserName);
if (existingUser.Id != default)
{
//user exists, generate a token and send email.
//check to see if there is an existing token sent to the user.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(existingUser.EmailAddress);
if (existingToken.Id == default)
{
var token = new Token()
{
Body = NewToken(),
EmailAddress = existingUser.EmailAddress
};
var result = _tokenData.CreateNewToken(token);
if (result)
{
result = _mailHelper.NotifyUserForPasswordReset(existingUser.EmailAddress, token.Body).Success;
}
}
}
//for security purposes we want to always return true for this method.
//otherwise someone can spam the reset password method to sniff out users.
return new OperationResponse { Success = true, Message = "If your user exists in the system you should receive an email shortly with instructions on how to proceed." };
}
public OperationResponse ResetPasswordByUser(LoginModel credentials)
{
var existingToken = _tokenData.GetTokenRecordByBody(credentials.Token);
if (existingToken.Id == default || existingToken.EmailAddress != credentials.EmailAddress)
{
return new OperationResponse { Success = false, Message = "Invalid Token" };
}
if (string.IsNullOrWhiteSpace(credentials.Password))
{
return new OperationResponse { Success = false, Message = "New Password cannot be blank" };
}
//if token is valid.
var existingUser = _userData.GetUserRecordByEmailAddress(credentials.EmailAddress);
if (existingUser.Id == default)
{
return new OperationResponse { Success = false, Message = "Unable to locate user" };
}
existingUser.Password = GetHash(credentials.Password);
var result = _userData.SaveUserRecord(existingUser);
//delete token
_tokenData.DeleteToken(existingToken.Id);
if (result)
{
return new OperationResponse { Success = true, Message = "Password resetted, you will be redirected to login page shortly." };
} else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
/// <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,
IsRootUser = 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 bool MakeUserAdmin(int userId, bool isAdmin)
{
var user = _userData.GetUserRecordById(userId);
if (user == default)
{
return false;
}
user.IsAdmin = isAdmin;
var result = _userData.SaveUserRecord(user);
return result;
}
public List<UserData> GetAllUsers()
{
var result = _userData.GetUsers();
return result;
}
public List<Token> GetAllTokens()
{
var result = _tokenData.GetTokens();
return result;
}
public OperationResponse GenerateUserToken(string emailAddress, bool autoNotify)
{
//check if email address already has a token attached to it.
var existingToken = _tokenData.GetTokenRecordByEmailAddress(emailAddress);
if (existingToken.Id != default)
{
return new OperationResponse { Success = false, Message = "There is an existing token tied to this email address" };
}
var token = new Token()
{
Body = NewToken(),
EmailAddress = emailAddress
};
var result = _tokenData.CreateNewToken(token);
if (result && autoNotify)
{
result = _mailHelper.NotifyUserForRegistration(emailAddress, token.Body).Success;
if (!result)
{
return new OperationResponse { Success = false, Message = "Token Generated, but Email failed to send, please check your SMTP settings." };
}
}
if (result)
{
return new OperationResponse { Success = true, Message = "Token Generated!" };
}
else
{
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public bool DeleteUserToken(int tokenId)
{
var result = _tokenData.DeleteToken(tokenId);
return result;
}
public bool DeleteUser(int userId)
{
var result = _userData.DeleteUserRecord(userId);
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));
_cache.Remove("userConfig_-1");
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;
}
//clear out the cached config for the root user.
_cache.Remove("userConfig_-1");
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();
}
private string NewToken()
{
return Guid.NewGuid().ToString().Substring(0, 8);
}
}
}

118
Logic/UserLogic.cs Normal file
View File

@@ -0,0 +1,118 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace CarCareTracker.Logic
{
public interface IUserLogic
{
List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId);
bool AddUserAccessToVehicle(int userId, int vehicleId);
bool DeleteCollaboratorFromVehicle(int userId, int vehicleId);
OperationResponse AddCollaboratorToVehicle(int vehicleId, string username);
List<Vehicle> FilterUserVehicles(List<Vehicle> results, int userId);
bool UserCanEditVehicle(int userId, int vehicleId);
bool DeleteAllAccessToVehicle(int vehicleId);
bool DeleteAllAccessToUser(int userId);
}
public class UserLogic: IUserLogic
{
private readonly IUserAccessDataAccess _userAccess;
private readonly IUserRecordDataAccess _userData;
public UserLogic(IUserAccessDataAccess userAccess,
IUserRecordDataAccess userData) {
_userAccess = userAccess;
_userData = userData;
}
public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId)
{
var result = _userAccess.GetUserAccessByVehicleId(vehicleId);
var convertedResult = new List<UserCollaborator>();
//convert useraccess to usercollaborator
foreach(UserAccess userAccess in result)
{
var userCollaborator = new UserCollaborator
{
UserName = _userData.GetUserRecordById(userAccess.Id.UserId).UserName,
UserVehicle = userAccess.Id
};
convertedResult.Add(userCollaborator);
}
return convertedResult;
}
public OperationResponse AddCollaboratorToVehicle(int vehicleId, string username)
{
//try to find existing user.
var existingUser = _userData.GetUserRecordByUserName(username);
if (existingUser.Id != default)
{
//user exists.
var result = AddUserAccessToVehicle(existingUser.Id, vehicleId);
if (result)
{
return new OperationResponse { Success = true, Message = "Collaborator Added" };
}
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
return new OperationResponse { Success = false, Message = $"Unable to find user {username} in the system" };
}
public bool DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{
var result = _userAccess.DeleteUserAccess(userId, vehicleId);
return result;
}
public bool AddUserAccessToVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
var userVehicle = new UserVehicle { UserId = userId, VehicleId = vehicleId };
var userAccess = new UserAccess { Id = userVehicle };
var result = _userAccess.SaveUserAccess(userAccess);
return result;
}
public List<Vehicle> FilterUserVehicles(List<Vehicle> results, int userId)
{
//user is root user.
if (userId == -1)
{
return results;
}
var accessibleVehicles = _userAccess.GetUserAccessByUserId(userId);
if (accessibleVehicles.Any())
{
var vehicleIds = accessibleVehicles.Select(x => x.Id.VehicleId);
return results.Where(x => vehicleIds.Contains(x.Id)).ToList();
}
else
{
return new List<Vehicle>();
}
}
public bool UserCanEditVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(userId, vehicleId);
if (userAccess != null)
{
return true;
}
return false;
}
public bool DeleteAllAccessToVehicle(int vehicleId)
{
var result = _userAccess.DeleteAllAccessRecordsByVehicleId(vehicleId);
return result;
}
public bool DeleteAllAccessToUser(int userId)
{
var result = _userAccess.DeleteAllAccessRecordsByUserId(userId);
return result;
}
}
}

View File

@@ -9,7 +9,7 @@ namespace CarCareTracker.MapProfile
{
Map(m => m.Date).Name(["date", "fuelup_date"]);
Map(m => m.Odometer).Name(["odometer"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fueleconomy", "fuelconsumed"]);
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fuelconsumed"]);
Map(m => m.Cost).Name(["cost", "total cost", "totalcost", "total price"]);
Map(m => m.Notes).Name("notes", "note");
Map(m => m.Price).Name(["price"]);

View File

@@ -1,4 +1,4 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Components.Web;
@@ -15,20 +15,20 @@ namespace CarCareTracker.Middleware
{
private IHttpContextAccessor _httpContext;
private IDataProtector _dataProtector;
private ILoginHelper _loginHelper;
private ILoginLogic _loginLogic;
private bool enableAuth;
public Authen(
IOptionsMonitor<AuthenticationSchemeOptions> options,
UrlEncoder encoder,
ILoggerFactory logger,
IConfiguration configuration,
ILoginHelper loginHelper,
ILoginLogic loginLogic,
IDataProtectionProvider securityProvider,
IHttpContextAccessor httpContext) : base(options, logger, encoder)
{
_httpContext = httpContext;
_dataProtector = securityProvider.CreateProtector("login");
_loginHelper = loginHelper;
_loginLogic = loginLogic;
enableAuth = bool.Parse(configuration["EnableAuth"]);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
@@ -39,7 +39,9 @@ namespace CarCareTracker.Middleware
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, "admin")
new(ClaimTypes.Name, "admin"),
new(ClaimTypes.NameIdentifier, "-1"),
new(ClaimTypes.Role, nameof(UserData.IsRootUser))
};
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
@@ -64,16 +66,26 @@ namespace CarCareTracker.Middleware
if (splitString.Count() != 2)
{
return AuthenticateResult.Fail("Invalid credentials");
} else
}
else
{
var validUser = _loginHelper.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
if (validUser)
var userData = _loginLogic.ValidateUserCredentials(new LoginModel { UserName = splitString[0], Password = splitString[1] });
if (userData.Id != default)
{
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, splitString[0])
new(ClaimTypes.Name, splitString[0]),
new(ClaimTypes.NameIdentifier, userData.Id.ToString())
};
if (userData.IsAdmin)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
}
if (userData.IsRootUser)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
@@ -82,33 +94,55 @@ namespace CarCareTracker.Middleware
}
else if (!string.IsNullOrWhiteSpace(access_token))
{
//decrypt the access token.
var decryptedCookie = _dataProtector.Unprotect(access_token);
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
if (authCookie != null)
try
{
//validate auth cookie
if (authCookie.ExpiresOn < DateTime.Now)
//decrypt the access token.
var decryptedCookie = _dataProtector.Unprotect(access_token);
AuthCookie authCookie = JsonSerializer.Deserialize<AuthCookie>(decryptedCookie);
if (authCookie != null)
{
//if cookie is expired
return AuthenticateResult.Fail("Expired credentials");
}
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>
//validate auth cookie
if (authCookie.ExpiresOn < DateTime.Now)
{
new(ClaimTypes.Name, authCookie.UserName)
};
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), this.Scheme.Name);
return AuthenticateResult.Success(ticket);
//if cookie is expired
return AuthenticateResult.Fail("Expired credentials");
}
else if (authCookie.UserData is null || authCookie.UserData.Id == default || string.IsNullOrWhiteSpace(authCookie.UserData.UserName))
{
return AuthenticateResult.Fail("Corrupted credentials");
}
else
{
if (!_loginLogic.CheckIfUserIsValid(authCookie.UserData.Id))
{
return AuthenticateResult.Fail("Cookie points to non-existant user.");
}
//validate if user is still valid
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, authCookie.UserData.UserName),
new(ClaimTypes.NameIdentifier, authCookie.UserData.Id.ToString()),
new(ClaimTypes.Role, "CookieAuth")
};
if (authCookie.UserData.IsAdmin)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
}
if (authCookie.UserData.IsRootUser)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
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");
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class AdminViewModel
{
public List<UserData> Users { get; set; }
public List<Token> Tokens { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class MailConfig
{
public string EmailServer { get; set; }
public string EmailFrom { get; set; }
public bool UseSSL { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@@ -4,6 +4,7 @@
{
public int Id { get; set; }
public int VehicleId { get; set; }
public int MonthId { get; set; }
public string Date { get; set; }
/// <summary>
/// American moment

View File

@@ -2,8 +2,7 @@
{
public class AuthCookie
{
public int Id { get; set; }
public string UserName { get; set; }
public UserData UserData { get; set; }
public DateTime ExpiresOn { get; set; }
}
}

View File

@@ -4,6 +4,8 @@
{
public string UserName { get; set; }
public string Password { get; set; }
public string EmailAddress { get; set; }
public string Token { get; set; }
public bool IsPersistent { get; set; } = false;
}
}

9
Models/Login/Token.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace CarCareTracker.Models
{
public class Token
{
public int Id { get; set; }
public string Body { get; set; }
public string EmailAddress { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class OperationResponse
{
public bool Success { get; set; }
public string Message { get; set; }
}
}

View File

@@ -3,8 +3,10 @@
public class ReportViewModel
{
public List<CostForVehicleByMonth> CostForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public List<CostForVehicleByMonth> FuelMileageForVehicleByMonth { get; set; } = new List<CostForVehicleByMonth>();
public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle();
public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle();
public List<int> Years { get; set; } = new List<int>();
public List<UserCollaborator> Collaborators { get; set; } = new List<UserCollaborator>();
}
}

12
Models/User/UserAccess.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class UserVehicle
{
public int UserId { get; set; }
public int VehicleId { get; set; }
}
public class UserAccess
{
public UserVehicle Id { get; set; }
}
}

View File

@@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class UserCollaborator
{
public string UserName { get; set; }
public UserVehicle UserVehicle { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace CarCareTracker.Models
{
public class UserConfigData
{
/// <summary>
/// User ID
/// </summary>
public int Id { get; set; }
public UserConfig UserConfig { get; set; }
}
}

12
Models/User/UserData.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class UserData
{
public int Id { get; set; }
public string UserName { get; set; }
public string EmailAddress { get; set; }
public string Password { get; set; }
public bool IsAdmin { get; set; }
public bool IsRootUser { get; set; } = false;
}
}

View File

@@ -1,6 +1,7 @@
using CarCareTracker.External.Implementations;
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Middleware;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
@@ -17,13 +18,22 @@ builder.Services.AddSingleton<ICollisionRecordDataAccess, CollisionRecordDataAcc
builder.Services.AddSingleton<ITaxRecordDataAccess, TaxRecordDataAccess>();
builder.Services.AddSingleton<IReminderRecordDataAccess, ReminderRecordDataAccess>();
builder.Services.AddSingleton<IUpgradeRecordDataAccess, UpgradeRecordDataAccess>();
builder.Services.AddSingleton<IUserRecordDataAccess, UserRecordDataAccess>();
builder.Services.AddSingleton<ITokenRecordDataAccess, TokenRecordDataAccess>();
builder.Services.AddSingleton<IUserAccessDataAccess, UserAccessDataAccess>();
builder.Services.AddSingleton<IUserConfigDataAccess, UserConfigDataAccess>();
//configure helpers
builder.Services.AddSingleton<IFileHelper, FileHelper>();
builder.Services.AddSingleton<IGasHelper, GasHelper>();
builder.Services.AddSingleton<IReminderHelper, ReminderHelper>();
builder.Services.AddSingleton<ILoginHelper, LoginHelper>();
builder.Services.AddSingleton<IReportHelper, ReportHelper>();
builder.Services.AddSingleton<IMailHelper, MailHelper>();
builder.Services.AddSingleton<IConfigHelper, ConfigHelper>();
//configure logic
builder.Services.AddSingleton<ILoginLogic, LoginLogic>();
builder.Services.AddSingleton<IUserLogic, UserLogic>();
if (!Directory.Exists("data"))
{

153
Views/Admin/Index.cshtml Normal file
View File

@@ -0,0 +1,153 @@
@{
ViewData["Title"] = "Admin";
}
@inject IConfiguration config;
@{
bool emailServerIsSetup = true;
var mailConfig = config.GetSection("MailConfig").Get<MailConfig>();
if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer))
{
emailServerIsSetup = false;
}
}
@model AdminViewModel
<div class="container">
<div class="row">
<div class="col-1">
<a href="/Home" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></a>
</div>
<div class="col-11">
<span class="display-6">Admin Panel</span>
</div>
</div>
<hr />
<div class="row">
<div class="col-md-5 col-12">
<span class="lead">Tokens</span>
<hr />
<div class="row">
<div class="col-6">
<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>
<div class="col-6 d-flex align-items-center">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" role="switch" id="enableAutoNotify" @(emailServerIsSetup ? "checked" : "disabled")>
<label class="form-check-label" for="enableAutoNotify">Auto Notify(via Email)</label>
</div>
</div>
</div>
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-4">Token</th>
<th scope="col" class="col-6">Issued To</th>
<th scope="col" class="col-2">Delete</th>
</tr>
</thead>
<tbody>
@foreach (Token token in Model.Tokens)
{
<tr class="d-flex">
<td class="col-4" style="cursor:pointer;" onclick="copyToClipboard(this)">@token.Body</td>
<td class="col-6 text-truncate">@token.EmailAddress</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="deleteToken(@token.Id, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col-12 col-md-7">
<span class="lead">Users</span>
<hr />
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-4">Username</th>
<th scope="col" class="col-4">Email</th>
<th scope="col" class="col-2">Is Admin</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-4">@userData.EmailAddress</td>
<td class="col-2"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "")/></td>
<td class="col-2"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
<script>
function updateUserAdmin(userId, sender){
var isChecked = $(sender).is(":checked");
$.post('/Admin/UpdateUserAdminStatus', { userId: userId, isAdmin: isChecked }, function (data) {
if (data){
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
});
}
function reloadPage() {
window.location.reload();
}
function deleteToken(tokenId) {
$.post(`/Admin/DeleteToken?tokenId=${tokenId}`, function (data) {
if (data) {
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
});
}
function deleteUser(userId) {
$.post(`/Admin/DeleteUser?userId=${userId}`, function (data) {
if (data) {
reloadPage();
} else {
errorToast("An error has occurred, please try again later.");
}
})
}
function copyToClipboard(e) {
var textToCopy = e.textContent;
navigator.clipboard.writeText(textToCopy);
successToast("Copied to Clipboard");
}
function generateNewToken() {
Swal.fire({
title: 'Generate Token',
html: `
<input type="text" id="inputEmail" class="swal2-input" placeholder="Email Address">
`,
confirmButtonText: 'Generate',
focusConfirm: false,
preConfirm: () => {
const emailAddress = $("#inputEmail").val();
if (!emailAddress) {
Swal.showValidationMessage(`Please enter an email address`)
}
return { emailAddress }
},
}).then(function (result) {
if (result.isConfirmed) {
var autoNotify = $("#enableAutoNotify").is(":checked");
$.get('/Admin/GenerateNewToken', { emailAddress: result.value.emailAddress, autoNotify: autoNotify }, function (data) {
if (data.success) {
reloadPage();
} else {
errorToast(data.message)
}
});
}
});
}
</script>

View File

@@ -1,6 +1,7 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableAuth = bool.Parse(Configuration[nameof(UserConfig.EnableAuth)]);
var enableAuth = config.GetUserConfig(User).EnableAuth;
}
@model string
@{
@@ -17,8 +18,14 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button>
</li>
@if (enableAuth)
@if (User.IsInRole("CookieAuth"))
{
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li class="nav-item" role="presentation">
<a class="dropdown-item" href="/Admin"><span class="display-3 ms-2"><i class="bi bi-people me-2"></i>Admin Panel</span></a>
</li>
}
<li class="nav-item" role="presentation">
<button class="nav-link" onclick="performLogOut()"><span class="display-3 ms-2"><i class="bi bi-box-arrow-right me-2"></i>Logout</span></button>
</li>
@@ -42,10 +49,21 @@
<li class="nav-item ms-auto" role="presentation">
<button class="nav-link @(Model == "settings" ? "active" : "")" id="settings-tab" data-bs-toggle="tab" data-bs-target="#settings-tab-pane" type="button" role="tab"><i class="bi bi-gear me-2"></i>Settings</button>
</li>
@if (enableAuth)
@if (User.IsInRole("CookieAuth"))
{
<li class="nav-item">
<button class="nav-link" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
<li class="nav-item dropdown" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false"><i class="bi bi-person me-2"></i>@User.Identity.Name</a>
<ul class="dropdown-menu">
@if (User.IsInRole(nameof(UserData.IsAdmin)))
{
<li>
<a class="dropdown-item" href="/Admin"><i class="bi bi-people me-2"></i>Admin Panel</a>
</li>
}
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>Logout</button>
</li>
</ul>
</li>
}
</ul>
@@ -69,6 +87,5 @@
</div>
<script>
loadGarage();
loadSettings();
bindWindowResize();
</script>

View File

@@ -32,10 +32,13 @@
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="hideZero" checked="@Model.HideZero">
<label class="form-check-label" for="hideZero">Replace @(0.ToString("C")) Costs with ---</label>
</div>
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="form-check form-switch">
<input class="form-check-input" onChange="enableAuthCheckChanged()" type="checkbox" role="switch" id="enableAuth" checked="@Model.EnableAuth">
<label class="form-check-label" for="enableAuth">Enable Authentication</label>
</div>
}
</div>
</div>
<div class="row">
@@ -120,7 +123,7 @@
if (result.isConfirmed) {
$.post('/Login/CreateLoginCreds', { userName: result.value.username, password: result.value.password }, function (data) {
if (data) {
window.location.href = '/Login';
setTimeout(function () { window.location.href = '/Login' }, 500);
} else {
errorToast("An error occurred, please try again later.");
}

View File

@@ -0,0 +1,26 @@
@{
ViewData["Title"] = "LubeLogger - Login";
}
@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="inputUserName">Username</label>
<input type="text" id="inputUserName" class="form-control">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="requestPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Request</button>
</div>
<div class="d-grid">
<a href="/Login/ResetPassword" class="btn btn-link mt-2">I Have a Token</a>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
</div>
</div>
</div>
</div>

View File

@@ -23,6 +23,12 @@
<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>
</div>
<div class="d-grid">
<a href="/Login/ForgotPassword" class="btn btn-link mt-2">Forgot Password</a>
</div>
<div class="d-grid">
<a href="/Login/Registration" class="btn btn-link mt-2">Register</a>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,35 @@
@{
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">Email Address</label>
<input type="text" id="inputEmail" 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" 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>

View 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">Email Address</label>
<input type="text" id="inputEmail" class="form-control">
</div>
<div class="form-group">
<label for="inputUserPassword">New Password</label>
<input type="password" id="inputUserPassword" class="form-control">
</div>
<div class="d-grid">
<button type="button" class="btn btn-warning mt-2" onclick="performPasswordReset()"><i class="bi bi-box-arrow-in-right me-2"></i>Reset Password</button>
</div>
<div class="d-grid">
<a href="/Login/Index" class="btn btn-link mt-2">Back to Login</a>
</div>
</div>
</div>
</div>

1
Views/Shared/401.cshtml Normal file
View File

@@ -0,0 +1 @@
<h1>Access Denied</h1>

View File

@@ -1,8 +1,10 @@
<!DOCTYPE html>
@inject IConfiguration Configuration
@using CarCareTracker.Helper
<!DOCTYPE html>
@inject IConfigHelper config
@{
var useDarkMode = bool.Parse(Configuration["UseDarkMode"]);
var enableCsvImports = bool.Parse(Configuration["EnableCsvImports"]);
var userConfig = config.GetUserConfig(User);
var useDarkMode = userConfig.UseDarkMode;
var enableCsvImports = userConfig.EnableCsvImports;
var shortDatePattern = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
shortDatePattern = shortDatePattern.ToLower();
if (!shortDatePattern.Contains("dd"))

View File

@@ -11,6 +11,7 @@
<script src="~/js/reminderrecord.js" asp-append-version="true"></script>
<script src="~/js/upgraderecord.js" asp-append-version="true"></script>
<script src="~/js/note.js" asp-append-version="true"></script>
<script src="~/js/reports.js" asp-append-version="true"></script>
<script src="~/lib/chart-js/chart.umd.js"></script>
}
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
@@ -22,7 +23,10 @@
<button class="nav-link" onclick="editVehicle(@Model.Id)"><span class="display-3 ms-2"><i class="bi bi-pencil-square"></i>Edit Vehicle</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link active" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>Service Records</span></button>
<button class="nav-link active" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>Dashboard</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><span class="display-3 ms-2"><i class="bi bi-card-checklist me-2"></i>Service Records</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</span></button>
@@ -43,9 +47,6 @@
<button class="nav-link" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</span></button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-file-bar-graph me-2"></i>Reports</span></button>
</li>
<li class="nav-item dropdown" role="presentation">
<button onclick="deleteVehicle(@Model.Id)" class="dropdown-item"><span class="display-3 ms-2"><i class="bi bi-trash me-2"></i>Delete Vehicle</span></button>
</li>
</ul>
@@ -64,7 +65,10 @@
<hr />
<ul class="nav nav-tabs lubelogger-tab" id="vehicleTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>Service Records</button>
<button class="nav-link active" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Dash</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="servicerecord-tab" data-bs-toggle="tab" data-bs-target="#servicerecord-tab-pane" type="button" role="tab" aria-selected="true"><i class="bi bi-card-checklist me-2"></i>Service Records</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="accident-tab" data-bs-toggle="tab" data-bs-target="#accident-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-exclamation-octagon me-2"></i>Repairs</button>
@@ -84,9 +88,6 @@
<li class="nav-item" role="presentation">
<button class="nav-link" id="reminder-tab" data-bs-toggle="tab" data-bs-target="#reminder-tab-pane" type="button" role="tab" aria-selected="false"><div class="reminderBellDiv" style="display:inline-flex;"><i class="reminderBell bi bi-bell me-2"></i></div>Reminders</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="report-tab" data-bs-toggle="tab" data-bs-target="#report-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-file-bar-graph me-2"></i>Reports</button>
</li>
<li class="nav-item dropdown ms-auto" role="presentation">
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Manage Vehicle</a>
<ul class="dropdown-menu">
@@ -95,13 +96,13 @@
</li>
</ul>
<div class="tab-content" id="vehicleTabContent">
<div class="tab-pane fade show active" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="tax-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="notes-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="accident-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="reminder-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade show active" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
<div class="tab-pane fade" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
</div>
</div>

View File

@@ -0,0 +1,72 @@
@model List<UserCollaborator>
<div class="row">
<div class="col-8">
<span class="lead">Collaborators</span>
</div>
<div class="col-4">
<button onclick="addCollaborator()" class="btn btn-link btn-sm"><i class="bi bi-person-add"></i></button>
</div>
</div>
<div class="row">
<table class="table table-hover">
<thead>
<tr class="d-flex">
<th scope="col" class="col-8">Username</th>
<th scope="col" class="col-4">Delete</th>
</tr>
</thead>
<tbody>
@foreach (UserCollaborator user in Model)
{
<tr class="d-flex">
<td class="col-8">@user.UserName</td>
<td class="col-4">
@if(User.Identity.Name != user.UserName)
{
<button onclick="deleteCollaborator(@user.UserVehicle.UserId, @user.UserVehicle.VehicleId)" class="btn btn-outline-danger btn-sm"><i class="bi bi-trash"></i></button>
}
</td>
</tr>
}
</tbody>
</table>
</div>
<script>
function deleteCollaborator(userId, vehicleId) {
$.post('/Vehicle/DeleteCollaboratorFromVehicle', {userId: userId, vehicleId: vehicleId}, function(data){
if (data) {
refreshCollaborators();
} else {
errorToast("An error occurred, please try again later");
}
})
}
function addCollaborator() {
Swal.fire({
title: 'Add Collaborator',
html: `
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username">
`,
confirmButtonText: 'Add',
focusConfirm: false,
preConfirm: () => {
const userName = $("#inputUserName").val();
if (!userName) {
Swal.showValidationMessage(`Please enter a username`);
}
return { userName }
},
}).then(function (result) {
if (result.isConfirmed) {
var vehicleId = GetVehicleId().vehicleId;
$.post('/Vehicle/AddCollaboratorsToVehicle', { username: result.value.userName, vehicleId: vehicleId }, function (data) {
if (data.success) {
refreshCollaborators();
} else {
errorToast(data.message)
}
});
}
});
}
</script>

View File

@@ -1,7 +1,8 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var hideZero = config.GetUserConfig(User).HideZero;
}
@model List<CollisionRecord>
<div class="row">

View File

@@ -1,10 +1,11 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@model GasRecordViewModelContainer
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var useMPG = config.GetUserConfig(User).UseMPG;
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
var hideZero = config.GetUserConfig(User).HideZero;
var useKwh = Model.UseKwh;
string consumptionUnit;
string fuelEconomyUnit;

View File

@@ -1,8 +1,9 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@model GasRecordInputContainer
@{
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var useMPG = config.GetUserConfig(User).UseMPG;
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
var useKwh = Model.UseKwh;
var isNew = Model.GasRecord.Id == 0;
string consumptionUnit;

View File

@@ -0,0 +1,58 @@
@model List<CostForVehicleByMonth>
@if (Model.Any())
{
<canvas id="bar-chart-mpg"></canvas>
<script>
renderChart();
function renderChart() {
var barGraphLabels = [];
var barGraphData = [];
var useDarkMode = getGlobalConfig().useDarkMode;
@foreach (CostForVehicleByMonth gasCost in Model)
{
@:barGraphLabels.push("@gasCost.MonthName");
@:barGraphData.push(@gasCost.Cost);
}
new Chart($("#bar-chart-mpg"), {
type: 'bar',
data: {
labels: barGraphLabels,
datasets: [
{
label: "Fuel Mileage by Month",
backgroundColor: ["#00876c", "#43956e", "#67a371", "#89b177", "#a9be80", "#c8cb8b", "#e6d79b", "#e4c281", "#e3ab6b", "#e2925b", "#e07952", "#db5d4f"],
data: barGraphData
}
]
},
options: {
plugins: {
legend: {
labels: {
color: useDarkMode ? "#fff" : "#000"
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: useDarkMode ? "#fff" : "#000"
}
},
x: {
ticks: {
color: useDarkMode ? "#fff" : "#000"
}
}
}
}
});
}
</script>
} else
{
<div class="text-center">
<h4>No data found, insert/select some data to see visualizations here.</h4>
</div>
}

View File

@@ -69,80 +69,18 @@
</div>
<hr />
<div class="row hideOnPrint">
<div class="d-flex justify-content-center">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-pencil-square me-2"></i>Generate and Print Vehicle Maintenance Report</button>
<div class="col-md-3 col-12 chartContainer" id="collaboratorContent">
@await Html.PartialAsync("_Collaborators", Model.Collaborators)
</div>
<div class="col-md-6 col-12 chartContainer">
<div class="d-flex justify-content-center align-items-center col-12 chartContainer" id="monthFuelMileageReportContent">
@await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth)
</div>
</div>
<div class="col-md-3 col-12 chartContainer">
<div class="d-flex justify-content-center">
<button onclick="generateVehicleHistoryReport()" class="btn btn-secondary btn-md mt-1 mb-1">Vehicle Maintenance Report<i class="bi ms-2 bi-box-arrow-in-up-right"></i></button>
</div>
</div>
</div>
<div id="vehicleHistoryReport" class="showOnPrint"></div>
<script>
function getYear() {
return $("#yearOption").val();
}
function generateVehicleHistoryReport(){
var vehicleId = GetVehicleId().vehicleId;
$.get(`/Vehicle/GetVehicleHistory?vehicleId=${vehicleId}`, function(data){
if (data) {
$("#vehicleHistoryReport").html(data);
setTimeout(function () {
window.print();
}, 500);
}
})
}
var debounce = null;
function updateCheck(sender) {
clearTimeout(debounce);
debounce = setTimeout(function () {
refreshBarChart();
}, 1000);
}
function refreshBarChart(callBack) {
var selectedMetrics = [];
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
if ($("#serviceExpenseCheck").is(":checked")) {
selectedMetrics.push('ServiceRecord');
}
if ($("#repairExpenseCheck").is(":checked")) {
selectedMetrics.push('RepairRecord');
}
if ($("#upgradeExpenseCheck").is(":checked")) {
selectedMetrics.push('UpgradeRecord');
}
if ($("#gasExpenseCheck").is(":checked")) {
selectedMetrics.push('GasRecord');
}
if ($("#taxExpenseCheck").is(":checked")) {
selectedMetrics.push('TaxRecord');
}
$.post('/Vehicle/GetCostByMonthByVehicle',
{
vehicleId: vehicleId,
selectedMetrics: selectedMetrics,
year: year
}, function (data) {
$("#gasCostByMonthReportContent").html(data);
if (callBack != undefined) {
callBack();
}
});
}
function updateReminderPie() {
var vehicleId = GetVehicleId().vehicleId;
var daysToAdd = $("#reminderOption").val();
$.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) {
$("#reminderMakeUpReportContent").html(data);
});
}
//called when year selected is changed.
function yearUpdated() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.get(`/Vehicle/GetCostMakeUpForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) {
$("#costMakeUpReportContent").html(data);
refreshBarChart();
})
}
</script>
<div id="vehicleHistoryReport" class="showOnPrint"></div>

View File

@@ -1,7 +1,8 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var hideZero = config.GetUserConfig(User).HideZero;
}
@model List<ServiceRecord>
<div class="row">

View File

@@ -1,7 +1,8 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var hideZero = config.GetUserConfig(User).HideZero;
}
@model List<TaxRecord>
<div class="row">

View File

@@ -1,7 +1,8 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var enableCsvImports = bool.Parse(Configuration[nameof(UserConfig.EnableCsvImports)]);
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var enableCsvImports = config.GetUserConfig(User).EnableCsvImports;
var hideZero = config.GetUserConfig(User).HideZero;
}
@model List<UpgradeRecord>
<div class="row">

View File

@@ -1,8 +1,9 @@
@inject IConfiguration Configuration
@using CarCareTracker.Helper
@inject IConfigHelper config
@{
var hideZero = bool.Parse(Configuration[nameof(UserConfig.HideZero)]);
var useMPG = bool.Parse(Configuration[nameof(UserConfig.UseMPG)]);
var useUKMPG = bool.Parse(Configuration[nameof(UserConfig.UseUKMPG)]);
var hideZero = config.GetUserConfig(User).HideZero;
var useMPG = config.GetUserConfig(User).UseMPG;
var useUKMPG = config.GetUserConfig(User).UseUKMPG;
var useKwh = Model.VehicleData.IsElectric;
string fuelEconomyUnit;
if (useKwh)

View File

@@ -14,6 +14,7 @@ function hideAddVehicleModal() {
function loadGarage() {
$.get('/Home/Garage', function (data) {
$("#garageContainer").html(data);
loadSettings();
});
}
function loadSettings() {

View File

@@ -10,6 +10,45 @@
}
})
}
function performRegistration() {
var token = $("#inputToken").val();
var userName = $("#inputUserName").val();
var userPassword = $("#inputUserPassword").val();
var userEmail = $("#inputEmail").val();
$.post('/Login/Register', { userName: userName, password: userPassword, token: token, emailAddress: userEmail }, function (data) {
if (data.success) {
successToast(data.message);
setTimeout(function () { window.location.href = '/Login/Index' }, 500);
} else {
errorToast(data.message);
}
});
}
function requestPasswordReset() {
var userName = $("#inputUserName").val();
$.post('/Login/RequestResetPassword', { userName: userName }, function (data) {
if (data.success) {
successToast(data.message);
setTimeout(function () { window.location.href = '/Login/Index' }, 500);
} else {
errorToast(data.message);
}
})
}
function performPasswordReset() {
var token = $("#inputToken").val();
var userPassword = $("#inputUserPassword").val();
var userEmail = $("#inputEmail").val();
$.post('/Login/PerformPasswordReset', { password: userPassword, token: token, emailAddress: userEmail }, function (data) {
if (data.success) {
successToast(data.message);
setTimeout(function () { window.location.href = '/Login/Index' }, 500);
} else {
errorToast(data.message);
}
});
}
function handlePasswordKeyPress(event) {
if (event.keyCode == 13) {
performLogin();

81
wwwroot/js/reports.js Normal file
View File

@@ -0,0 +1,81 @@
function getYear() {
return $("#yearOption").val();
}
function generateVehicleHistoryReport() {
var vehicleId = GetVehicleId().vehicleId;
$.get(`/Vehicle/GetVehicleHistory?vehicleId=${vehicleId}`, function (data) {
if (data) {
$("#vehicleHistoryReport").html(data);
setTimeout(function () {
window.print();
}, 500);
}
})
}
var debounce = null;
function updateCheck(sender) {
clearTimeout(debounce);
debounce = setTimeout(function () {
refreshBarChart();
}, 1000);
}
function refreshMPGChart() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.post('/Vehicle/GetMonthMPGByVehicle', {vehicleId: vehicleId, year: year}, function (data) {
$("#monthFuelMileageReportContent").html(data);
})
}
function refreshBarChart(callBack) {
var selectedMetrics = [];
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
if ($("#serviceExpenseCheck").is(":checked")) {
selectedMetrics.push('ServiceRecord');
}
if ($("#repairExpenseCheck").is(":checked")) {
selectedMetrics.push('RepairRecord');
}
if ($("#upgradeExpenseCheck").is(":checked")) {
selectedMetrics.push('UpgradeRecord');
}
if ($("#gasExpenseCheck").is(":checked")) {
selectedMetrics.push('GasRecord');
}
if ($("#taxExpenseCheck").is(":checked")) {
selectedMetrics.push('TaxRecord');
}
$.post('/Vehicle/GetCostByMonthByVehicle',
{
vehicleId: vehicleId,
selectedMetrics: selectedMetrics,
year: year
}, function (data) {
$("#gasCostByMonthReportContent").html(data);
refreshMPGChart();
});
}
function updateReminderPie() {
var vehicleId = GetVehicleId().vehicleId;
var daysToAdd = $("#reminderOption").val();
$.get(`/Vehicle/GetReminderMakeUpByVehicle?vehicleId=${vehicleId}`, { daysToAdd: daysToAdd }, function (data) {
$("#reminderMakeUpReportContent").html(data);
});
}
//called when year selected is changed.
function yearUpdated() {
var vehicleId = GetVehicleId().vehicleId;
var year = getYear();
$.get(`/Vehicle/GetCostMakeUpForVehicle?vehicleId=${vehicleId}`, { year: year }, function (data) {
$("#costMakeUpReportContent").html(data);
refreshBarChart();
})
}
function refreshCollaborators() {
var vehicleId = GetVehicleId().vehicleId;
$.get(`/Vehicle/GetCollaboratorsForVehicle?vehicleId=${vehicleId}`, function (data) {
$("#collaboratorContent").html(data);
});
}

View File

@@ -58,7 +58,7 @@ $(document).ready(function () {
break;
}
});
getVehicleServiceRecords(vehicleId);
getVehicleReport(vehicleId);
});
function getVehicleNotes(vehicleId) {