diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 0062f44..73e2f34 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -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) diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs new file mode 100644 index 0000000..ff10876 --- /dev/null +++ b/Controllers/AdminController.cs @@ -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); + } + } +} diff --git a/Controllers/Error.cs b/Controllers/Error.cs new file mode 100644 index 0000000..5f72cb0 --- /dev/null +++ b/Controllers/Error.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace CarCareTracker.Controllers +{ + public class ErrorController : Controller + { + public IActionResult Unauthorized() + { + return View("401"); + } + } +} diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 01f750a..981177d 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -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 _logger; private readonly IVehicleDataAccess _dataAccess; - private readonly IFileHelper _fileHelper; - private readonly IConfiguration _config; + private readonly IUserLogic _userLogic; + private readonly IConfigHelper _config; - public HomeController(ILogger logger, IVehicleDataAccess dataAccess, IFileHelper fileHelper, IConfiguration configuration) + public HomeController(ILogger 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(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() { diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs index fc9733e..1358dd3 100644 --- a/Controllers/LoginController.cs +++ b/Controllers/LoginController.cs @@ -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 _logger; public LoginController( ILogger 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(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(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(); - } } } diff --git a/Controllers/VehicleController.cs b/Controllers/VehicleController.cs index 62098e9..fb773c0 100644 --- a/Controllers/VehicleController.cs +++ b/Controllers/VehicleController.cs @@ -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 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 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(); @@ -815,6 +906,7 @@ namespace CarCareTracker.Controllers List 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) { diff --git a/External/Implementations/TokenRecordDataAccess.cs b/External/Implementations/TokenRecordDataAccess.cs new file mode 100644 index 0000000..961f583 --- /dev/null +++ b/External/Implementations/TokenRecordDataAccess.cs @@ -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 GetTokens() + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + return table.FindAll().ToList(); + }; + } + public Token GetTokenRecordByBody(string tokenBody) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(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(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(tableName); + table.Insert(token); + return true; + }; + } + public bool DeleteToken(int tokenId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Delete(tokenId); + return true; + }; + } + } +} \ No newline at end of file diff --git a/External/Implementations/UserAccessDataAcces.cs b/External/Implementations/UserAccessDataAcces.cs new file mode 100644 index 0000000..d8b7fea --- /dev/null +++ b/External/Implementations/UserAccessDataAcces.cs @@ -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"; + /// + /// Gets a list of vehicles user have access to. + /// + /// + /// + public List GetUserAccessByUserId(int userId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(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(tableName); + return table.Find(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId).FirstOrDefault(); + }; + } + public List GetUserAccessByVehicleId(int vehicleId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(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(tableName); + table.Upsert(userAccess); + return true; + }; + } + public bool DeleteUserAccess(int userId, int vehicleId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.DeleteMany(x => x.Id.UserId == userId && x.Id.VehicleId == vehicleId); + return true; + }; + } + /// + /// Delete all access records when a vehicle is deleted. + /// + /// + /// + public bool DeleteAllAccessRecordsByVehicleId(int vehicleId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.DeleteMany(x=>x.Id.VehicleId == vehicleId); + return true; + }; + } + /// + /// Delee all access records when a user is deleted. + /// + /// + /// + public bool DeleteAllAccessRecordsByUserId(int userId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.DeleteMany(x => x.Id.UserId == userId); + return true; + }; + } + } +} \ No newline at end of file diff --git a/External/Implementations/UserConfigDataAccess.cs b/External/Implementations/UserConfigDataAccess.cs new file mode 100644 index 0000000..7d63103 --- /dev/null +++ b/External/Implementations/UserConfigDataAccess.cs @@ -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(tableName); + return table.FindById(userId); + }; + } + public bool SaveUserConfig(UserConfigData userConfigData) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Upsert(userConfigData); + return true; + }; + } + public bool DeleteUserConfig(int userId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Delete(userId); + return true; + }; + } + } +} diff --git a/External/Implementations/UserRecordDataAccess.cs b/External/Implementations/UserRecordDataAccess.cs new file mode 100644 index 0000000..7e13f46 --- /dev/null +++ b/External/Implementations/UserRecordDataAccess.cs @@ -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 GetUsers() + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + return table.FindAll().ToList(); + }; + } + public UserData GetUserRecordByUserName(string userName) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(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(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(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(tableName); + table.Upsert(userRecord); + return true; + }; + } + public bool DeleteUserRecord(int userId) + { + using (var db = new LiteDatabase(dbName)) + { + var table = db.GetCollection(tableName); + table.Delete(userId); + return true; + }; + } + } +} \ No newline at end of file diff --git a/External/Implementations/VehicleDataAccess.cs b/External/Implementations/VehicleDataAccess.cs index e2cd45a..db8afab 100644 --- a/External/Implementations/VehicleDataAccess.cs +++ b/External/Implementations/VehicleDataAccess.cs @@ -14,7 +14,7 @@ namespace CarCareTracker.External.Implementations using (var db = new LiteDatabase(dbName)) { var table = db.GetCollection(tableName); - table.Upsert(vehicle); + var result = table.Upsert(vehicle); return true; }; } diff --git a/External/Interfaces/ITokenRecordDataAccess.cs b/External/Interfaces/ITokenRecordDataAccess.cs new file mode 100644 index 0000000..d0dcca2 --- /dev/null +++ b/External/Interfaces/ITokenRecordDataAccess.cs @@ -0,0 +1,13 @@ +using CarCareTracker.Models; + +namespace CarCareTracker.External.Interfaces +{ + public interface ITokenRecordDataAccess + { + public List GetTokens(); + public Token GetTokenRecordByBody(string tokenBody); + public Token GetTokenRecordByEmailAddress(string emailAddress); + public bool CreateNewToken(Token token); + public bool DeleteToken(int tokenId); + } +} diff --git a/External/Interfaces/IUserAccessDataAccess.cs b/External/Interfaces/IUserAccessDataAccess.cs new file mode 100644 index 0000000..ee4d941 --- /dev/null +++ b/External/Interfaces/IUserAccessDataAccess.cs @@ -0,0 +1,15 @@ +using CarCareTracker.Models; + +namespace CarCareTracker.External.Interfaces +{ + public interface IUserAccessDataAccess + { + List GetUserAccessByUserId(int userId); + UserAccess GetUserAccessByVehicleAndUserId(int userId, int vehicleId); + List GetUserAccessByVehicleId(int vehicleId); + bool SaveUserAccess(UserAccess userAccess); + bool DeleteUserAccess(int userId, int vehicleId); + bool DeleteAllAccessRecordsByVehicleId(int vehicleId); + bool DeleteAllAccessRecordsByUserId(int userId); + } +} diff --git a/External/Interfaces/IUserConfigDataAccess.cs b/External/Interfaces/IUserConfigDataAccess.cs new file mode 100644 index 0000000..d4bf1eb --- /dev/null +++ b/External/Interfaces/IUserConfigDataAccess.cs @@ -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); + } +} diff --git a/External/Interfaces/IUserRecordDataAccess.cs b/External/Interfaces/IUserRecordDataAccess.cs new file mode 100644 index 0000000..971072d --- /dev/null +++ b/External/Interfaces/IUserRecordDataAccess.cs @@ -0,0 +1,14 @@ +using CarCareTracker.Models; + +namespace CarCareTracker.External.Interfaces +{ + public interface IUserRecordDataAccess + { + public List 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); + } +} \ No newline at end of file diff --git a/Filter/CollaboratorFilter.cs b/Filter/CollaboratorFilter.cs new file mode 100644 index 0000000..550c458 --- /dev/null +++ b/Filter/CollaboratorFilter.cs @@ -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"); + } + } + } + } +} diff --git a/Helper/ConfigHelper.cs b/Helper/ConfigHelper.cs new file mode 100644 index 0000000..830e6c6 --- /dev/null +++ b/Helper/ConfigHelper.cs @@ -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(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_{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_{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_{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; + } + } + }); + } + } +} diff --git a/Helper/GasHelper.cs b/Helper/GasHelper.cs index 23d78b9..9172552 100644 --- a/Helper/GasHelper.cs +++ b/Helper/GasHelper.cs @@ -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, diff --git a/Helper/LoginHelper.cs b/Helper/LoginHelper.cs deleted file mode 100644 index a43b7c6..0000000 --- a/Helper/LoginHelper.cs +++ /dev/null @@ -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(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(); - } - } -} diff --git a/Helper/MailHelper.cs b/Helper/MailHelper.cs new file mode 100644 index 0000000..6915f1d --- /dev/null +++ b/Helper/MailHelper.cs @@ -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(); + } + 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; + } + } + } +} diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs index 2780549..04633cc 100644 --- a/Helper/StaticHelper.cs +++ b/Helper/StaticHelper.cs @@ -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) { diff --git a/Logic/LoginLogic.cs b/Logic/LoginLogic.cs new file mode 100644 index 0000000..8dda856 --- /dev/null +++ b/Logic/LoginLogic.cs @@ -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 GetAllUsers(); + List 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." }; + } + } + /// + /// Generates a token and notifies user via email so they can reset their password. + /// + /// + /// + 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 }; + } + } + /// + /// Returns an empty user if can't auth against neither root nor db user. + /// + /// credentials from login page + /// + 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 GetAllUsers() + { + var result = _userData.GetUsers(); + return result; + } + public List 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(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(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(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); + } + } +} diff --git a/Logic/UserLogic.cs b/Logic/UserLogic.cs new file mode 100644 index 0000000..bd80c08 --- /dev/null +++ b/Logic/UserLogic.cs @@ -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 GetCollaboratorsForVehicle(int vehicleId); + bool AddUserAccessToVehicle(int userId, int vehicleId); + bool DeleteCollaboratorFromVehicle(int userId, int vehicleId); + OperationResponse AddCollaboratorToVehicle(int vehicleId, string username); + List FilterUserVehicles(List 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 GetCollaboratorsForVehicle(int vehicleId) + { + var result = _userAccess.GetUserAccessByVehicleId(vehicleId); + var convertedResult = new List(); + //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 FilterUserVehicles(List 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(); + } + } + 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; + } + } +} diff --git a/MapProfile/FuellyMappers.cs b/MapProfile/FuellyMappers.cs index b90d948..8f3ed5e 100644 --- a/MapProfile/FuellyMappers.cs +++ b/MapProfile/FuellyMappers.cs @@ -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"]); diff --git a/Middleware/Authen.cs b/Middleware/Authen.cs index fb10b03..5cfdd89 100644 --- a/Middleware/Authen.cs +++ b/Middleware/Authen.cs @@ -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 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 HandleAuthenticateAsync() @@ -39,7 +39,9 @@ namespace CarCareTracker.Middleware var appIdentity = new ClaimsIdentity("Custom"); var userIdentity = new List { - 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 { - 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(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(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 + //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 + { + 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"); } diff --git a/Models/Admin/AdminViewModel.cs b/Models/Admin/AdminViewModel.cs new file mode 100644 index 0000000..2aeba42 --- /dev/null +++ b/Models/Admin/AdminViewModel.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class AdminViewModel + { + public List Users { get; set; } + public List Tokens { get; set; } + } +} diff --git a/Models/Configuration/MailConfig.cs b/Models/Configuration/MailConfig.cs new file mode 100644 index 0000000..23243e8 --- /dev/null +++ b/Models/Configuration/MailConfig.cs @@ -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; } + } +} diff --git a/Models/GasRecord/GasRecordViewModel.cs b/Models/GasRecord/GasRecordViewModel.cs index 0131d7d..e790af0 100644 --- a/Models/GasRecord/GasRecordViewModel.cs +++ b/Models/GasRecord/GasRecordViewModel.cs @@ -4,6 +4,7 @@ { public int Id { get; set; } public int VehicleId { get; set; } + public int MonthId { get; set; } public string Date { get; set; } /// /// American moment diff --git a/Models/Login/AuthCookie.cs b/Models/Login/AuthCookie.cs index 0a3356a..24b4d96 100644 --- a/Models/Login/AuthCookie.cs +++ b/Models/Login/AuthCookie.cs @@ -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; } } } diff --git a/Models/Login/LoginModel.cs b/Models/Login/LoginModel.cs index 8afa0fb..167787e 100644 --- a/Models/Login/LoginModel.cs +++ b/Models/Login/LoginModel.cs @@ -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; } } diff --git a/Models/Login/Token.cs b/Models/Login/Token.cs new file mode 100644 index 0000000..78a7e19 --- /dev/null +++ b/Models/Login/Token.cs @@ -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; } + } +} diff --git a/Models/OperationResponse.cs b/Models/OperationResponse.cs new file mode 100644 index 0000000..a5be6c7 --- /dev/null +++ b/Models/OperationResponse.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class OperationResponse + { + public bool Success { get; set; } + public string Message { get; set; } + } +} diff --git a/Models/Report/ReportViewModel.cs b/Models/Report/ReportViewModel.cs index 77d4be2..2c2e6a4 100644 --- a/Models/Report/ReportViewModel.cs +++ b/Models/Report/ReportViewModel.cs @@ -3,8 +3,10 @@ public class ReportViewModel { public List CostForVehicleByMonth { get; set; } = new List(); + public List FuelMileageForVehicleByMonth { get; set; } = new List(); public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle(); public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle(); public List Years { get; set; } = new List(); + public List Collaborators { get; set; } = new List(); } } diff --git a/Models/User/UserAccess.cs b/Models/User/UserAccess.cs new file mode 100644 index 0000000..039da7c --- /dev/null +++ b/Models/User/UserAccess.cs @@ -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; } + } +} diff --git a/Models/User/UserCollaborator.cs b/Models/User/UserCollaborator.cs new file mode 100644 index 0000000..0d050d0 --- /dev/null +++ b/Models/User/UserCollaborator.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class UserCollaborator + { + public string UserName { get; set; } + public UserVehicle UserVehicle { get; set; } + } +} diff --git a/Models/User/UserConfigData.cs b/Models/User/UserConfigData.cs new file mode 100644 index 0000000..6aa68c6 --- /dev/null +++ b/Models/User/UserConfigData.cs @@ -0,0 +1,11 @@ +namespace CarCareTracker.Models +{ + public class UserConfigData + { + /// + /// User ID + /// + public int Id { get; set; } + public UserConfig UserConfig { get; set; } + } +} diff --git a/Models/User/UserData.cs b/Models/User/UserData.cs new file mode 100644 index 0000000..6fb77a1 --- /dev/null +++ b/Models/User/UserData.cs @@ -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; + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index e729b1c..157f3c2 100644 --- a/Program.cs +++ b/Program.cs @@ -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(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); //configure helpers builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +//configure logic +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); if (!Directory.Exists("data")) { diff --git a/Views/Admin/Index.cshtml b/Views/Admin/Index.cshtml new file mode 100644 index 0000000..f101972 --- /dev/null +++ b/Views/Admin/Index.cshtml @@ -0,0 +1,153 @@ +@{ + ViewData["Title"] = "Admin"; +} +@inject IConfiguration config; +@{ + bool emailServerIsSetup = true; + var mailConfig = config.GetSection("MailConfig").Get(); + if (mailConfig is null || string.IsNullOrWhiteSpace(mailConfig.EmailServer)) + { + emailServerIsSetup = false; + } +} +@model AdminViewModel +
+
+
+ +
+
+ Admin Panel +
+
+
+
+
+ Tokens +
+
+
+ +
+
+
+ + +
+
+
+ + + + + + + + + + @foreach (Token token in Model.Tokens) + { + + + + + + } + +
TokenIssued ToDelete
@token.Body@token.EmailAddress + +
+
+
+ Users +
+ + + + + + + + + + + @foreach (UserData userData in Model.Users) + { + + + + + + + } + +
UsernameEmailIs AdminDelete
@userData.UserName@userData.EmailAddress
+
+
+
+ \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 2dd47ce..1e875db 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -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 @@ - @if (enableAuth) + @if (User.IsInRole("CookieAuth")) { + @if (User.IsInRole(nameof(UserData.IsAdmin))) + { + + } @@ -42,10 +49,21 @@ - @if (enableAuth) + @if (User.IsInRole("CookieAuth")) { - } @@ -69,6 +87,5 @@ \ No newline at end of file diff --git a/Views/Home/_Settings.cshtml b/Views/Home/_Settings.cshtml index 7266531..76b3070 100644 --- a/Views/Home/_Settings.cshtml +++ b/Views/Home/_Settings.cshtml @@ -32,10 +32,13 @@ -
- - -
+ @if (User.IsInRole(nameof(UserData.IsRootUser))) + { +
+ + +
+ }
@@ -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."); } diff --git a/Views/Login/ForgotPassword.cshtml b/Views/Login/ForgotPassword.cshtml new file mode 100644 index 0000000..f75bb3a --- /dev/null +++ b/Views/Login/ForgotPassword.cshtml @@ -0,0 +1,26 @@ +@{ + ViewData["Title"] = "LubeLogger - Login"; +} +@section Scripts { + +} +
+
+
+ +
+ + +
+
+ +
+ + +
+
+
\ No newline at end of file diff --git a/Views/Login/Index.cshtml b/Views/Login/Index.cshtml index e7bc17e..0885abc 100644 --- a/Views/Login/Index.cshtml +++ b/Views/Login/Index.cshtml @@ -23,6 +23,12 @@
+ +
+ Register +
\ No newline at end of file diff --git a/Views/Login/Registration.cshtml b/Views/Login/Registration.cshtml new file mode 100644 index 0000000..131272f --- /dev/null +++ b/Views/Login/Registration.cshtml @@ -0,0 +1,35 @@ +@{ + ViewData["Title"] = "LubeLogger - Register"; +} +@section Scripts { + +} +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/Views/Login/ResetPassword.cshtml b/Views/Login/ResetPassword.cshtml new file mode 100644 index 0000000..f15af72 --- /dev/null +++ b/Views/Login/ResetPassword.cshtml @@ -0,0 +1,31 @@ +@{ + ViewData["Title"] = "LubeLogger - Register"; +} +@section Scripts { + +} +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/Views/Shared/401.cshtml b/Views/Shared/401.cshtml new file mode 100644 index 0000000..6a9e903 --- /dev/null +++ b/Views/Shared/401.cshtml @@ -0,0 +1 @@ +

Access Denied

\ No newline at end of file diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 17fd6c5..5694d37 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,8 +1,10 @@ - -@inject IConfiguration Configuration +@using CarCareTracker.Helper + +@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")) diff --git a/Views/Vehicle/Index.cshtml b/Views/Vehicle/Index.cshtml index 4aafe19..ef89385 100644 --- a/Views/Vehicle/Index.cshtml +++ b/Views/Vehicle/Index.cshtml @@ -11,6 +11,7 @@ + }
@@ -22,7 +23,10 @@ + - @@ -64,7 +65,10 @@
diff --git a/Views/Vehicle/_Collaborators.cshtml b/Views/Vehicle/_Collaborators.cshtml new file mode 100644 index 0000000..20fc53e --- /dev/null +++ b/Views/Vehicle/_Collaborators.cshtml @@ -0,0 +1,72 @@ +@model List +
+
+ Collaborators +
+
+ +
+
+
+ + + + + + + + + @foreach (UserCollaborator user in Model) + { + + + + + } + +
UsernameDelete
@user.UserName + @if(User.Identity.Name != user.UserName) + { + + } +
+
+ \ No newline at end of file diff --git a/Views/Vehicle/_CollisionRecords.cshtml b/Views/Vehicle/_CollisionRecords.cshtml index 9237b23..b84ef61 100644 --- a/Views/Vehicle/_CollisionRecords.cshtml +++ b/Views/Vehicle/_CollisionRecords.cshtml @@ -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
diff --git a/Views/Vehicle/_Gas.cshtml b/Views/Vehicle/_Gas.cshtml index ed7591b..233d29a 100644 --- a/Views/Vehicle/_Gas.cshtml +++ b/Views/Vehicle/_Gas.cshtml @@ -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; diff --git a/Views/Vehicle/_GasModal.cshtml b/Views/Vehicle/_GasModal.cshtml index bfbac9a..fc86707 100644 --- a/Views/Vehicle/_GasModal.cshtml +++ b/Views/Vehicle/_GasModal.cshtml @@ -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; diff --git a/Views/Vehicle/_MPGByMonthReport.cshtml b/Views/Vehicle/_MPGByMonthReport.cshtml new file mode 100644 index 0000000..b079022 --- /dev/null +++ b/Views/Vehicle/_MPGByMonthReport.cshtml @@ -0,0 +1,58 @@ +@model List +@if (Model.Any()) +{ + + +} else +{ +
+

No data found, insert/select some data to see visualizations here.

+
+} \ No newline at end of file diff --git a/Views/Vehicle/_Report.cshtml b/Views/Vehicle/_Report.cshtml index a97f42b..1b36458 100644 --- a/Views/Vehicle/_Report.cshtml +++ b/Views/Vehicle/_Report.cshtml @@ -69,80 +69,18 @@

-
- +
+ @await Html.PartialAsync("_Collaborators", Model.Collaborators) +
+
+
+ @await Html.PartialAsync("_MPGByMonthReport", Model.FuelMileageForVehicleByMonth) +
+
+
+
+ +
-
- \ No newline at end of file +
\ No newline at end of file diff --git a/Views/Vehicle/_ServiceRecords.cshtml b/Views/Vehicle/_ServiceRecords.cshtml index 72a9d05..c657262 100644 --- a/Views/Vehicle/_ServiceRecords.cshtml +++ b/Views/Vehicle/_ServiceRecords.cshtml @@ -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
diff --git a/Views/Vehicle/_TaxRecords.cshtml b/Views/Vehicle/_TaxRecords.cshtml index b7facd8..296a678 100644 --- a/Views/Vehicle/_TaxRecords.cshtml +++ b/Views/Vehicle/_TaxRecords.cshtml @@ -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
diff --git a/Views/Vehicle/_UpgradeRecords.cshtml b/Views/Vehicle/_UpgradeRecords.cshtml index 94de026..2bc9b2c 100644 --- a/Views/Vehicle/_UpgradeRecords.cshtml +++ b/Views/Vehicle/_UpgradeRecords.cshtml @@ -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
diff --git a/Views/Vehicle/_VehicleHistory.cshtml b/Views/Vehicle/_VehicleHistory.cshtml index 0b2c036..4c4b0ac 100644 --- a/Views/Vehicle/_VehicleHistory.cshtml +++ b/Views/Vehicle/_VehicleHistory.cshtml @@ -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) diff --git a/wwwroot/js/garage.js b/wwwroot/js/garage.js index f436ae7..a63f96b 100644 --- a/wwwroot/js/garage.js +++ b/wwwroot/js/garage.js @@ -14,6 +14,7 @@ function hideAddVehicleModal() { function loadGarage() { $.get('/Home/Garage', function (data) { $("#garageContainer").html(data); + loadSettings(); }); } function loadSettings() { diff --git a/wwwroot/js/login.js b/wwwroot/js/login.js index cd06122..f88daf9 100644 --- a/wwwroot/js/login.js +++ b/wwwroot/js/login.js @@ -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(); diff --git a/wwwroot/js/reports.js b/wwwroot/js/reports.js new file mode 100644 index 0000000..2a2a336 --- /dev/null +++ b/wwwroot/js/reports.js @@ -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); + }); +} \ No newline at end of file diff --git a/wwwroot/js/vehicle.js b/wwwroot/js/vehicle.js index 788b23b..a104a78 100644 --- a/wwwroot/js/vehicle.js +++ b/wwwroot/js/vehicle.js @@ -58,7 +58,7 @@ $(document).ready(function () { break; } }); - getVehicleServiceRecords(vehicleId); + getVehicleReport(vehicleId); }); function getVehicleNotes(vehicleId) {