Compare commits
124 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d79804872 | ||
|
|
3598bb6adb | ||
|
|
50b18a1a71 | ||
|
|
0ab585d4cc | ||
|
|
d32b9b879d | ||
|
|
594ad38454 | ||
|
|
ae8a885b4d | ||
|
|
975bbadaae | ||
|
|
42711bc92a | ||
|
|
f4538ffabd | ||
|
|
d60c3d3ea6 | ||
|
|
3cc32e48f4 | ||
|
|
0d4b7d8ee1 | ||
|
|
d8249c7163 | ||
|
|
60edb65b55 | ||
|
|
bc2bb3636b | ||
|
|
58c49c1240 | ||
|
|
2eb9a58aa6 | ||
|
|
b05ded67a9 | ||
|
|
76c9473704 | ||
|
|
3909223d2f | ||
|
|
ab720272da | ||
|
|
b702a663f4 | ||
|
|
1d8baaf423 | ||
|
|
0869c99090 | ||
|
|
48b2bab5d6 | ||
|
|
0bfa036603 | ||
|
|
a02bcf94d7 | ||
|
|
9e0b45deac | ||
|
|
1b04faad9c | ||
|
|
17f5cf6a1d | ||
|
|
bf7cffdf8f | ||
|
|
a3ba527765 | ||
|
|
a085b2d87d | ||
|
|
06f1ce5884 | ||
|
|
c77f9bce51 | ||
|
|
cf11c203ce | ||
|
|
4550d33b92 | ||
|
|
503127eb03 | ||
|
|
e1a5a871ae | ||
|
|
0714ec6432 | ||
|
|
36339a04e1 | ||
|
|
8af9868d2f | ||
|
|
99d5372f25 | ||
|
|
bd6defe205 | ||
|
|
f5276031c0 | ||
|
|
50ebdf547a | ||
|
|
ba248758cb | ||
|
|
4bde8181b7 | ||
|
|
d0d733b7d2 | ||
|
|
e377f6c8d0 | ||
|
|
2fafe1101c | ||
|
|
56a315524a | ||
|
|
244a164891 | ||
|
|
2ec0c1d465 | ||
|
|
239ca73a64 | ||
|
|
b03460d6bb | ||
|
|
d2686949c5 | ||
|
|
971242b015 | ||
|
|
bd3b821226 | ||
|
|
6ffa856795 | ||
|
|
6895d2060d | ||
|
|
d413a06b87 | ||
|
|
263000f0ae | ||
|
|
578f6ab62a | ||
|
|
469b625989 | ||
|
|
95a630b600 | ||
|
|
5f576a2792 | ||
|
|
f5af92da93 | ||
|
|
4ccfcd6be7 | ||
|
|
917d093bcf | ||
|
|
c916771815 | ||
|
|
d4517ac986 | ||
|
|
040cc6adf9 | ||
|
|
ec740089fe | ||
|
|
e29e14bc3e | ||
|
|
592baa4c6e | ||
|
|
5e5c9c96b4 | ||
|
|
487f64e459 | ||
|
|
d67aeaa333 | ||
|
|
fb5cdfce29 | ||
|
|
6e5cfde2cf | ||
|
|
7122f4ac0d | ||
|
|
2fb24b8b65 | ||
|
|
6540d96d4d | ||
|
|
62e196b97d | ||
|
|
4975861710 | ||
|
|
8d74799099 | ||
|
|
c58b4552b2 | ||
|
|
8c6920afab | ||
|
|
b77fd2c1c7 | ||
|
|
2035d6f6e2 | ||
|
|
e58454ef5d | ||
|
|
915eb1722d | ||
|
|
4f706d3e93 | ||
|
|
d80f0dcb8f | ||
|
|
2ae334d06d | ||
|
|
4388df71f3 | ||
|
|
c972f9c8a2 | ||
|
|
90fa6ad5fc | ||
|
|
a1b2b40abe | ||
|
|
00fd499805 | ||
|
|
8f3f71772b | ||
|
|
08104eef2a | ||
|
|
8d989ee81c | ||
|
|
2247b1b1db | ||
|
|
249ad938f6 | ||
|
|
c9d60910e5 | ||
|
|
03b89786ec | ||
|
|
8815009b04 | ||
|
|
bb4a8f7f83 | ||
|
|
c7730d1775 | ||
|
|
b7a3ef0fa7 | ||
|
|
d525e2195c | ||
|
|
cb73be0e43 | ||
|
|
a06bdbff88 | ||
|
|
0de6ab7547 | ||
|
|
5b54b8113e | ||
|
|
4d804803a8 | ||
|
|
2434245c84 | ||
|
|
d00e6e252d | ||
|
|
e006c158fc | ||
|
|
75d610200b | ||
|
|
f696030ac2 |
6
.env
6
.env
@@ -1,2 +1,8 @@
|
||||
LC_ALL=en_US.UTF-8
|
||||
LANG=en_US.UTF-8
|
||||
MailConfig__EmailServer=""
|
||||
MailConfig__EmailFrom=""
|
||||
MailConfig__UseSSL="false"
|
||||
MailConfig__Port=587
|
||||
MailConfig__Username=""
|
||||
MailConfig__Password=""
|
||||
30
.github/workflows/dockerhub-docker-image.yml
vendored
Normal file
30
.github/workflows/dockerhub-docker-image.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Docker Image To Docker Hub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: "${{ secrets.DH_USER }}"
|
||||
password: "${{ secrets.DH_PASS }}"
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
hargata/lubelogger:latest
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Docker Image CI
|
||||
name: Docker Image To GHCR
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
ghcr.io/hargata/lubelogger:latest
|
||||
@@ -1,8 +1,12 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.External.Implementations;
|
||||
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
|
||||
{
|
||||
@@ -17,8 +21,11 @@ namespace CarCareTracker.Controllers
|
||||
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
|
||||
private readonly IReminderHelper _reminderHelper;
|
||||
private readonly IGasHelper _gasHelper;
|
||||
private readonly IUserLogic _userLogic;
|
||||
private readonly IFileHelper _fileHelper;
|
||||
public APIController(IVehicleDataAccess dataAccess,
|
||||
IGasHelper gasHelper,
|
||||
IReminderHelper reminderHelper,
|
||||
@@ -28,7 +35,10 @@ namespace CarCareTracker.Controllers
|
||||
ICollisionRecordDataAccess collisionRecordDataAccess,
|
||||
ITaxRecordDataAccess taxRecordDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess)
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
||||
IOdometerRecordDataAccess odometerRecordDataAccess,
|
||||
IFileHelper fileHelper,
|
||||
IUserLogic userLogic)
|
||||
{
|
||||
_dataAccess = dataAccess;
|
||||
_noteDataAccess = noteDataAccess;
|
||||
@@ -38,21 +48,32 @@ namespace CarCareTracker.Controllers
|
||||
_taxRecordDataAccess = taxRecordDataAccess;
|
||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||
_odometerRecordDataAccess = odometerRecordDataAccess;
|
||||
_gasHelper = gasHelper;
|
||||
_reminderHelper = reminderHelper;
|
||||
_userLogic = userLogic;
|
||||
_fileHelper = fileHelper;
|
||||
}
|
||||
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 +82,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 +91,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 +100,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,14 +108,67 @@ namespace CarCareTracker.Controllers
|
||||
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/odometerrecords")]
|
||||
public IActionResult OdometerRecords(int vehicleId)
|
||||
{
|
||||
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
var result = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Odometer = x.Mileage.ToString(), Notes = x.Notes });
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
[Route("/api/vehicle/odometerrecords/add")]
|
||||
public IActionResult AddOdometerRecord(int vehicleId, OdometerRecordExportModel input)
|
||||
{
|
||||
var response = new OperationResponse();
|
||||
if (vehicleId == default)
|
||||
{
|
||||
response.Success = false;
|
||||
response.Message = "Must provide a valid vehicle id";
|
||||
return Json(response);
|
||||
}
|
||||
try
|
||||
{
|
||||
var odometerRecord = new OdometerRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(input.Date),
|
||||
Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes,
|
||||
Mileage = int.Parse(input.Odometer)
|
||||
};
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord);
|
||||
response.Success = true;
|
||||
response.Message = "Odometer Record Added";
|
||||
return Json(response);
|
||||
} catch (Exception ex)
|
||||
{
|
||||
response.Success = false;
|
||||
response.Message = StaticHelper.GenericErrorMessage;
|
||||
return Json(response);
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/gasrecords")]
|
||||
public IActionResult GasRecords(int vehicleId, bool useMPG, bool useUKMPG)
|
||||
{
|
||||
var vehicleRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
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()});
|
||||
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(),
|
||||
IsFillToFull = x.IsFillToFull.ToString(),
|
||||
MissedFuelUp = x.MissedFuelUp.ToString(),
|
||||
Notes = x.Notes
|
||||
});
|
||||
return Json(result);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
[Route("/api/vehicle/reminders")]
|
||||
public IActionResult Reminders(int vehicleId)
|
||||
@@ -101,6 +178,14 @@ namespace CarCareTracker.Controllers
|
||||
var results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, DateTime.Now).Select(x=> new ReminderExportModel { Description = x.Description, Urgency = x.Urgency.ToString(), Metric = x.Metric.ToString(), Notes = x.Notes});
|
||||
return Json(results);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
[Route("/api/makebackup")]
|
||||
public IActionResult MakeBackup()
|
||||
{
|
||||
var result = _fileHelper.MakeBackup();
|
||||
return Json(result);
|
||||
}
|
||||
private int GetMaxMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
@@ -124,6 +209,11 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
if (odometerRecords.Any())
|
||||
{
|
||||
numbersArray.Add(odometerRecords.Max(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
56
Controllers/AdminController.cs
Normal file
56
Controllers/AdminController.cs
Normal 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
12
Controllers/Error.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace CarCareTracker.Controllers
|
||||
{
|
||||
public class ErrorController : Controller
|
||||
{
|
||||
public IActionResult Unauthorized()
|
||||
{
|
||||
return View("401");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,12 +46,25 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public ActionResult DeleteFiles(string fileLocation)
|
||||
public IActionResult DeleteFiles(string fileLocation)
|
||||
{
|
||||
var result = _fileHelper.DeleteFile(fileLocation);
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpGet]
|
||||
public IActionResult MakeBackup()
|
||||
{
|
||||
var result = _fileHelper.MakeBackup();
|
||||
return Json(result);
|
||||
}
|
||||
[Authorize(Roles = nameof(UserData.IsRootUser))]
|
||||
[HttpPost]
|
||||
public IActionResult RestoreBackup(string fileName)
|
||||
{
|
||||
var result = _fileHelper.RestoreBackup(fileName);
|
||||
return Json(result);
|
||||
}
|
||||
private string UploadFile(IFormFile fileToUpload)
|
||||
{
|
||||
string uploadDirectory = "temp/";
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
if (result)
|
||||
{
|
||||
Response.Cookies.Delete("ACCESS_TOKEN");
|
||||
return Json(true);
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
@@ -22,13 +25,16 @@ namespace CarCareTracker.Controllers
|
||||
private readonly ITaxRecordDataAccess _taxRecordDataAccess;
|
||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||
private readonly IUpgradeRecordDataAccess _upgradeRecordDataAccess;
|
||||
private readonly ISupplyRecordDataAccess _supplyRecordDataAccess;
|
||||
private readonly IPlanRecordDataAccess _planRecordDataAccess;
|
||||
private readonly IOdometerRecordDataAccess _odometerRecordDataAccess;
|
||||
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 +49,12 @@ namespace CarCareTracker.Controllers
|
||||
ITaxRecordDataAccess taxRecordDataAccess,
|
||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||
IUpgradeRecordDataAccess upgradeRecordDataAccess,
|
||||
ISupplyRecordDataAccess supplyRecordDataAccess,
|
||||
IPlanRecordDataAccess planRecordDataAccess,
|
||||
IOdometerRecordDataAccess odometerRecordDataAccess,
|
||||
IUserLogic userLogic,
|
||||
IWebHostEnvironment webEnv,
|
||||
IConfiguration config)
|
||||
IConfigHelper config)
|
||||
{
|
||||
_logger = logger;
|
||||
_dataAccess = dataAccess;
|
||||
@@ -59,10 +69,18 @@ namespace CarCareTracker.Controllers
|
||||
_taxRecordDataAccess = taxRecordDataAccess;
|
||||
_reminderRecordDataAccess = reminderRecordDataAccess;
|
||||
_upgradeRecordDataAccess = upgradeRecordDataAccess;
|
||||
_supplyRecordDataAccess = supplyRecordDataAccess;
|
||||
_planRecordDataAccess = planRecordDataAccess;
|
||||
_odometerRecordDataAccess = odometerRecordDataAccess;
|
||||
_userLogic = userLogic;
|
||||
_webEnv = webEnv;
|
||||
_config = config;
|
||||
_useDescending = bool.Parse(config[nameof(UserConfig.UseDescending)]);
|
||||
}
|
||||
private int GetUserID()
|
||||
{
|
||||
return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier));
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult Index(int vehicleId)
|
||||
{
|
||||
@@ -74,6 +92,7 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
return PartialView("_VehicleModal", new Vehicle());
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetEditVehiclePartialViewById(int vehicleId)
|
||||
{
|
||||
@@ -85,10 +104,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 +128,7 @@ namespace CarCareTracker.Controllers
|
||||
return Json(false);
|
||||
}
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult DeleteVehicle(int vehicleId)
|
||||
{
|
||||
@@ -108,15 +140,19 @@ namespace CarCareTracker.Controllers
|
||||
_noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) &&
|
||||
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
|
||||
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
|
||||
_planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) &&
|
||||
_supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) &&
|
||||
_userLogic.DeleteAllAccessToVehicle(vehicleId) &&
|
||||
_dataAccess.DeleteVehicle(vehicleId);
|
||||
return Json(result);
|
||||
}
|
||||
#region "Bulk Imports"
|
||||
#region "Bulk Imports and Exports"
|
||||
[HttpGet]
|
||||
public IActionResult GetBulkImportModalPartialView(ImportMode mode)
|
||||
{
|
||||
return PartialView("_BulkDataImporter", mode);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult ExportFromVehicleToCsv(int vehicleId, ImportMode mode)
|
||||
{
|
||||
@@ -182,7 +218,53 @@ namespace CarCareTracker.Controllers
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord) {
|
||||
else if (mode == ImportMode.OdometerRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new OdometerRecordExportModel { Date = x.Date.ToShortDateString(), Notes = x.Notes, Odometer = x.Mileage.ToString() });
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.SupplyRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new SupplyRecordExportModel
|
||||
{
|
||||
Date = x.Date.ToShortDateString(),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
PartNumber = x.PartNumber,
|
||||
PartQuantity = x.Quantity.ToString(),
|
||||
PartSupplier = x.PartSupplier,
|
||||
Notes = x.Notes
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
@@ -199,16 +281,54 @@ namespace CarCareTracker.Controllers
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.PlanRecord)
|
||||
{
|
||||
var fileNameToExport = $"temp/{Guid.NewGuid()}.csv";
|
||||
var fullExportFilePath = _fileHelper.GetFullFilePath(fileNameToExport, false);
|
||||
var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
|
||||
if (vehicleRecords.Any())
|
||||
{
|
||||
var exportData = vehicleRecords.Select(x => new PlanRecordExportModel
|
||||
{
|
||||
DateCreated = x.DateCreated.ToString("G"),
|
||||
DateModified = x.DateModified.ToString("G"),
|
||||
Description = x.Description,
|
||||
Cost = x.Cost.ToString("C"),
|
||||
Type = x.ImportMode.ToString(),
|
||||
Priority = x.Priority.ToString(),
|
||||
Progress = x.Progress.ToString(),
|
||||
Notes = x.Notes
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
{
|
||||
csv.WriteRecords(exportData);
|
||||
}
|
||||
}
|
||||
return Json($"/{fileNameToExport}");
|
||||
}
|
||||
}
|
||||
else if (mode == ImportMode.GasRecord)
|
||||
{
|
||||
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() });
|
||||
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(),
|
||||
IsFillToFull = x.IsFillToFull.ToString(),
|
||||
MissedFuelUp = x.MissedFuelUp.ToString(),
|
||||
Notes = x.Notes
|
||||
});
|
||||
using (var writer = new StreamWriter(fullExportFilePath))
|
||||
{
|
||||
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
|
||||
@@ -220,6 +340,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
return Json(false);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpPost]
|
||||
public IActionResult ImportToVehicleIdFromCsv(int vehicleId, ImportMode mode, string fileName)
|
||||
{
|
||||
@@ -242,7 +363,7 @@ namespace CarCareTracker.Controllers
|
||||
config.PrepareHeaderForMatch = args => { return args.Header.Trim().ToLower(); };
|
||||
using (var csv = new CsvReader(reader, config))
|
||||
{
|
||||
csv.Context.RegisterClassMap<FuellyMapper>();
|
||||
csv.Context.RegisterClassMap<ImportMapper>();
|
||||
var records = csv.GetRecords<ImportModel>().ToList();
|
||||
if (records.Any())
|
||||
{
|
||||
@@ -256,7 +377,8 @@ namespace CarCareTracker.Controllers
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any)
|
||||
Gallons = decimal.Parse(importModel.FuelConsumed, NumberStyles.Any),
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
|
||||
};
|
||||
if (string.IsNullOrWhiteSpace(importModel.Cost) && !string.IsNullOrWhiteSpace(importModel.Price))
|
||||
{
|
||||
@@ -264,7 +386,8 @@ namespace CarCareTracker.Controllers
|
||||
//fuelly sometimes exports CSVs without total cost.
|
||||
var parsedPrice = decimal.Parse(importModel.Price, NumberStyles.Any);
|
||||
convertedRecord.Cost = convertedRecord.Gallons * parsedPrice;
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedRecord.Cost = decimal.Parse(importModel.Cost, NumberStyles.Any);
|
||||
}
|
||||
@@ -272,14 +395,17 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
var parsedBool = importModel.PartialFuelUp.Trim() == "1";
|
||||
convertedRecord.IsFillToFull = !parsedBool;
|
||||
} else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(importModel.IsFillToFull))
|
||||
{
|
||||
var parsedBool = importModel.IsFillToFull.Trim() == "1" || importModel.IsFillToFull.Trim() == "Full";
|
||||
var possibleFillToFullValues = new List<string> { "1", "true", "full" };
|
||||
var parsedBool = possibleFillToFullValues.Contains(importModel.IsFillToFull.Trim().ToLower());
|
||||
convertedRecord.IsFillToFull = parsedBool;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(importModel.MissedFuelUp))
|
||||
{
|
||||
var parsedBool = importModel.MissedFuelUp.Trim() == "1";
|
||||
var possibleMissedFuelUpValues = new List<string> { "1", "true" };
|
||||
var parsedBool = possibleMissedFuelUpValues.Contains(importModel.MissedFuelUp.Trim().ToLower());
|
||||
convertedRecord.MissedFuelUp = parsedBool;
|
||||
}
|
||||
//insert record into db, check to make sure fuelconsumed is not zero so we don't get a divide by zero error.
|
||||
@@ -301,6 +427,36 @@ namespace CarCareTracker.Controllers
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.OdometerRecord)
|
||||
{
|
||||
var convertedRecord = new OdometerRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
Mileage = int.Parse(importModel.Odometer, NumberStyles.Any),
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes
|
||||
};
|
||||
_odometerRecordDataAccess.SaveOdometerRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.PlanRecord)
|
||||
{
|
||||
var progressIsEnum = Enum.TryParse(importModel.Progress, out PlanProgress parsedProgress);
|
||||
var typeIsEnum = Enum.TryParse(importModel.Type, out ImportMode parsedType);
|
||||
var priorityIsEnum = Enum.TryParse(importModel.Priority, out PlanPriority parsedPriority);
|
||||
var convertedRecord = new PlanRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
DateCreated = DateTime.Parse(importModel.DateCreated),
|
||||
DateModified = DateTime.Parse(importModel.DateModified),
|
||||
Progress = parsedProgress,
|
||||
ImportMode = parsedType,
|
||||
Priority = parsedPriority,
|
||||
Description = string.IsNullOrWhiteSpace(importModel.Description) ? $"Plan Record on {importModel.DateCreated}" : importModel.Description,
|
||||
Notes = string.IsNullOrWhiteSpace(importModel.Notes) ? "" : importModel.Notes,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any)
|
||||
};
|
||||
_planRecordDataAccess.SavePlanRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.RepairRecord)
|
||||
{
|
||||
var convertedRecord = new CollisionRecord()
|
||||
@@ -327,6 +483,21 @@ namespace CarCareTracker.Controllers
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.SupplyRecord)
|
||||
{
|
||||
var convertedRecord = new SupplyRecord()
|
||||
{
|
||||
VehicleId = vehicleId,
|
||||
Date = DateTime.Parse(importModel.Date),
|
||||
PartNumber = importModel.PartNumber,
|
||||
PartSupplier = importModel.PartSupplier,
|
||||
Quantity = decimal.Parse(importModel.PartQuantity, NumberStyles.Any),
|
||||
Description = importModel.Description,
|
||||
Cost = decimal.Parse(importModel.Cost, NumberStyles.Any),
|
||||
Notes = importModel.Notes
|
||||
};
|
||||
_supplyRecordDataAccess.SaveSupplyRecordToVehicle(convertedRecord);
|
||||
}
|
||||
else if (mode == ImportMode.TaxRecord)
|
||||
{
|
||||
var convertedRecord = new TaxRecord()
|
||||
@@ -353,6 +524,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Gas Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetGasRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
@@ -360,10 +532,11 @@ 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)]);
|
||||
var userConfig = _config.GetUserConfig(User);
|
||||
bool useMPG = userConfig.UseMPG;
|
||||
bool useUKMPG = userConfig.UseUKMPG;
|
||||
var computedResults = _gasHelper.GetGasRecordViewModels(result, useMPG, useUKMPG);
|
||||
if (_useDescending)
|
||||
if (userConfig.UseDescending)
|
||||
{
|
||||
computedResults = computedResults.OrderByDescending(x => DateTime.Parse(x.Date)).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
@@ -401,7 +574,8 @@ namespace CarCareTracker.Controllers
|
||||
Files = result.Files,
|
||||
Gallons = result.Gallons,
|
||||
IsFillToFull = result.IsFillToFull,
|
||||
MissedFuelUp = result.MissedFuelUp
|
||||
MissedFuelUp = result.MissedFuelUp,
|
||||
Notes = result.Notes
|
||||
};
|
||||
var vehicleIsElectric = _dataAccess.GetVehicleById(convertedResult.VehicleId).IsElectric;
|
||||
var viewModel = new GasRecordInputContainer()
|
||||
@@ -419,10 +593,12 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Service Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetServiceRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
@@ -472,10 +648,12 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Collision Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetCollisionRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
@@ -525,10 +703,12 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Tax Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetTaxRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ToList();
|
||||
@@ -577,6 +757,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Reports"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetReportPartialView(int vehicleId)
|
||||
{
|
||||
@@ -601,12 +782,11 @@ namespace CarCareTracker.Controllers
|
||||
allCosts.AddRange(_reportHelper.GetServiceRecordSum(serviceRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetRepairRecordSum(collisionRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetUpgradeRecordSum(upgradeRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetGasRecordSum(gasRecords, 0));
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, 0));
|
||||
viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => x.MonthName).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
viewModel.CostForVehicleByMonth = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key,
|
||||
MonthName = x.Key.MonthName,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
//get reminders
|
||||
@@ -642,8 +822,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 +885,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 +898,90 @@ namespace CarCareTracker.Controllers
|
||||
};
|
||||
return PartialView("_ReminderMakeUpReport", viewModel);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
public IActionResult GetVehicleHistory(int vehicleId)
|
||||
{
|
||||
var vehicleHistory = new VehicleHistoryViewModel();
|
||||
vehicleHistory.VehicleData = _dataAccess.GetVehicleById(vehicleId);
|
||||
vehicleHistory.Odometer = GetMaxMileage(vehicleId).ToString("N0");
|
||||
List<GenericReportModel> reportData = new List<GenericReportModel>();
|
||||
var serviceRecords = _serviceRecordDataAccess.GetServiceRecordsByVehicleId(vehicleId);
|
||||
var repairRecords = _collisionRecordDataAccess.GetCollisionRecordsByVehicleId(vehicleId);
|
||||
var upgradeRecords = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
var gasRecords = _gasRecordDataAccess.GetGasRecordsByVehicleId(vehicleId);
|
||||
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;
|
||||
var gasViewModels = _gasHelper.GetGasRecordViewModels(gasRecords, useMPG, useUKMPG);
|
||||
if (gasViewModels.Any())
|
||||
{
|
||||
averageMPG = gasViewModels.Average(x => x.MilesPerGallon);
|
||||
}
|
||||
vehicleHistory.MPG = averageMPG;
|
||||
//insert servicerecords
|
||||
reportData.AddRange(serviceRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.ServiceRecord
|
||||
}));
|
||||
//repair records
|
||||
reportData.AddRange(repairRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.RepairRecord
|
||||
}));
|
||||
reportData.AddRange(upgradeRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = x.Mileage,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.UpgradeRecord
|
||||
}));
|
||||
reportData.AddRange(taxRecords.Select(x => new GenericReportModel
|
||||
{
|
||||
Date = x.Date,
|
||||
Odometer = 0,
|
||||
Description = x.Description,
|
||||
Notes = x.Notes,
|
||||
Cost = x.Cost,
|
||||
DataType = ImportMode.TaxRecord
|
||||
}));
|
||||
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)
|
||||
{
|
||||
@@ -711,15 +1011,16 @@ namespace CarCareTracker.Controllers
|
||||
var taxRecords = _taxRecordDataAccess.GetTaxRecordsByVehicleId(vehicleId);
|
||||
allCosts.AddRange(_reportHelper.GetTaxRecordSum(taxRecords, year));
|
||||
}
|
||||
var groupedRecord = allCosts.GroupBy(x => x.MonthName).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
var groupedRecord = allCosts.GroupBy(x => new { x.MonthName, x.MonthId }).OrderBy(x => x.Key.MonthId).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthName = x.Key,
|
||||
MonthName = x.Key.MonthName,
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
}).ToList();
|
||||
return PartialView("_GasCostByMonthReport", groupedRecord);
|
||||
}
|
||||
#endregion
|
||||
#region "Reminders"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
private int GetMaxMileage(int vehicleId)
|
||||
{
|
||||
var numbersArray = new List<int>();
|
||||
@@ -743,6 +1044,11 @@ namespace CarCareTracker.Controllers
|
||||
{
|
||||
numbersArray.Add(upgradeRecords.Max(x => x.Mileage));
|
||||
}
|
||||
var odometerRecords = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
if (odometerRecords.Any())
|
||||
{
|
||||
numbersArray.Add(odometerRecords.Max(x => x.Mileage));
|
||||
}
|
||||
return numbersArray.Any() ? numbersArray.Max() : 0;
|
||||
}
|
||||
private List<ReminderRecordViewModel> GetRemindersAndUrgency(int vehicleId, DateTime dateCompare)
|
||||
@@ -752,16 +1058,48 @@ namespace CarCareTracker.Controllers
|
||||
List<ReminderRecordViewModel> results = _reminderHelper.GetReminderRecordViewModels(reminders, currentMileage, dateCompare);
|
||||
return results;
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetVehicleHaveUrgentOrPastDueReminders(int vehicleId)
|
||||
{
|
||||
var result = GetRemindersAndUrgency(vehicleId, DateTime.Now);
|
||||
if (result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue).Any())
|
||||
//check for past due reminders that are eligible for recurring.
|
||||
var pastDueAndRecurring = result.Where(x => x.Urgency == ReminderUrgency.PastDue && x.IsRecurring);
|
||||
if (pastDueAndRecurring.Any())
|
||||
{
|
||||
foreach (ReminderRecordViewModel reminderRecord in pastDueAndRecurring)
|
||||
{
|
||||
//update based on recurring intervals.
|
||||
//pull reminderRecord based on ID
|
||||
var existingReminder = _reminderRecordDataAccess.GetReminderRecordById(reminderRecord.Id);
|
||||
if (existingReminder.Metric == ReminderMetric.Both)
|
||||
{
|
||||
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
|
||||
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
||||
}
|
||||
else if (existingReminder.Metric == ReminderMetric.Odometer)
|
||||
{
|
||||
existingReminder.Mileage += (int)existingReminder.ReminderMileageInterval;
|
||||
}
|
||||
else if (existingReminder.Metric == ReminderMetric.Date)
|
||||
{
|
||||
existingReminder.Date = existingReminder.Date.AddMonths((int)existingReminder.ReminderMonthInterval);
|
||||
}
|
||||
//save to db.
|
||||
_reminderRecordDataAccess.SaveReminderRecordToVehicle(existingReminder);
|
||||
//set urgency to not urgent so it gets excluded in count.
|
||||
reminderRecord.Urgency = ReminderUrgency.NotUrgent;
|
||||
}
|
||||
}
|
||||
//check for very urgent or past due reminders that were not eligible for recurring.
|
||||
var pastDueAndUrgentReminders = result.Where(x => x.Urgency == ReminderUrgency.VeryUrgent || x.Urgency == ReminderUrgency.PastDue);
|
||||
if (pastDueAndUrgentReminders.Any())
|
||||
{
|
||||
return Json(true);
|
||||
}
|
||||
return Json(false);
|
||||
}
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetReminderRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
@@ -800,7 +1138,10 @@ namespace CarCareTracker.Controllers
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Mileage = result.Mileage,
|
||||
Metric = result.Metric
|
||||
Metric = result.Metric,
|
||||
IsRecurring = result.IsRecurring,
|
||||
ReminderMileageInterval = result.ReminderMileageInterval,
|
||||
ReminderMonthInterval = result.ReminderMonthInterval
|
||||
};
|
||||
return PartialView("_ReminderRecordModal", convertedResult);
|
||||
}
|
||||
@@ -812,10 +1153,12 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Upgrade Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetUpgradeRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _upgradeRecordDataAccess.GetUpgradeRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
@@ -865,6 +1208,7 @@ namespace CarCareTracker.Controllers
|
||||
}
|
||||
#endregion
|
||||
#region "Notes"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetNotesByVehicleId(int vehicleId)
|
||||
{
|
||||
@@ -895,5 +1239,225 @@ namespace CarCareTracker.Controllers
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Supply Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetSupplyRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ToList();
|
||||
}
|
||||
return PartialView("_SupplyRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveSupplyRecordToVehicleId(SupplyRecordInput supplyRecord)
|
||||
{
|
||||
//move files from temp.
|
||||
supplyRecord.Files = supplyRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _supplyRecordDataAccess.SaveSupplyRecordToVehicle(supplyRecord.ToSupplyRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddSupplyRecordPartialView()
|
||||
{
|
||||
return PartialView("_SupplyRecordModal", new SupplyRecordInput());
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetSupplyRecordForEditById(int supplyRecordId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.GetSupplyRecordById(supplyRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new SupplyRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Cost = result.Cost,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Description = result.Description,
|
||||
PartNumber = result.PartNumber,
|
||||
Quantity = result.Quantity,
|
||||
PartSupplier = result.PartSupplier,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
};
|
||||
return PartialView("_SupplyRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteSupplyRecordById(int supplyRecordId)
|
||||
{
|
||||
var result = _supplyRecordDataAccess.DeleteSupplyRecordById(supplyRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Plan Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId);
|
||||
return PartialView("_PlanRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SavePlanRecordToVehicleId(PlanRecordInput planRecord)
|
||||
{
|
||||
//populate createdDate
|
||||
if (planRecord.Id == default)
|
||||
{
|
||||
planRecord.DateCreated = DateTime.Now.ToString("G");
|
||||
}
|
||||
planRecord.DateModified = DateTime.Now.ToString("G");
|
||||
//move files from temp.
|
||||
planRecord.Files = planRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(planRecord.ToPlanRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddPlanRecordPartialView()
|
||||
{
|
||||
return PartialView("_PlanRecordModal", new PlanRecordInput());
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult UpdatePlanRecordProgress(int planRecordId, PlanProgress planProgress, int odometer = 0)
|
||||
{
|
||||
var existingRecord = _planRecordDataAccess.GetPlanRecordById(planRecordId);
|
||||
existingRecord.Progress = planProgress;
|
||||
existingRecord.DateModified = DateTime.Now;
|
||||
var result = _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord);
|
||||
if (planProgress == PlanProgress.Done)
|
||||
{
|
||||
//convert plan record to service/upgrade/repair record.
|
||||
if (existingRecord.ImportMode == ImportMode.ServiceRecord)
|
||||
{
|
||||
var newRecord = new ServiceRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files
|
||||
};
|
||||
_serviceRecordDataAccess.SaveServiceRecordToVehicle(newRecord);
|
||||
}
|
||||
else if (existingRecord.ImportMode == ImportMode.RepairRecord)
|
||||
{
|
||||
var newRecord = new CollisionRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files
|
||||
};
|
||||
_collisionRecordDataAccess.SaveCollisionRecordToVehicle(newRecord);
|
||||
}
|
||||
else if (existingRecord.ImportMode == ImportMode.UpgradeRecord)
|
||||
{
|
||||
var newRecord = new UpgradeRecord()
|
||||
{
|
||||
VehicleId = existingRecord.VehicleId,
|
||||
Date = DateTime.Now,
|
||||
Mileage = odometer,
|
||||
Description = existingRecord.Description,
|
||||
Cost = existingRecord.Cost,
|
||||
Notes = existingRecord.Notes,
|
||||
Files = existingRecord.Files
|
||||
};
|
||||
_upgradeRecordDataAccess.SaveUpgradeRecordToVehicle(newRecord);
|
||||
}
|
||||
}
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetPlanRecordForEditById(int planRecordId)
|
||||
{
|
||||
var result = _planRecordDataAccess.GetPlanRecordById(planRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new PlanRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Description = result.Description,
|
||||
DateCreated = result.DateCreated.ToString("G"),
|
||||
DateModified = result.DateModified.ToString("G"),
|
||||
ImportMode = result.ImportMode,
|
||||
Priority = result.Priority,
|
||||
Progress = result.Progress,
|
||||
Cost = result.Cost,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
};
|
||||
return PartialView("_PlanRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeletePlanRecordById(int planRecordId)
|
||||
{
|
||||
var result = _planRecordDataAccess.DeletePlanRecordById(planRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
#region "Odometer Records"
|
||||
[TypeFilter(typeof(CollaboratorFilter))]
|
||||
[HttpGet]
|
||||
public IActionResult GetOdometerRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.GetOdometerRecordsByVehicleId(vehicleId);
|
||||
bool _useDescending = _config.GetUserConfig(User).UseDescending;
|
||||
if (_useDescending)
|
||||
{
|
||||
result = result.OrderByDescending(x => x.Date).ThenByDescending(x => x.Mileage).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
result = result.OrderBy(x => x.Date).ThenBy(x => x.Mileage).ToList();
|
||||
}
|
||||
return PartialView("_OdometerRecords", result);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult SaveOdometerRecordToVehicleId(OdometerRecordInput odometerRecord)
|
||||
{
|
||||
//move files from temp.
|
||||
odometerRecord.Files = odometerRecord.Files.Select(x => { return new UploadedFiles { Name = x.Name, Location = _fileHelper.MoveFileFromTemp(x.Location, "documents/") }; }).ToList();
|
||||
var result = _odometerRecordDataAccess.SaveOdometerRecordToVehicle(odometerRecord.ToOdometerRecord());
|
||||
return Json(result);
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetAddOdometerRecordPartialView()
|
||||
{
|
||||
return PartialView("_OdometerRecordModal", new OdometerRecordInput());
|
||||
}
|
||||
[HttpGet]
|
||||
public IActionResult GetOdometerRecordForEditById(int odometerRecordId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.GetOdometerRecordById(odometerRecordId);
|
||||
//convert to Input object.
|
||||
var convertedResult = new OdometerRecordInput
|
||||
{
|
||||
Id = result.Id,
|
||||
Date = result.Date.ToShortDateString(),
|
||||
Mileage = result.Mileage,
|
||||
Notes = result.Notes,
|
||||
VehicleId = result.VehicleId,
|
||||
Files = result.Files
|
||||
};
|
||||
return PartialView("_OdometerRecordModal", convertedResult);
|
||||
}
|
||||
[HttpPost]
|
||||
public IActionResult DeleteOdometerRecordById(int odometerRecordId)
|
||||
{
|
||||
var result = _odometerRecordDataAccess.DeleteOdometerRecordById(odometerRecordId);
|
||||
return Json(result);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
|
||||
WORKDIR /App
|
||||
|
||||
COPY . ./
|
||||
RUN dotnet restore
|
||||
RUN dotnet publish -c Release -o out
|
||||
ARG TARGETARCH
|
||||
RUN dotnet restore -a $TARGETARCH
|
||||
RUN dotnet publish -a $TARGETARCH -c Release -o out
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
WORKDIR /App
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
RepairRecord = 1,
|
||||
GasRecord = 2,
|
||||
TaxRecord = 3,
|
||||
UpgradeRecord = 4
|
||||
UpgradeRecord = 4,
|
||||
ReminderRecord = 5,
|
||||
NoteRecord = 6,
|
||||
SupplyRecord = 7,
|
||||
Dashboard = 8,
|
||||
PlanRecord = 9,
|
||||
OdometerRecord = 10
|
||||
}
|
||||
}
|
||||
|
||||
9
Enum/PlanPriority.cs
Normal file
9
Enum/PlanPriority.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum PlanPriority
|
||||
{
|
||||
Critical = 0,
|
||||
Normal = 1,
|
||||
Low = 2
|
||||
}
|
||||
}
|
||||
10
Enum/PlanProgress.cs
Normal file
10
Enum/PlanProgress.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum PlanProgress
|
||||
{
|
||||
Backlog = 0,
|
||||
InProgress = 1,
|
||||
Testing = 2,
|
||||
Done = 3
|
||||
}
|
||||
}
|
||||
13
Enum/ReminderMileageInterval.cs
Normal file
13
Enum/ReminderMileageInterval.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ReminderMileageInterval
|
||||
{
|
||||
FiveHundredMiles = 500,
|
||||
OneThousandMiles = 1000,
|
||||
ThreeThousandMiles = 3000,
|
||||
FiveThousandMiles = 5000,
|
||||
SevenThousandFiveHundredMiles = 7500,
|
||||
TenThousandMiles = 10000,
|
||||
FiftyThousandMiles = 50000
|
||||
}
|
||||
}
|
||||
10
Enum/ReminderMonthInterval.cs
Normal file
10
Enum/ReminderMonthInterval.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public enum ReminderMonthInterval
|
||||
{
|
||||
ThreeMonths = 3,
|
||||
SixMonths = 6,
|
||||
OneYear = 12,
|
||||
FiveYears = 60
|
||||
}
|
||||
}
|
||||
57
External/Implementations/OdometerRecordDataAccess.cs
vendored
Normal file
57
External/Implementations/OdometerRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace CarCareTracker.External.Implementations
|
||||
{
|
||||
public class OdometerRecordDataAccess : IOdometerRecordDataAccess
|
||||
{
|
||||
private static string dbName = StaticHelper.DbName;
|
||||
private static string tableName = "odometerrecords";
|
||||
public List<OdometerRecord> GetOdometerRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<OdometerRecord>(tableName);
|
||||
var odometerRecords = table.Find(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId));
|
||||
return odometerRecords.ToList() ?? new List<OdometerRecord>();
|
||||
};
|
||||
}
|
||||
public OdometerRecord GetOdometerRecordById(int odometerRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<OdometerRecord>(tableName);
|
||||
return table.FindById(odometerRecordId);
|
||||
};
|
||||
}
|
||||
public bool DeleteOdometerRecordById(int odometerRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<OdometerRecord>(tableName);
|
||||
table.Delete(odometerRecordId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<OdometerRecord>(tableName);
|
||||
table.Upsert(odometerRecord);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<OdometerRecord>(tableName);
|
||||
var odometerRecords = table.DeleteMany(Query.EQ(nameof(OdometerRecord.VehicleId), vehicleId));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
57
External/Implementations/PlanRecordDataAccess.cs
vendored
Normal file
57
External/Implementations/PlanRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace CarCareTracker.External.Implementations
|
||||
{
|
||||
public class PlanRecordDataAccess : IPlanRecordDataAccess
|
||||
{
|
||||
private static string dbName = StaticHelper.DbName;
|
||||
private static string tableName = "planrecords";
|
||||
public List<PlanRecord> GetPlanRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<PlanRecord>(tableName);
|
||||
var planRecords = table.Find(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId));
|
||||
return planRecords.ToList() ?? new List<PlanRecord>();
|
||||
};
|
||||
}
|
||||
public PlanRecord GetPlanRecordById(int planRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<PlanRecord>(tableName);
|
||||
return table.FindById(planRecordId);
|
||||
};
|
||||
}
|
||||
public bool DeletePlanRecordById(int planRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<PlanRecord>(tableName);
|
||||
table.Delete(planRecordId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool SavePlanRecordToVehicle(PlanRecord planRecord)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<PlanRecord>(tableName);
|
||||
table.Upsert(planRecord);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteAllPlanRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<PlanRecord>(tableName);
|
||||
var planRecords = table.DeleteMany(Query.EQ(nameof(PlanRecord.VehicleId), vehicleId));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
57
External/Implementations/SupplyRecordDataAccess.cs
vendored
Normal file
57
External/Implementations/SupplyRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace CarCareTracker.External.Implementations
|
||||
{
|
||||
public class SupplyRecordDataAccess : ISupplyRecordDataAccess
|
||||
{
|
||||
private static string dbName = StaticHelper.DbName;
|
||||
private static string tableName = "supplyrecords";
|
||||
public List<SupplyRecord> GetSupplyRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<SupplyRecord>(tableName);
|
||||
var supplyRecords = table.Find(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId));
|
||||
return supplyRecords.ToList() ?? new List<SupplyRecord>();
|
||||
};
|
||||
}
|
||||
public SupplyRecord GetSupplyRecordById(int supplyRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<SupplyRecord>(tableName);
|
||||
return table.FindById(supplyRecordId);
|
||||
};
|
||||
}
|
||||
public bool DeleteSupplyRecordById(int supplyRecordId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<SupplyRecord>(tableName);
|
||||
table.Delete(supplyRecordId);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<SupplyRecord>(tableName);
|
||||
table.Upsert(supplyRecord);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId)
|
||||
{
|
||||
using (var db = new LiteDatabase(dbName))
|
||||
{
|
||||
var table = db.GetCollection<SupplyRecord>(tableName);
|
||||
var supplyRecords = table.DeleteMany(Query.EQ(nameof(SupplyRecord.VehicleId), vehicleId));
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
57
External/Implementations/TokenRecordDataAccess.cs
vendored
Normal file
57
External/Implementations/TokenRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
using CarCareTracker.External.Interfaces;
|
||||
using CarCareTracker.Helper;
|
||||
using CarCareTracker.Models;
|
||||
using LiteDB;
|
||||
|
||||
namespace CarCareTracker.External.Implementations
|
||||
{
|
||||
public class 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
88
External/Implementations/UserAccessDataAcces.cs
vendored
Normal file
88
External/Implementations/UserAccessDataAcces.cs
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
40
External/Implementations/UserConfigDataAccess.cs
vendored
Normal file
40
External/Implementations/UserConfigDataAccess.cs
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
66
External/Implementations/UserRecordDataAccess.cs
vendored
Normal file
66
External/Implementations/UserRecordDataAccess.cs
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
13
External/Interfaces/IOdometerRecordDataAccess.cs
vendored
Normal file
13
External/Interfaces/IOdometerRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.External.Interfaces
|
||||
{
|
||||
public interface IOdometerRecordDataAccess
|
||||
{
|
||||
public List<OdometerRecord> GetOdometerRecordsByVehicleId(int vehicleId);
|
||||
public OdometerRecord GetOdometerRecordById(int odometerRecordId);
|
||||
public bool DeleteOdometerRecordById(int odometerRecordId);
|
||||
public bool SaveOdometerRecordToVehicle(OdometerRecord odometerRecord);
|
||||
public bool DeleteAllOdometerRecordsByVehicleId(int vehicleId);
|
||||
}
|
||||
}
|
||||
13
External/Interfaces/IPlanRecordDataAccess.cs
vendored
Normal file
13
External/Interfaces/IPlanRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.External.Interfaces
|
||||
{
|
||||
public interface IPlanRecordDataAccess
|
||||
{
|
||||
public List<PlanRecord> GetPlanRecordsByVehicleId(int vehicleId);
|
||||
public PlanRecord GetPlanRecordById(int planRecordId);
|
||||
public bool DeletePlanRecordById(int planRecordId);
|
||||
public bool SavePlanRecordToVehicle(PlanRecord planRecord);
|
||||
public bool DeleteAllPlanRecordsByVehicleId(int vehicleId);
|
||||
}
|
||||
}
|
||||
13
External/Interfaces/ISupplyRecordDataAccess.cs
vendored
Normal file
13
External/Interfaces/ISupplyRecordDataAccess.cs
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.External.Interfaces
|
||||
{
|
||||
public interface ISupplyRecordDataAccess
|
||||
{
|
||||
public List<SupplyRecord> GetSupplyRecordsByVehicleId(int vehicleId);
|
||||
public SupplyRecord GetSupplyRecordById(int supplyRecordId);
|
||||
public bool DeleteSupplyRecordById(int supplyRecordId);
|
||||
public bool SaveSupplyRecordToVehicle(SupplyRecord supplyRecord);
|
||||
public bool DeleteAllSupplyRecordsByVehicleId(int vehicleId);
|
||||
}
|
||||
}
|
||||
13
External/Interfaces/ITokenRecordDataAccess.cs
vendored
Normal file
13
External/Interfaces/ITokenRecordDataAccess.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
15
External/Interfaces/IUserAccessDataAccess.cs
vendored
Normal file
15
External/Interfaces/IUserAccessDataAccess.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
11
External/Interfaces/IUserConfigDataAccess.cs
vendored
Normal file
11
External/Interfaces/IUserConfigDataAccess.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
14
External/Interfaces/IUserRecordDataAccess.cs
vendored
Normal file
14
External/Interfaces/IUserRecordDataAccess.cs
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
28
Filter/CollaboratorFilter.cs
Normal file
28
Filter/CollaboratorFilter.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
140
Helper/ConfigHelper.cs
Normal file
140
Helper/ConfigHelper.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
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)]),
|
||||
UseThreeDecimalGasCost = bool.Parse(_config[nameof(UserConfig.UseThreeDecimalGasCost)]),
|
||||
VisibleTabs = _config.GetSection("VisibleTabs").Get<List<ImportMode>>(),
|
||||
DefaultTab = (ImportMode)int.Parse(_config[nameof(UserConfig.DefaultTab)])
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
namespace CarCareTracker.Helper
|
||||
using System.IO.Compression;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
public interface IFileHelper
|
||||
{
|
||||
string GetFullFilePath(string currentFilePath, bool mustExist = true);
|
||||
string MoveFileFromTemp(string currentFilePath, string newFolder);
|
||||
bool DeleteFile(string currentFilePath);
|
||||
string MakeBackup();
|
||||
bool RestoreBackup(string fileName);
|
||||
}
|
||||
public class FileHelper : IFileHelper
|
||||
{
|
||||
private readonly IWebHostEnvironment _webEnv;
|
||||
public FileHelper(IWebHostEnvironment webEnv)
|
||||
private readonly ILogger<IFileHelper> _logger;
|
||||
public FileHelper(IWebHostEnvironment webEnv, ILogger<IFileHelper> logger)
|
||||
{
|
||||
_webEnv = webEnv;
|
||||
_logger = logger;
|
||||
}
|
||||
public string GetFullFilePath(string currentFilePath, bool mustExist = true)
|
||||
{
|
||||
@@ -23,7 +29,8 @@
|
||||
if (File.Exists(oldFilePath))
|
||||
{
|
||||
return oldFilePath;
|
||||
} else if (!mustExist)
|
||||
}
|
||||
else if (!mustExist)
|
||||
{
|
||||
return oldFilePath;
|
||||
}
|
||||
@@ -31,6 +38,122 @@
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
public bool RestoreBackup(string fileName)
|
||||
{
|
||||
var fullFilePath = GetFullFilePath(fileName);
|
||||
if (string.IsNullOrWhiteSpace(fullFilePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
try
|
||||
{
|
||||
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{Guid.NewGuid()}");
|
||||
if (!Directory.Exists(tempPath))
|
||||
Directory.CreateDirectory(tempPath);
|
||||
//extract zip file
|
||||
ZipFile.ExtractToDirectory(fullFilePath, tempPath);
|
||||
//copy over images and documents.
|
||||
var imagePath = Path.Combine(tempPath, "images");
|
||||
var documentPath = Path.Combine(tempPath, "documents");
|
||||
var dataPath = Path.Combine(tempPath, StaticHelper.DbName);
|
||||
var configPath = Path.Combine(tempPath, StaticHelper.UserConfigPath);
|
||||
if (Directory.Exists(imagePath))
|
||||
{
|
||||
var existingPath = Path.Combine(_webEnv.WebRootPath, "images");
|
||||
if (!Directory.Exists(existingPath))
|
||||
{
|
||||
Directory.CreateDirectory(existingPath);
|
||||
}
|
||||
//copy each files from temp folder to newPath
|
||||
var filesToUpload = Directory.GetFiles(imagePath);
|
||||
foreach(string file in filesToUpload)
|
||||
{
|
||||
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
|
||||
}
|
||||
}
|
||||
if (Directory.Exists(documentPath))
|
||||
{
|
||||
var existingPath = Path.Combine(_webEnv.WebRootPath, "documents");
|
||||
if (!Directory.Exists(existingPath))
|
||||
{
|
||||
Directory.CreateDirectory(existingPath);
|
||||
}
|
||||
//copy each files from temp folder to newPath
|
||||
var filesToUpload = Directory.GetFiles(documentPath);
|
||||
foreach (string file in filesToUpload)
|
||||
{
|
||||
File.Copy(file, $"{existingPath}/{Path.GetFileName(file)}", true);
|
||||
}
|
||||
}
|
||||
if (File.Exists(dataPath))
|
||||
{
|
||||
//data path will always exist as it is created on startup if not.
|
||||
File.Move(dataPath, StaticHelper.DbName, true);
|
||||
}
|
||||
if (File.Exists(configPath))
|
||||
{
|
||||
//check if config folder exists.
|
||||
if (!Directory.Exists("config/"))
|
||||
{
|
||||
Directory.CreateDirectory("config/");
|
||||
}
|
||||
File.Move(configPath, StaticHelper.UserConfigPath, true);
|
||||
}
|
||||
return true;
|
||||
} catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error Restoring Database Backup: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public string MakeBackup()
|
||||
{
|
||||
var folderName = $"db_backup_{DateTime.Now.ToString("yyyy-MM-dd-HH-mm-ss")}";
|
||||
var tempPath = Path.Combine(_webEnv.WebRootPath, $"temp/{folderName}");
|
||||
var imagePath = Path.Combine(_webEnv.WebRootPath, "images");
|
||||
var documentPath = Path.Combine(_webEnv.WebRootPath, "documents");
|
||||
var dataPath = StaticHelper.DbName;
|
||||
var configPath = StaticHelper.UserConfigPath;
|
||||
if (!Directory.Exists(tempPath))
|
||||
Directory.CreateDirectory(tempPath);
|
||||
if (Directory.Exists(imagePath))
|
||||
{
|
||||
var files = Directory.GetFiles(imagePath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var newPath = Path.Combine(tempPath, "images");
|
||||
Directory.CreateDirectory(newPath);
|
||||
File.Copy(file, $"{newPath}/{Path.GetFileName(file)}");
|
||||
}
|
||||
}
|
||||
if (Directory.Exists(documentPath))
|
||||
{
|
||||
var files = Directory.GetFiles(documentPath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
var newPath = Path.Combine(tempPath, "documents");
|
||||
Directory.CreateDirectory(newPath);
|
||||
File.Copy(file, $"{newPath}/{Path.GetFileName(file)}");
|
||||
}
|
||||
}
|
||||
if (File.Exists(dataPath))
|
||||
{
|
||||
var newPath = Path.Combine(tempPath, "data");
|
||||
Directory.CreateDirectory(newPath);
|
||||
File.Copy(dataPath, $"{newPath}/{Path.GetFileName(dataPath)}");
|
||||
}
|
||||
if (File.Exists(configPath))
|
||||
{
|
||||
var newPath = Path.Combine(tempPath, "config");
|
||||
Directory.CreateDirectory(newPath);
|
||||
File.Copy(configPath, $"{newPath}/{Path.GetFileName(configPath)}");
|
||||
}
|
||||
var destFilePath = $"{tempPath}.zip";
|
||||
ZipFile.CreateFromDirectory(tempPath, destFilePath);
|
||||
//delete temp directory
|
||||
Directory.Delete(tempPath, true);
|
||||
return $"/temp/{folderName}.zip";
|
||||
}
|
||||
public string MoveFileFromTemp(string currentFilePath, string newFolder)
|
||||
{
|
||||
string tempPath = "temp/";
|
||||
@@ -38,7 +161,8 @@
|
||||
{
|
||||
return currentFilePath;
|
||||
}
|
||||
if (currentFilePath.StartsWith("/")) {
|
||||
if (currentFilePath.StartsWith("/"))
|
||||
{
|
||||
currentFilePath = currentFilePath.Substring(1);
|
||||
}
|
||||
string uploadPath = Path.Combine(_webEnv.WebRootPath, newFolder);
|
||||
@@ -67,7 +191,8 @@
|
||||
if (!File.Exists(filePath)) //verify file no longer exists.
|
||||
{
|
||||
return true;
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,12 +36,16 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
VehicleId = currentObject.VehicleId,
|
||||
MonthId = currentObject.Date.Month,
|
||||
Date = currentObject.Date.ToShortDateString(),
|
||||
Mileage = currentObject.Mileage,
|
||||
Gallons = convertedConsumption,
|
||||
Cost = currentObject.Cost,
|
||||
DeltaMileage = deltaMileage,
|
||||
CostPerGallon = currentObject.Cost / convertedConsumption
|
||||
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
|
||||
IsFillToFull = currentObject.IsFillToFull,
|
||||
MissedFuelUp = currentObject.MissedFuelUp,
|
||||
Notes = currentObject.Notes
|
||||
};
|
||||
if (currentObject.MissedFuelUp)
|
||||
{
|
||||
@@ -54,7 +58,10 @@ namespace CarCareTracker.Helper
|
||||
else if (currentObject.IsFillToFull)
|
||||
{
|
||||
//if user filled to full.
|
||||
if (convertedConsumption > 0.00M)
|
||||
{
|
||||
gasRecordViewModel.MilesPerGallon = useMPG ? (unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption) : 100 / ((unFactoredMileage + deltaMileage) / (unFactoredConsumption + convertedConsumption));
|
||||
}
|
||||
//reset unFactored vars
|
||||
unFactoredConsumption = 0;
|
||||
unFactoredMileage = 0;
|
||||
@@ -73,13 +80,17 @@ namespace CarCareTracker.Helper
|
||||
{
|
||||
Id = currentObject.Id,
|
||||
VehicleId = currentObject.VehicleId,
|
||||
MonthId = currentObject.Date.Month,
|
||||
Date = currentObject.Date.ToShortDateString(),
|
||||
Mileage = currentObject.Mileage,
|
||||
Gallons = convertedConsumption,
|
||||
Cost = currentObject.Cost,
|
||||
DeltaMileage = 0,
|
||||
MilesPerGallon = 0,
|
||||
CostPerGallon = currentObject.Cost / convertedConsumption
|
||||
CostPerGallon = convertedConsumption > 0.00M ? currentObject.Cost / convertedConsumption : 0,
|
||||
IsFillToFull = currentObject.IsFillToFull,
|
||||
MissedFuelUp = currentObject.MissedFuelUp,
|
||||
Notes = currentObject.Notes
|
||||
});
|
||||
}
|
||||
previousMileage = currentObject.Mileage;
|
||||
|
||||
@@ -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
85
Helper/MailHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ namespace CarCareTracker.Helper
|
||||
Mileage = reminder.Mileage,
|
||||
Description = reminder.Description,
|
||||
Notes = reminder.Notes,
|
||||
Metric = reminder.Metric
|
||||
Metric = reminder.Metric,
|
||||
IsRecurring = reminder.IsRecurring
|
||||
};
|
||||
if (reminder.Metric == ReminderMetric.Both)
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return serviceRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
@@ -33,6 +34,7 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return repairRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
@@ -45,6 +47,7 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return upgradeRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
@@ -57,6 +60,7 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return gasRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
@@ -69,6 +73,7 @@ namespace CarCareTracker.Helper
|
||||
}
|
||||
return taxRecords.GroupBy(x => x.Date.Month).OrderBy(x => x.Key).Select(x => new CostForVehicleByMonth
|
||||
{
|
||||
MonthId = x.Key,
|
||||
MonthName = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(x.Key),
|
||||
Cost = x.Sum(y => y.Cost)
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace CarCareTracker.Helper
|
||||
using CarCareTracker.Models;
|
||||
|
||||
namespace CarCareTracker.Helper
|
||||
{
|
||||
/// <summary>
|
||||
/// helper method for static vars
|
||||
@@ -7,6 +9,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)
|
||||
{
|
||||
@@ -17,10 +20,48 @@
|
||||
if (input.Length > maxLength)
|
||||
{
|
||||
return (input.Substring(0, maxLength) + "...");
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
public static string DefaultActiveTab(UserConfig userConfig, ImportMode tab)
|
||||
{
|
||||
var defaultTab = userConfig.DefaultTab;
|
||||
var visibleTabs = userConfig.VisibleTabs;
|
||||
if (visibleTabs.Contains(tab) && tab == defaultTab)
|
||||
{
|
||||
return "active";
|
||||
}
|
||||
else if (!visibleTabs.Contains(tab))
|
||||
{
|
||||
return "d-none";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public static string DefaultActiveTabContent(UserConfig userConfig, ImportMode tab)
|
||||
{
|
||||
var defaultTab = userConfig.DefaultTab;
|
||||
if (tab == defaultTab)
|
||||
{
|
||||
return "show active";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
public static string DefaultTabSelected(UserConfig userConfig, ImportMode tab)
|
||||
{
|
||||
var defaultTab = userConfig.DefaultTab;
|
||||
var visibleTabs = userConfig.VisibleTabs;
|
||||
if (!visibleTabs.Contains(tab))
|
||||
{
|
||||
return "disabled";
|
||||
}
|
||||
else if (tab == defaultTab)
|
||||
{
|
||||
return "selected";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5
LICENSE
5
LICENSE
@@ -1,3 +1,8 @@
|
||||
LubeLogger by Hargata Softworks is licensed under the MIT License for individual
|
||||
and personal use. Commercial users and/or corporate entities are required
|
||||
to maintain an active subscription in order to continue using LubeLogger.
|
||||
For pricing information please contact us at hargatasoftworks@gmail.com
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Hargata Softworks
|
||||
|
||||
354
Logic/LoginLogic.cs
Normal file
354
Logic/LoginLogic.cs
Normal 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
118
Logic/UserLogic.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,28 @@ using CsvHelper.Configuration;
|
||||
|
||||
namespace CarCareTracker.MapProfile
|
||||
{
|
||||
public class FuellyMapper: ClassMap<ImportModel>
|
||||
public class ImportMapper: ClassMap<ImportModel>
|
||||
{
|
||||
public FuellyMapper()
|
||||
public ImportMapper()
|
||||
{
|
||||
Map(m => m.Date).Name(["date", "fuelup_date"]);
|
||||
Map(m => m.DateCreated).Name(["datecreated"]);
|
||||
Map(m => m.DateModified).Name(["datemodified"]);
|
||||
Map(m => m.Odometer).Name(["odometer"]);
|
||||
Map(m => m.FuelConsumed).Name(["gallons", "liters", "litres", "consumption", "quantity", "fueleconomy"]);
|
||||
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"]);
|
||||
Map(m => m.PartialFuelUp).Name(["partial_fuelup"]);
|
||||
Map(m => m.IsFillToFull).Name(["isfilltofull", "filled up"]);
|
||||
Map(m => m.Description).Name(["description"]);
|
||||
Map(m => m.MissedFuelUp).Name(["missed_fuelup"]);
|
||||
Map(m => m.MissedFuelUp).Name(["missed_fuelup", "missedfuelup"]);
|
||||
Map(m => m.PartSupplier).Name(["partsupplier"]);
|
||||
Map(m => m.PartQuantity).Name(["partquantity"]);
|
||||
Map(m => m.PartNumber).Name(["partnumber"]);
|
||||
Map(m => m.Progress).Name(["progress"]);
|
||||
Map(m => m.Type).Name(["type"]);
|
||||
Map(m => m.Priority).Name(["priority"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -81,6 +93,8 @@ namespace CarCareTracker.Middleware
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(access_token))
|
||||
{
|
||||
try
|
||||
{
|
||||
//decrypt the access token.
|
||||
var decryptedCookie = _dataProtector.Unprotect(access_token);
|
||||
@@ -93,23 +107,43 @@ namespace CarCareTracker.Middleware
|
||||
//if cookie is expired
|
||||
return AuthenticateResult.Fail("Expired credentials");
|
||||
}
|
||||
else if (authCookie.Id == default || string.IsNullOrWhiteSpace(authCookie.UserName))
|
||||
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.UserName)
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -126,5 +160,18 @@ namespace CarCareTracker.Middleware
|
||||
Response.Redirect("/Login/Index");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
protected override Task HandleForbiddenAsync(AuthenticationProperties properties)
|
||||
{
|
||||
if (Request.RouteValues.TryGetValue("controller", out object value))
|
||||
{
|
||||
if (value.ToString().ToLower() == "api")
|
||||
{
|
||||
Response.StatusCode = 403;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Response.Redirect("/Error/401");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
Models/Admin/AdminViewModel.cs
Normal file
8
Models/Admin/AdminViewModel.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class AdminViewModel
|
||||
{
|
||||
public List<UserData> Users { get; set; }
|
||||
public List<Token> Tokens { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
|
||||
12
Models/Configuration/MailConfig.cs
Normal file
12
Models/Configuration/MailConfig.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
public decimal Cost { get; set; }
|
||||
public bool IsFillToFull { get; set; } = true;
|
||||
public bool MissedFuelUp { get; set; } = false;
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
/// <summary>
|
||||
/// American moment
|
||||
/// </summary>
|
||||
@@ -16,6 +16,7 @@
|
||||
public decimal Cost { get; set; }
|
||||
public bool IsFillToFull { get; set; } = true;
|
||||
public bool MissedFuelUp { get; set; } = false;
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public GasRecord ToGasRecord() { return new GasRecord {
|
||||
Id = Id,
|
||||
@@ -26,7 +27,8 @@
|
||||
VehicleId = VehicleId,
|
||||
Files = Files,
|
||||
IsFillToFull = IsFillToFull,
|
||||
MissedFuelUp = MissedFuelUp
|
||||
MissedFuelUp = MissedFuelUp,
|
||||
Notes = Notes
|
||||
}; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -17,5 +18,8 @@
|
||||
public int DeltaMileage { get; set; }
|
||||
public decimal MilesPerGallon { get; set; }
|
||||
public decimal CostPerGallon { get; set; }
|
||||
public bool IsFillToFull { get; set; }
|
||||
public bool MissedFuelUp { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Import model used for importing Gas records.
|
||||
/// Import model used for importing records via CSV.
|
||||
/// </summary>
|
||||
public class ImportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string DateCreated { get; set; }
|
||||
public string DateModified { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Priority { get; set; }
|
||||
public string Progress { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
@@ -15,7 +20,22 @@
|
||||
public string PartialFuelUp { get; set; }
|
||||
public string IsFillToFull { get; set; }
|
||||
public string MissedFuelUp { get; set; }
|
||||
public string PartNumber { get; set; }
|
||||
public string PartSupplier { get; set; }
|
||||
public string PartQuantity { get; set; }
|
||||
}
|
||||
|
||||
public class SupplyRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string PartNumber { get; set; }
|
||||
public string PartSupplier { get; set; }
|
||||
public string PartQuantity { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Cost { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
|
||||
public class ServiceRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
@@ -24,6 +44,12 @@
|
||||
public string Notes { get; set; }
|
||||
public string Cost { get; set; }
|
||||
}
|
||||
public class OdometerRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
public class TaxRecordExportModel
|
||||
{
|
||||
public string Date { get; set; }
|
||||
@@ -39,6 +65,9 @@
|
||||
public string FuelConsumed { get; set; }
|
||||
public string Cost { get; set; }
|
||||
public string FuelEconomy { get; set; }
|
||||
public string IsFillToFull { get; set; }
|
||||
public string MissedFuelUp { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
public class ReminderExportModel
|
||||
{
|
||||
@@ -47,4 +76,16 @@
|
||||
public string Metric { get; set; }
|
||||
public string Notes { get; set; }
|
||||
}
|
||||
public class PlanRecordExportModel
|
||||
{
|
||||
public string DateCreated { get; set; }
|
||||
public string DateModified { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string Priority { get; set; }
|
||||
public string Progress { get; set; }
|
||||
public string Cost { get; set; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
9
Models/Login/Token.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
12
Models/OdometerRecord/OdometerRecord.cs
Normal file
12
Models/OdometerRecord/OdometerRecord.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OdometerRecord
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public int Mileage { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
}
|
||||
13
Models/OdometerRecord/OdometerRecordInput.cs
Normal file
13
Models/OdometerRecord/OdometerRecordInput.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OdometerRecordInput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public OdometerRecord ToOdometerRecord() { return new OdometerRecord { Id = Id, VehicleId = VehicleId, Date = DateTime.Parse(Date), Mileage = Mileage, Notes = Notes, Files = Files }; }
|
||||
}
|
||||
}
|
||||
8
Models/OperationResponse.cs
Normal file
8
Models/OperationResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class OperationResponse
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
||||
8
Models/PlanRecord/PlanCostItem.cs
Normal file
8
Models/PlanRecord/PlanCostItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class PlanCostItem
|
||||
{
|
||||
public string CostName { get; set; }
|
||||
public decimal CostAmount { get; set; }
|
||||
}
|
||||
}
|
||||
17
Models/PlanRecord/PlanRecord.cs
Normal file
17
Models/PlanRecord/PlanRecord.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class PlanRecord
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public DateTime DateCreated { get; set; }
|
||||
public DateTime DateModified { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public ImportMode ImportMode { get; set; }
|
||||
public PlanPriority Priority { get; set; }
|
||||
public PlanProgress Progress { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
}
|
||||
30
Models/PlanRecord/PlanRecordInput.cs
Normal file
30
Models/PlanRecord/PlanRecordInput.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class PlanRecordInput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string DateCreated { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string DateModified { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public ImportMode ImportMode { get; set; }
|
||||
public PlanPriority Priority { get; set; }
|
||||
public PlanProgress Progress { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public PlanRecord ToPlanRecord() { return new PlanRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
DateCreated = DateTime.Parse(DateCreated),
|
||||
DateModified = DateTime.Parse(DateModified),
|
||||
Description = Description,
|
||||
Notes = Notes,
|
||||
Files = Files,
|
||||
ImportMode = ImportMode,
|
||||
Cost = Cost,
|
||||
Priority = Priority,
|
||||
Progress = Progress
|
||||
}; }
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,9 @@
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
public ReminderMileageInterval ReminderMileageInterval { get; set; } = ReminderMileageInterval.FiveThousandMiles;
|
||||
public ReminderMonthInterval ReminderMonthInterval { get; set; } = ReminderMonthInterval.OneYear;
|
||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||
public ReminderRecord ToReminderRecord() { return new ReminderRecord {
|
||||
Id = Id,
|
||||
@@ -16,6 +19,9 @@
|
||||
Mileage = Mileage,
|
||||
Description = Description,
|
||||
Metric = Metric,
|
||||
IsRecurring = IsRecurring,
|
||||
ReminderMileageInterval = ReminderMileageInterval,
|
||||
ReminderMonthInterval = ReminderMonthInterval,
|
||||
Notes = Notes }; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,5 +13,9 @@
|
||||
/// </summary>
|
||||
public ReminderMetric Metric { get; set; } = ReminderMetric.Date;
|
||||
public ReminderUrgency Urgency { get; set; } = ReminderUrgency.NotUrgent;
|
||||
/// <summary>
|
||||
/// Recurring Reminders
|
||||
/// </summary>
|
||||
public bool IsRecurring { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{
|
||||
public class CostForVehicleByMonth
|
||||
{
|
||||
public int MonthId { get; set; }
|
||||
public string MonthName { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
|
||||
15
Models/Report/GenericReportModel.cs
Normal file
15
Models/Report/GenericReportModel.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Generic Model used for vehicle history report.
|
||||
/// </summary>
|
||||
public class GenericReportModel
|
||||
{
|
||||
public ImportMode DataType { get; set; }
|
||||
public DateTime Date { get; set; }
|
||||
public int Odometer { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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/Report/VehicleHistoryViewModel.cs
Normal file
12
Models/Report/VehicleHistoryViewModel.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class VehicleHistoryViewModel
|
||||
{
|
||||
public Vehicle VehicleData { get; set; }
|
||||
public List<GenericReportModel> VehicleHistory { get; set; }
|
||||
public string Odometer { get; set; }
|
||||
public decimal MPG { get; set; }
|
||||
public decimal TotalCost { get; set; }
|
||||
public decimal TotalGasCost { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
|
||||
37
Models/Supply/SupplyRecord.cs
Normal file
37
Models/Supply/SupplyRecord.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class SupplyRecord
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
/// <summary>
|
||||
/// When the part or supplies were purchased.
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
/// <summary>
|
||||
/// Part number can be alphanumeric.
|
||||
/// </summary>
|
||||
public string PartNumber { get; set; }
|
||||
/// <summary>
|
||||
/// Where the part/supplies were purchased from.
|
||||
/// </summary>
|
||||
public string PartSupplier { get; set; }
|
||||
/// <summary>
|
||||
/// Amount purchased, can be partial quantities such as fluids.
|
||||
/// </summary>
|
||||
public decimal Quantity { get; set; }
|
||||
/// <summary>
|
||||
/// Description of the part/supplies purchased.
|
||||
/// </summary>
|
||||
public string Description { get; set; }
|
||||
/// <summary>
|
||||
/// How much it costs
|
||||
/// </summary>
|
||||
public decimal Cost { get; set; }
|
||||
/// <summary>
|
||||
/// Additional notes.
|
||||
/// </summary>
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
}
|
||||
}
|
||||
27
Models/Supply/SupplyRecordInput.cs
Normal file
27
Models/Supply/SupplyRecordInput.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class SupplyRecordInput
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string PartNumber { get; set; }
|
||||
public string PartSupplier { get; set; }
|
||||
public decimal Quantity { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public string Notes { get; set; }
|
||||
public List<UploadedFiles> Files { get; set; } = new List<UploadedFiles>();
|
||||
public SupplyRecord ToSupplyRecord() { return new SupplyRecord {
|
||||
Id = Id,
|
||||
VehicleId = VehicleId,
|
||||
Date = DateTime.Parse(Date),
|
||||
Cost = Cost,
|
||||
PartNumber = PartNumber,
|
||||
PartSupplier = PartSupplier,
|
||||
Quantity = Quantity,
|
||||
Description = Description,
|
||||
Notes = Notes,
|
||||
Files = Files }; }
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
public string Notes { get; set; }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int VehicleId { get; set; }
|
||||
public string Date { get; set; }
|
||||
public string Date { get; set; } = DateTime.Now.ToShortDateString();
|
||||
public int Mileage { get; set; }
|
||||
public string Description { get; set; }
|
||||
public decimal Cost { get; set; }
|
||||
12
Models/User/UserAccess.cs
Normal file
12
Models/User/UserAccess.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
8
Models/User/UserCollaborator.cs
Normal file
8
Models/User/UserCollaborator.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace CarCareTracker.Models
|
||||
{
|
||||
public class UserCollaborator
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
public UserVehicle UserVehicle { get; set; }
|
||||
}
|
||||
}
|
||||
11
Models/User/UserConfigData.cs
Normal file
11
Models/User/UserConfigData.cs
Normal 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
12
Models/User/UserData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,18 @@
|
||||
public bool EnableAuth { get; set; }
|
||||
public bool HideZero { get; set; }
|
||||
public bool UseUKMPG {get;set;}
|
||||
public bool UseThreeDecimalGasCost { get; set; }
|
||||
public string UserNameHash { get; set; }
|
||||
public string UserPasswordHash { get; set;}
|
||||
public List<ImportMode> VisibleTabs { get; set; } = new List<ImportMode>() {
|
||||
ImportMode.Dashboard,
|
||||
ImportMode.ServiceRecord,
|
||||
ImportMode.RepairRecord,
|
||||
ImportMode.GasRecord,
|
||||
ImportMode.UpgradeRecord,
|
||||
ImportMode.TaxRecord,
|
||||
ImportMode.ReminderRecord,
|
||||
ImportMode.NoteRecord};
|
||||
public ImportMode DefaultTab { get; set; } = ImportMode.Dashboard;
|
||||
}
|
||||
}
|
||||
15
Program.cs
15
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,25 @@ 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>();
|
||||
builder.Services.AddSingleton<ISupplyRecordDataAccess, SupplyRecordDataAccess>();
|
||||
builder.Services.AddSingleton<IPlanRecordDataAccess, PlanRecordDataAccess>();
|
||||
builder.Services.AddSingleton<IOdometerRecordDataAccess, OdometerRecordDataAccess>();
|
||||
|
||||
//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"))
|
||||
{
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
|
||||
A self-hosted, open-source vehicle service records and maintainence tracker.
|
||||
|
||||
Support this project on Patreon: https://patreon.com/LubeLogger
|
||||
|
||||
## Why
|
||||
Because nobody should have to deal with a homemade spreadsheet or a shoebox full of receipts when it comes to vehicle maintainence.
|
||||
|
||||
## Screenshots
|
||||
<a href="/docs/screenshots.md">Screenshots</a>
|
||||
|
||||
## Dependencies
|
||||
- Bootstrap
|
||||
- LiteDB
|
||||
@@ -16,14 +21,14 @@ Because nobody should have to deal with a homemade spreadsheet or a shoebox full
|
||||
## Docker Setup (GHCR)
|
||||
1. Install Docker
|
||||
2. Run `docker pull ghcr.io/hargata/lubelogger:latest`
|
||||
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats.
|
||||
3. CHECK culture in .env file, default is en_US, this will change the currency and date formats. You can also setup SMTP Config here.
|
||||
4. If not using traefik, use docker-compose-notraefik.yml
|
||||
5. Run `docker-compose up`
|
||||
|
||||
## Docker Setup (Manual Build)
|
||||
1. Install Docker
|
||||
2. Clone this repo
|
||||
3. CHECK culture in .env file, default is en_US
|
||||
3. CHECK culture in .env file, default is en_US, also setup SMTP for user management if you want that.
|
||||
4. Run `docker build -t lubelogger -f Dockerfile .`
|
||||
5. CHECK docker-compose.yml and make sure the mounting directories look correct.
|
||||
6. If not using traefik, use docker-compose-notraefik.yml
|
||||
|
||||
@@ -125,3 +125,54 @@
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/makebackup</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Creates a snapshot/backup of the DB at the time and returns a URL to download it.
|
||||
</div>
|
||||
<div class="col-3">
|
||||
No Params(must be root user)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
GET
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/odometerrecords</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of odometer records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-1">
|
||||
POST
|
||||
</div>
|
||||
<div class="col-5">
|
||||
<code>/api/vehicle/odometerrecords/add</code>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
Returns a list of odometer records for the vehicle
|
||||
</div>
|
||||
<div class="col-3">
|
||||
vehicleId - Id of Vehicle
|
||||
<br />
|
||||
Body(form-data): {<br />
|
||||
date - Date to be entered<br />
|
||||
odometer - Odometer reading<br />
|
||||
notes - notes(optional)<br />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
153
Views/Admin/Index.cshtml
Normal file
153
Views/Admin/Index.cshtml
Normal 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 class="sticky-top">
|
||||
<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 class="sticky-top">
|
||||
<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>
|
||||
@@ -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
|
||||
@{
|
||||
@@ -9,24 +10,60 @@
|
||||
@section Scripts {
|
||||
<script src="~/js/garage.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
||||
<ul class="navbar-nav" id="homeTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><span class="ms-2 display-3"><i class="bi bi-car-front me-2"></i>Garage</span></button>
|
||||
</li>
|
||||
<li class="nav-item" 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"><span class="ms-2 display-3"><i class="bi bi-gear me-2"></i>Settings</span></button>
|
||||
</li>
|
||||
@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>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row mt-2">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="d-flex lubelogger-navbar">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<div class="lubelogger-navbar-button">
|
||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<ul class="nav nav-tabs" id="homeTab" role="tablist">
|
||||
<ul class="nav nav-tabs lubelogger-tab" id="homeTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @(Model == "garage" ? "active" : "")" id="garage-tab" data-bs-toggle="tab" data-bs-target="#garage-tab-pane" type="button" role="tab"><i class="bi bi-car-front me-2"></i>Garage</button>
|
||||
</li>
|
||||
<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>
|
||||
@@ -38,7 +75,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade @(Model == "settings" ? "show active" : "")" id="settings-tab-pane" role="tabpanel" tabindex="0">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,11 +82,10 @@
|
||||
<div class="modal fade" id="addVehicleModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content" id="addVehicleModalContent">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
loadGarage();
|
||||
loadSettings();
|
||||
bindWindowResize();
|
||||
</script>
|
||||
@@ -1,4 +1,5 @@
|
||||
@model UserConfig
|
||||
@using CarCareTracker.Helper
|
||||
@model UserConfig
|
||||
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-center">
|
||||
@@ -22,8 +23,6 @@
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useUKMPG" checked="@Model.UseUKMPG">
|
||||
<label class="form-check-label" for="useUKMPG">Use UK MPG Calculation<br /><small class="text-body-secondary">Input Gas Consumption in Liters, it will be converted to UK Gals for MPG Calculation</small></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" onChange="updateSettings()" type="checkbox" role="switch" id="useDescending" checked="@Model.UseDescending">
|
||||
<label class="form-check-label" for="useDescending">Sort lists in Descending Order(Newest to Oldest)</label>
|
||||
@@ -32,10 +31,109 @@
|
||||
<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="updateSettings()" type="checkbox" role="switch" id="useThreeDecimal" checked="@Model.UseThreeDecimalGasCost">
|
||||
<label class="form-check-label" for="useThreeDecimal">Use Three Decimals For Fuel Cost</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 class="col-12 col-md-6">
|
||||
<div class="row" id="visibleTabs">
|
||||
<div class="col-12">
|
||||
<span class="lead">Visible Tabs</span>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ServiceRecord" id="serviceRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ServiceRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="serviceRecordTab">Service Records</label>
|
||||
</li>
|
||||
<li class="list-group-item d-none">
|
||||
<input onChange="updateSettings()" disabled class="form-check-input me-1" type="checkbox" value="Dashboard" id="dashboardTab" @(Model.VisibleTabs.Contains(ImportMode.Dashboard) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="dashboardTab">Dashboard</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="RepairRecord" id="repairRecordTab" @(Model.VisibleTabs.Contains(ImportMode.RepairRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="repairRecordTab">Repairs</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="UpgradeRecord" id="upgradeRecordTab" @(Model.VisibleTabs.Contains(ImportMode.UpgradeRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="upgradeRecordTab">Upgrades</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="GasRecord" id="gasRecordTab" @(Model.VisibleTabs.Contains(ImportMode.GasRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="gasRecordTab">Fuel</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="OdometerRecord" id="odometerRecordTab" @(Model.VisibleTabs.Contains(ImportMode.OdometerRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="odometerRecordTab">Odometer</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<ul class="list-group">
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="TaxRecord" id="taxRecordTab" @(Model.VisibleTabs.Contains(ImportMode.TaxRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="taxRecordTab">Taxes</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="NoteRecord" id="noteRecordTab" @(Model.VisibleTabs.Contains(ImportMode.NoteRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="noteRecordTab">Notes</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="ReminderRecord" id="reminderRecordTab" @(Model.VisibleTabs.Contains(ImportMode.ReminderRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="reminderRecordTab">Reminder</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="SupplyRecord" id="supplyRecordTab" @(Model.VisibleTabs.Contains(ImportMode.SupplyRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="supplyRecordTab">Supplies</label>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<input onChange="updateSettings()" class="form-check-input me-1" type="checkbox" value="PlanRecord" id="planRecordTab" @(Model.VisibleTabs.Contains(ImportMode.PlanRecord) ? "checked" : "")>
|
||||
<label class="form-check-label stretched-link" for="planRecordTab">Planner</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead">Default Tab</span>
|
||||
<select class="form-select" onchange="updateSettings()" id="defaultTab">
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.Dashboard)) value="Dashboard">Dashboard</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model,ImportMode.ServiceRecord)) value="ServiceRecord">Service Record</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.RepairRecord)) value="RepairRecord">Repairs</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.UpgradeRecord)) value="UpgradeRecord">Upgrades</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.GasRecord)) value="GasRecord">Fuel</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.TaxRecord)) value="TaxRecord">Tax</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.NoteRecord)) value="NoteRecord">Notes</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.ReminderRecord)) value="ReminderRecord">Reminders</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.SupplyRecord)) value="SupplyRecord">Supplies</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.PlanRecord)) value="PlanRecord">Planner</!option>
|
||||
<!option @(StaticHelper.DefaultTabSelected(Model, ImportMode.OdometerRecord)) value="OdometerRecord">Odometer</!option>
|
||||
</select>
|
||||
</div>
|
||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||
{
|
||||
<div class="col-12 col-md-6">
|
||||
<span class="lead">Backups</span>
|
||||
<div class="row">
|
||||
<div class="col-6 d-grid">
|
||||
<button onclick="makeBackup()" class="btn btn-primary btn-md">Make</button>
|
||||
</div>
|
||||
<div class="col-6 d-grid">
|
||||
<input onChange="restoreBackup(this)" type="file" accept=".zip" class="d-none" id="inputBackup">
|
||||
<button onclick="openRestoreBackup()" class="btn btn-secondary btn-md">Restore</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
@@ -47,10 +145,16 @@
|
||||
<div class="d-flex justify-content-center">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<small class="text-body-secondary">Version 1.0.6</small>
|
||||
</div>
|
||||
<p class="lead">
|
||||
Proudly developed in the rural town of Price, Utah by Hargata Softworks.
|
||||
</p>
|
||||
<p class="lead">If you enjoyed using this app, please consider spreading the good word.</p>
|
||||
<p class="lead">
|
||||
If you enjoyed using this app, please consider spreading the good word.<br />
|
||||
If you are a commercial user, or if you just want to support the development of this project, consider subscribing to <a class="link-light link-offset-2 link-underline-opacity-25 link-underline-opacity-100-hover" href="https://www.patreon.com/LubeLogger" target="_blank">our Patreon</a>
|
||||
</p>
|
||||
<div class="d-flex justify-content-center">
|
||||
<h6 class="display-7 mt-2">Hometown Shoutout</h6>
|
||||
</div>
|
||||
@@ -79,16 +183,32 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function getCheckedTabs() {
|
||||
var visibleTabs = $("#visibleTabs :checked").map(function () {
|
||||
return this.value;
|
||||
});
|
||||
return visibleTabs.toArray();
|
||||
}
|
||||
function updateSettings(){
|
||||
var visibleTabs = getCheckedTabs();
|
||||
var defaultTab = $("#defaultTab").val();
|
||||
if (!visibleTabs.includes(defaultTab)){
|
||||
defaultTab = "Dashboard"; //default to dashboard.
|
||||
}
|
||||
var userConfigObject = {
|
||||
useDarkMode: $("#enableDarkMode").is(':checked'),
|
||||
enableCsvImports: $("#enableCsvImports").is(':checked'),
|
||||
useMPG: $("#useMPG").is(':checked'),
|
||||
useDescending: $("#useDescending").is(':checked'),
|
||||
hideZero: $("#hideZero").is(":checked"),
|
||||
useUKMpg: $("#useUKMPG").is(":checked")
|
||||
useUKMpg: $("#useUKMPG").is(":checked"),
|
||||
useThreeDecimalGasCost: $("#useThreeDecimal").is(":checked"),
|
||||
visibleTabs: visibleTabs,
|
||||
defaultTab: defaultTab
|
||||
}
|
||||
sloader.show();
|
||||
$.post('/Home/WriteToSettings', { userConfig: userConfigObject}, function(data){
|
||||
sloader.hide();
|
||||
if (data) {
|
||||
setTimeout(function () { window.location.href = '/Home/Index?tab=settings' }, 500);
|
||||
} else {
|
||||
@@ -96,6 +216,40 @@
|
||||
}
|
||||
})
|
||||
}
|
||||
function makeBackup() {
|
||||
$.get('/Files/MakeBackup', function (data) {
|
||||
window.location.href = data;
|
||||
});
|
||||
}
|
||||
function openRestoreBackup(){
|
||||
$("#inputBackup").click();
|
||||
}
|
||||
function restoreBackup(event) {
|
||||
let formData = new FormData();
|
||||
formData.append("file", event.files[0]);
|
||||
sloader.show();
|
||||
$.ajax({
|
||||
url: "/Files/HandleFileUpload",
|
||||
data: formData,
|
||||
cache: false,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
type: 'POST',
|
||||
success: function (response) {
|
||||
if (response.trim() != '') {
|
||||
$.post('/Files/RestoreBackup', { fileName : response}, function (data) {
|
||||
sloader.hide();
|
||||
if (data){
|
||||
successToast("Backup Restored");
|
||||
setTimeout(function () { window.location.href = '/Home/Index' }, 500);
|
||||
} else {
|
||||
errorToast("An error occurred, please try again later.");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
function enableAuthCheckChanged(){
|
||||
var enableAuth = $("#enableAuth").is(":checked");
|
||||
if (enableAuth) {
|
||||
@@ -120,7 +274,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.");
|
||||
}
|
||||
|
||||
26
Views/Login/ForgotPassword.cshtml
Normal file
26
Views/Login/ForgotPassword.cshtml
Normal 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>
|
||||
@@ -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>
|
||||
35
Views/Login/Registration.cshtml
Normal file
35
Views/Login/Registration.cshtml
Normal 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>
|
||||
31
Views/Login/ResetPassword.cshtml
Normal file
31
Views/Login/ResetPassword.cshtml
Normal file
@@ -0,0 +1,31 @@
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - Register";
|
||||
}
|
||||
@section Scripts {
|
||||
<script src="~/js/login.js" asp-append-version="true"></script>
|
||||
}
|
||||
<div class="container d-flex align-items-center justify-content-center" style="height:100vh">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
<div class="form-group">
|
||||
<label for="inputToken">Token</label>
|
||||
<input type="text" id="inputToken" class="form-control">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inputUserName">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
1
Views/Shared/401.cshtml
Normal file
@@ -0,0 +1 @@
|
||||
<h1>Access Denied</h1>
|
||||
@@ -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"))
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
@{
|
||||
@using CarCareTracker.Helper
|
||||
@{
|
||||
ViewData["Title"] = "LubeLogger - View Vehicle";
|
||||
}
|
||||
@inject IConfigHelper config
|
||||
@{
|
||||
var userConfig = config.GetUserConfig(User);
|
||||
}
|
||||
@model Vehicle
|
||||
@section Scripts {
|
||||
<script src="~/js/vehicle.js" asp-append-version="true"></script>
|
||||
@@ -11,41 +16,103 @@
|
||||
<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="~/js/supplyrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/planrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/js/odometerrecord.js" asp-append-version="true"></script>
|
||||
<script src="~/lib/chart-js/chart.umd.js"></script>
|
||||
}
|
||||
<div class="lubelogger-mobile-nav" onclick="hideMobileNav()">
|
||||
<ul class="nav navbar-nav" id="vehicleTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" onclick="returnToGarage()"><span class="display-3 ms-2"><i class="bi bi-arrow-left-square"></i>Garage</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<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 @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" 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 @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-bar-chart-steps me-2"></i>Planner</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-speedometer me-2"></i>Odometer</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" 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 @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" 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>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-fuel-pump me-2"></i>Fuel</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-shop me-2"></i>Supplies</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-currency-dollar me-2"></i>Taxes</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><span class="display-3 ms-2"><i class="bi bi-journal-bookmark me-2"></i>Notes</span></button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" 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 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>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="d-flex justify-content-between">
|
||||
<button onclick="returnToGarage()" class="btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
|
||||
<button onclick="returnToGarage()" class="lubelogger-tab btn btn-secondary btn-md mt-1 mb-1"><i class="bi bi-arrow-left-square"></i></button>
|
||||
<h1 class="text-truncate display-4">@($"{Model.Year} {Model.Make} {Model.Model}")<small class="text-body-secondary">@($"(#{Model.LicensePlate})")</small></h1>
|
||||
<button onclick="editVehicle(@Model.Id)" class="btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
|
||||
<button onclick="editVehicle(@Model.Id)" class="lubelogger-tab btn btn-warning btn-md mt-1 mb-1"><i class="bi bi-pencil-square"></i></button>
|
||||
<div class="lubelogger-navbar-button">
|
||||
<button type="button" class="btn btn-dark" onclick="showMobileNav()"><i class="bi bi-list lubelogger-menu-icon"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<ul class="nav nav-tabs" id="vehicleTab" role="tablist">
|
||||
<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 @StaticHelper.DefaultActiveTab(userConfig, ImportMode.Dashboard)" 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="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>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.PlanRecord)" id="plan-tab" data-bs-toggle="tab" data-bs-target="#plan-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-bar-chart-steps me-2"></i>Planner</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.OdometerRecord)" id="odometer-tab" data-bs-toggle="tab" data-bs-target="#odometer-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-speedometer me-2"></i>Odometer</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</button>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ServiceRecord)" 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="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>Taxes</button>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.RepairRecord)" 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>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab" data-bs-toggle="tab" data-bs-target="#upgrade-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-wrench-adjustable me-2"></i>Upgrades</button>
|
||||
</li>
|
||||
<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 id="reminderBellDiv" style="display:inline-flex;"><i id="reminderBell" class="bi bi-bell me-2"></i></div>Reminders</button>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.GasRecord)" id="gas-tab" data-bs-toggle="tab" data-bs-target="#gas-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-fuel-pump me-2"></i>Fuel</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>
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.SupplyRecord)" id="supply-tab" data-bs-toggle="tab" data-bs-target="#supply-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-shop me-2"></i>Supplies</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.TaxRecord)" id="tax-tab" data-bs-toggle="tab" data-bs-target="#tax-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-currency-dollar me-2"></i>Taxes</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.NoteRecord)" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes-tab-pane" type="button" role="tab" aria-selected="false"><i class="bi bi-journal-bookmark me-2"></i>Notes</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link @StaticHelper.DefaultActiveTab(userConfig, ImportMode.ReminderRecord)" 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 dropdown ms-auto" role="presentation">
|
||||
<a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-expanded="false">Manage Vehicle</a>
|
||||
@@ -55,14 +122,17 @@
|
||||
</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="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" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.ServiceRecord)" id="servicerecord-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.GasRecord)" id="gas-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.TaxRecord)" id="tax-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.NoteRecord)" id="notes-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.RepairRecord)" id="accident-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.ReminderRecord)" id="reminder-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.Dashboard)" id="report-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.UpgradeRecord)" id="upgrade-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.SupplyRecord)" id="supply-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.PlanRecord)" id="plan-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
<div class="tab-pane fade @StaticHelper.DefaultActiveTabContent(userConfig, ImportMode.OdometerRecord)" id="odometer-tab-pane" role="tabpanel" tabindex="0"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="editVehicleModal" tabindex="-1" role="dialog">
|
||||
@@ -87,4 +157,8 @@
|
||||
function GetVehicleId() {
|
||||
return { vehicleId: @Model.Id};
|
||||
}
|
||||
function GetDefaultTab() {
|
||||
return { tab: "@userConfig.DefaultTab" };
|
||||
}
|
||||
bindWindowResize();
|
||||
</script>
|
||||
@@ -25,6 +25,15 @@
|
||||
} else if (Model == ImportMode.TaxRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/taxrecordsample.csv" target="_blank">Download Sample</a>
|
||||
} else if (Model == ImportMode.SupplyRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/supplysample.csv" target="_blank">Download Sample</a>
|
||||
} else if (Model == ImportMode.PlanRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/plansample.csv" target="_blank">Download Sample</a>
|
||||
} else if (Model == ImportMode.OdometerRecord)
|
||||
{
|
||||
<a class="btn btn-link" href="/defaults/odometersample.csv" target="_blank">Download Sample</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,6 +71,12 @@
|
||||
getVehicleTaxRecords(vehicleId);
|
||||
} else if (mode == "UpgradeRecord") {
|
||||
getVehicleUpgradeRecords(vehicleId);
|
||||
} else if (mode == "SupplyRecord") {
|
||||
getVehicleSupplyRecords(vehicleId);
|
||||
} else if (mode == "PlanRecord"){
|
||||
getVehiclePlanRecords(vehicleId);
|
||||
} else if (mode == "OdometerRecord") {
|
||||
getVehicleOdometerRecords(vehicleId);
|
||||
}
|
||||
} else {
|
||||
errorToast("An error has occurred, please double check the data and try again.");
|
||||
|
||||
72
Views/Vehicle/_Collaborators.cshtml
Normal file
72
Views/Vehicle/_Collaborators.cshtml
Normal 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>
|
||||
@@ -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">
|
||||
@@ -33,12 +34,17 @@
|
||||
</div>
|
||||
<div class="row vehicleDetailTabContainer">
|
||||
<div class="col-12">
|
||||
<div class="row mt-2 showOnPrint">
|
||||
<div class="d-flex">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-1">Date</th>
|
||||
<th scope="col" class="col-2 col-xl-1">Date</th>
|
||||
<th scope="col" class="col-2">Odometer</th>
|
||||
<th scope="col" class="col-4">Description</th>
|
||||
<th scope="col" class="col-3 col-xl-4">Description</th>
|
||||
<th scope="col" class="col-2">Cost</th>
|
||||
<th scope="col" class="col-3">Notes</th>
|
||||
</tr>
|
||||
@@ -47,9 +53,9 @@
|
||||
@foreach (CollisionRecord collisionRecord in Model)
|
||||
{
|
||||
<tr class="d-flex" style="cursor:pointer;" onclick="showEditCollisionRecordModal(@collisionRecord.Id)">
|
||||
<td class="col-1">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2 col-xl-1">@collisionRecord.Date.ToShortDateString()</td>
|
||||
<td class="col-2">@collisionRecord.Mileage</td>
|
||||
<td class="col-4">@collisionRecord.Description</td>
|
||||
<td class="col-3 col-xl-4">@collisionRecord.Description</td>
|
||||
<td class="col-2">@((hideZero && collisionRecord.Cost == default) ? "---" : collisionRecord.Cost.ToString("C"))</td>
|
||||
<td class="col-3 text-truncate">@CarCareTracker.Helper.StaticHelper.TruncateStrings(collisionRecord.Notes)</td>
|
||||
</tr>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
new Chart($("#pie-chart"), {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: ["Planned Maintenance(Service Records)", "Unplanned Maintenance(Repairs)", "Upgrades", "Tax", "Fuel"],
|
||||
labels: ["Service Records", "Repairs", "Upgrades", "Tax", "Fuel"],
|
||||
datasets: [
|
||||
{
|
||||
label: "Expenses by Category",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
@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 userConfig = config.GetUserConfig(User);
|
||||
var enableCsvImports = userConfig.EnableCsvImports;
|
||||
var useMPG = userConfig.UseMPG;
|
||||
var useUKMPG = userConfig.UseUKMPG;
|
||||
var hideZero = userConfig.HideZero;
|
||||
var useThreeDecimals = userConfig.UseThreeDecimalGasCost;
|
||||
var gasCostFormat = useThreeDecimals ? "C3" : "C2";
|
||||
var useKwh = Model.UseKwh;
|
||||
string consumptionUnit;
|
||||
string fuelEconomyUnit;
|
||||
@@ -41,7 +45,7 @@
|
||||
<span class="ms-2 badge bg-primary">@($"Max Fuel Economy: {Model.GasRecords.Max(x => x.MilesPerGallon).ToString("F") ?? "0"}")</span>
|
||||
}
|
||||
<span class="ms-2 badge bg-success">@($"Total Fuel Consumed: {Model.GasRecords.Sum(x => x.Gallons).ToString("F")}")</span>
|
||||
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString("C3")}")</span>
|
||||
<span class="ms-2 badge bg-success">@($"Total Cost: {Model.GasRecords.Sum(x => x.Cost).ToString(gasCostFormat)}")</span>
|
||||
</div>
|
||||
@if (enableCsvImports)
|
||||
{
|
||||
@@ -62,8 +66,13 @@
|
||||
</div>
|
||||
<div class="row vehicleDetailTabContainer">
|
||||
<div class="col-12">
|
||||
<div class="row mt-2 showOnPrint">
|
||||
<div class="d-flex">
|
||||
<img src="/defaults/lubelogger_logo.png" />
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<thead class="sticky-top">
|
||||
<tr class="d-flex">
|
||||
<th scope="col" class="col-2">Date Refueled</th>
|
||||
<th scope="col" class="col-2">Odometer(@(distanceUnit))</th>
|
||||
@@ -81,8 +90,8 @@
|
||||
<td class="col-2">@gasRecord.Mileage</td>
|
||||
<td class="col-2">@gasRecord.Gallons.ToString("F")</td>
|
||||
<td class="col-4">@(gasRecord.MilesPerGallon == 0 ? "---" : gasRecord.MilesPerGallon.ToString("F"))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString("C3"))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString("C3"))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.Cost == default) ? "---" : gasRecord.Cost.ToString(gasCostFormat))</td>
|
||||
<td class="col-1">@((hideZero && gasRecord.CostPerGallon == default) ? "---" : gasRecord.CostPerGallon.ToString(gasCostFormat))</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@model List<CostForVehicleByMonth>
|
||||
@if (Model.Any())
|
||||
{
|
||||
<canvas id="bar-chart" class="vehicleDetailTabContainer"></canvas>
|
||||
<canvas id="bar-chart"></canvas>
|
||||
<script>
|
||||
renderChart();
|
||||
function renderChart() {
|
||||
|
||||
@@ -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;
|
||||
@@ -55,9 +56,25 @@
|
||||
<label class="form-check-label" for="gasIsMissed">Missed Fuel Up(Skip MPG Calculation)</label>
|
||||
</div>
|
||||
<label for="GasRecordCost">Cost</label>
|
||||
@if (isNew)
|
||||
{
|
||||
<div class="input-group">
|
||||
<input type="text" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)">
|
||||
<div class="input-group-text">
|
||||
<select class="form-select form-select-sm" id="gasCostType">
|
||||
<option value="total">Total</option>
|
||||
<option value="unit">Unit</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
} else
|
||||
{
|
||||
<input type="text" id="gasRecordCost" class="form-control" placeholder="Cost of gas refueled" value="@(isNew ? "" : Model.GasRecord.Cost)">
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<label for="gasRecordNotes">Notes(optional)</label>
|
||||
<textarea id="gasRecordNotes" class="form-control" rows="5">@Model.GasRecord.Notes</textarea>
|
||||
@if (Model.GasRecord.Files.Any())
|
||||
{
|
||||
<div>
|
||||
|
||||
58
Views/Vehicle/_MPGByMonthReport.cshtml
Normal file
58
Views/Vehicle/_MPGByMonthReport.cshtml
Normal 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>
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user