Compare commits

...

14 Commits

Author SHA1 Message Date
Hargata Softworks
d09d10a5b3
Merge pull request #1134 from hargata/Hargata/1133
Handle image attachments that are too long to fit in a single page.
2025-11-12 13:18:18 -07:00
Hargata Softworks
98561e7f00
Merge pull request #1139 from hargata/Hargata/136
Hargata/136 - User Households
2025-11-12 09:15:30 -07:00
DESKTOP-T0O5CDB\DESK-555BD
780856433d z 2025-11-12 09:14:44 -07:00
DESKTOP-T0O5CDB\DESK-555BD
5c895b4f82 some code cleanup 2025-11-12 09:03:21 -07:00
DESKTOP-T0O5CDB\DESK-555BD
ec52bc09e4 allow admins to manage households for all users 2025-11-11 22:23:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
c0cfd74fc6 reload garage regardless of error 2025-11-11 17:21:41 -07:00
DESKTOP-T0O5CDB\DESK-555BD
066e5ca648 household members cannot delete vehicles if they are not direct collaborators 2025-11-11 16:48:35 -07:00
DESKTOP-T0O5CDB\DESK-555BD
e30f1def1e front end for managing households 2025-11-11 16:11:39 -07:00
DESKTOP-T0O5CDB\DESK-555BD
a137fbaa52 front end for managing households 2025-11-11 16:04:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
b98585006b User Households 2025-11-11 14:45:48 -07:00
DESKTOP-T0O5CDB\DESK-555BD
4d9c5c7237 add confirmation field for admin password. 2025-11-10 08:18:05 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d4f06b96ff add exception logging for JWT validation failure 2025-11-10 08:00:37 -07:00
DESKTOP-T0O5CDB\DESK-555BD
9f14cd0a5e bump version 2025-11-09 13:10:13 -07:00
DESKTOP-T0O5CDB\DESK-555BD
d8047b07c5 Handle image attachments that are too long to fit in a single page. 2025-11-09 12:10:27 -07:00
35 changed files with 933 additions and 117 deletions

View File

@ -72,7 +72,11 @@ namespace CarCareTracker.Controllers
[HttpPost]
public IActionResult DeleteUser(int userId)
{
var result =_userLogic.DeleteAllAccessToUser(userId) && _configHelper.DeleteUserConfig(userId) && _loginLogic.DeleteUser(userId);
var result =_userLogic.DeleteAllAccessToUser(userId)
&& _configHelper.DeleteUserConfig(userId)
&& _loginLogic.DeleteUser(userId)
&& _userLogic.DeleteAllHouseholdByChildUserId(userId)
&& _userLogic.DeleteAllHouseholdByParentUserId(userId);
return Json(result);
}
[HttpPost]
@ -81,5 +85,24 @@ namespace CarCareTracker.Controllers
var result = _loginLogic.MakeUserAdmin(userId, isAdmin);
return Json(result);
}
[HttpGet]
public IActionResult GetUserHouseholdModal(int userId)
{
var households = _userLogic.GetHouseholdForParentUserId(userId);
var viewModel = new UserHouseholdAdminViewModel { Households = households, ParentUserId = userId };
return PartialView("_AdminUserHouseholdModal", viewModel);
}
[HttpPost]
public IActionResult RemoveUserFromHousehold(int parentUserId, int childUserId)
{
var result = _userLogic.DeleteUserFromHousehold(parentUserId, childUserId);
return Json(result);
}
[HttpPost]
public IActionResult AddUserToHousehold(int parentUserId, string username)
{
var result = _userLogic.AddUserToHousehold(parentUserId, username);
return Json(result);
}
}
}

View File

@ -289,6 +289,24 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name;
return PartialView("_AccountModal", new UserData() { EmailAddress = emailAddress, UserName = userName });
}
[HttpGet]
public IActionResult GetHouseholdModal()
{
var households = _userLogic.GetHouseholdForParentUserId(GetUserID());
return PartialView("_UserHouseholdModal", households);
}
[HttpPost]
public IActionResult RemoveUserFromHousehold(int userId)
{
var result = _userLogic.DeleteUserFromHousehold(GetUserID(), userId);
return Json(result);
}
[HttpPost]
public IActionResult AddUserToHousehold(string username)
{
var result = _userLogic.AddUserToHousehold(GetUserID(), username);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetRootAccountInformationModal()

View File

@ -146,6 +146,7 @@ namespace CarCareTracker.Controllers
var userAccessToken = decodedToken?.access_token ?? string.Empty;
var tokenParser = new JsonWebTokenHandler();
bool passedSignatureCheck = true;
string signatureValidationError = "check jwks endpoint";
if (!string.IsNullOrWhiteSpace(openIdConfig.JwksURL))
{
//validate token signature if jwks endpoint is provided
@ -165,6 +166,10 @@ namespace CarCareTracker.Controllers
if (!validatedIdToken.IsValid)
{
passedSignatureCheck = false;
if (validatedIdToken.Exception != null && !string.IsNullOrWhiteSpace(validatedIdToken.Exception.Message))
{
signatureValidationError = validatedIdToken.Exception.Message;
}
}
}
}
@ -238,7 +243,7 @@ namespace CarCareTracker.Controllers
}
else
{
_logger.LogError($"OpenID Provider did not provide a valid id_token: check jwks endpoint");
_logger.LogError($"OpenID Provider did not provide a valid id_token: {signatureValidationError}");
}
}
else
@ -329,6 +334,10 @@ namespace CarCareTracker.Controllers
if (!validatedIdToken.IsValid)
{
passedSignatureCheck = false;
if (validatedIdToken.Exception != null && !string.IsNullOrWhiteSpace(validatedIdToken.Exception.Message))
{
results.Add(OperationResponse.Failed($"Failed JWT Validation: {validatedIdToken.Exception.Message}"));
}
} else
{
results.Add(OperationResponse.Succeed($"Passed JWT Validation - Valid To: {validatedIdToken.SecurityToken.ValidTo}"));

View File

@ -54,7 +54,8 @@ namespace CarCareTracker.Controllers
"CREATE TABLE IF NOT EXISTS app.useraccessrecords (userId INT, vehicleId INT, PRIMARY KEY(userId, vehicleId))",
"CREATE TABLE IF NOT EXISTS app.extrafields (id INT primary key, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.inspectionrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.inspectionrecordtemplates (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)"
"CREATE TABLE IF NOT EXISTS app.inspectionrecordtemplates (id INT GENERATED BY DEFAULT AS IDENTITY primary key, vehicleId INT not null, data jsonb not null)",
"CREATE TABLE IF NOT EXISTS app.userhouseholdrecords (parentUserId INT, childUserId INT, PRIMARY KEY(parentUserId, childUserId))"
};
foreach(string cmd in cmds)
{
@ -103,6 +104,7 @@ namespace CarCareTracker.Controllers
var extrafields = new List<RecordExtraField>();
var inspectionrecords = new List<InspectionRecord>();
var inspectionrecordtemplates = new List<InspectionRecordInput>();
var userhouseholdrecords = new List<UserHousehold>();
#region "Part1"
string cmd = $"SELECT data FROM app.vehicles";
using (var ctext = pgDataSource.CreateCommand(cmd))
@ -455,6 +457,32 @@ namespace CarCareTracker.Controllers
table.Upsert(record);
};
}
cmd = $"SELECT parentUserId, childUserId FROM app.userhouseholdrecords";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
UserHousehold result = new UserHousehold()
{
Id = new HouseholdAccess
{
ParentUserId = int.Parse(reader["parentUserId"].ToString()),
ChildUserId = int.Parse(reader["childUserId"].ToString())
}
};
userhouseholdrecords.Add(result);
}
}
foreach (var record in userhouseholdrecords)
{
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<UserHousehold>("userhouseholdrecords");
table.Upsert(record);
}
;
}
#endregion
var destFilePath = $"{fullFolderPath}.zip";
ZipFile.CreateFromDirectory(fullFolderPath, destFilePath);
@ -505,6 +533,7 @@ namespace CarCareTracker.Controllers
var extrafields = new List<RecordExtraField>();
var inspectionrecords = new List<InspectionRecord>();
var inspectionrecordtemplates = new List<InspectionRecordInput>();
var userhouseholdrecords = new List<UserHousehold>();
#region "Part1"
using (var db = new LiteDatabase(fullFileName))
{
@ -816,6 +845,22 @@ namespace CarCareTracker.Controllers
ctext.ExecuteNonQuery();
}
}
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<UserHousehold>("userhouseholdrecords");
userhouseholdrecords = table.FindAll().ToList();
}
;
foreach (var record in userhouseholdrecords)
{
string cmd = $"INSERT INTO app.userhouseholdrecords (parentUserId, childUserId) VALUES(@parentUserId, @childUserId)";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", record.Id.ParentUserId);
ctext.Parameters.AddWithValue("childUserId", record.Id.ChildUserId);
ctext.ExecuteNonQuery();
}
}
#endregion
return Json(OperationResponse.Succeed("Data Imported Successfully"));
}

View File

@ -118,7 +118,8 @@ namespace CarCareTracker.Controllers
}
//get collaborators
var collaborators = _userLogic.GetCollaboratorsForVehicle(vehicleId);
viewModel.Collaborators = collaborators;
var userCanModify = _userLogic.UserCanDirectlyEditVehicle(GetUserID(), vehicleId);
viewModel.Collaborators = new VehicleCollaboratorViewModel { CanModifyCollaborators = userCanModify, Collaborators = collaborators};
//get MPG per month.
var mileageData = _gasHelper.GetGasRecordViewModels(gasRecords, userConfig.UseMPG, !vehicleData.IsElectric && userConfig.UseUKMPG);
string preferredFuelMileageUnit = _config.GetUserConfig(User).PreferredGasMileageUnit;
@ -176,16 +177,22 @@ namespace CarCareTracker.Controllers
public IActionResult GetCollaboratorsForVehicle(int vehicleId)
{
var result = _userLogic.GetCollaboratorsForVehicle(vehicleId);
return PartialView("_Collaborators", result);
var userCanModify = _userLogic.UserCanDirectlyEditVehicle(GetUserID(), vehicleId);
var viewModel = new VehicleCollaboratorViewModel
{
Collaborators = result,
CanModifyCollaborators = userCanModify
};
return PartialView("_Collaborators", viewModel);
}
[TypeFilter(typeof(CollaboratorFilter))]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] {false, true})]
[HttpPost]
public IActionResult AddCollaboratorsToVehicle(int vehicleId, string username)
{
var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username);
return Json(result);
}
[TypeFilter(typeof(CollaboratorFilter))]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { false, true })]
[HttpPost]
public IActionResult DeleteCollaboratorFromVehicle(int userId, int vehicleId)
{

View File

@ -151,8 +151,8 @@ namespace CarCareTracker.Controllers
return Json(false);
}
}
[TypeFilter(typeof(CollaboratorFilter))]
[HttpPost]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { false, true })]
public IActionResult DeleteVehicle(int vehicleId)
{
//Delete all service records, gas records, notes, etc.
@ -175,42 +175,41 @@ namespace CarCareTracker.Controllers
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic(string.Empty, "vehicle.delete", User.Identity.Name, vehicleId.ToString()));
}
return Json(result);
return Json(OperationResponse.Succeed());
}
[HttpPost]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { true, true })]
public IActionResult DeleteVehicles(List<int> vehicleIds)
{
List<bool> results = new List<bool>();
foreach(int vehicleId in vehicleIds)
{
if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleId))
foreach (int vehicleId in vehicleIds)
{
//Delete all service records, gas records, notes, etc.
var result = _gasRecordDataAccess.DeleteAllGasRecordsByVehicleId(vehicleId) &&
_serviceRecordDataAccess.DeleteAllServiceRecordsByVehicleId(vehicleId) &&
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
_noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
_planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) &&
_planRecordTemplateDataAccess.DeleteAllPlanRecordTemplatesByVehicleId(vehicleId) &&
_inspectionRecordDataAccess.DeleteAllInspectionRecordsByVehicleId(vehicleId) &&
_inspectionRecordTemplateDataAccess.DeleteAllInspectionReportTemplatesByVehicleId(vehicleId) &&
_supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) &&
_odometerRecordDataAccess.DeleteAllOdometerRecordsByVehicleId(vehicleId) &&
_userLogic.DeleteAllAccessToVehicle(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId);
if (result)
{
//Delete all service records, gas records, notes, etc.
var result = _gasRecordDataAccess.DeleteAllGasRecordsByVehicleId(vehicleId) &&
_serviceRecordDataAccess.DeleteAllServiceRecordsByVehicleId(vehicleId) &&
_collisionRecordDataAccess.DeleteAllCollisionRecordsByVehicleId(vehicleId) &&
_taxRecordDataAccess.DeleteAllTaxRecordsByVehicleId(vehicleId) &&
_noteDataAccess.DeleteAllNotesByVehicleId(vehicleId) &&
_reminderRecordDataAccess.DeleteAllReminderRecordsByVehicleId(vehicleId) &&
_upgradeRecordDataAccess.DeleteAllUpgradeRecordsByVehicleId(vehicleId) &&
_planRecordDataAccess.DeleteAllPlanRecordsByVehicleId(vehicleId) &&
_planRecordTemplateDataAccess.DeleteAllPlanRecordTemplatesByVehicleId(vehicleId) &&
_inspectionRecordDataAccess.DeleteAllInspectionRecordsByVehicleId(vehicleId) &&
_inspectionRecordTemplateDataAccess.DeleteAllInspectionReportTemplatesByVehicleId(vehicleId) &&
_supplyRecordDataAccess.DeleteAllSupplyRecordsByVehicleId(vehicleId) &&
_odometerRecordDataAccess.DeleteAllOdometerRecordsByVehicleId(vehicleId) &&
_userLogic.DeleteAllAccessToVehicle(vehicleId) &&
_dataAccess.DeleteVehicle(vehicleId);
if (result)
{
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic(string.Empty, "vehicle.delete", User.Identity.Name, vehicleId.ToString()));
}
results.Add(result);
StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.Generic(string.Empty, "vehicle.delete", User.Identity.Name, vehicleId.ToString()));
}
results.Add(result);
}
return Json(results.All(x => x));
return Json(OperationResponse.Conditional(results.Any() && results.All(x => x), "", StaticHelper.GenericErrorMessage));
}
[HttpPost]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { true, true })]
public IActionResult GetVehiclesCollaborators(List<int> vehicleIds)
{
var viewModel = new UserCollaboratorViewModel();
@ -219,10 +218,10 @@ namespace CarCareTracker.Controllers
//only one vehicle to manage
if (_userLogic.UserCanEditVehicle(GetUserID(), vehicleIds.First()))
{
viewModel.CommonCollaborators = _userLogic.GetCollaboratorsForVehicle(vehicleIds.First()).Select(x=>x.UserName).ToList();
viewModel.CommonCollaborators = _userLogic.GetCollaboratorsForVehicle(vehicleIds.First()).Select(x => x.UserName).ToList();
viewModel.VehicleIds.Add(vehicleIds.First());
}
}
}
else
{
List<UserCollaborator> allCollaborators = new List<UserCollaborator>();
@ -239,15 +238,16 @@ namespace CarCareTracker.Controllers
viewModel.CommonCollaborators = groupedCollaborations.Where(x => x.Count() == vehicleIds.Count()).Select(y => y.Key).ToList();
viewModel.PartialCollaborators = groupedCollaborations.Where(x => x.Count() != vehicleIds.Count()).Select(y => y.Key).ToList();
}
return PartialView("_UserCollaborators",viewModel);
return PartialView("_UserCollaborators", viewModel);
}
[HttpPost]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { true, true })]
public IActionResult AddCollaboratorsToVehicles(List<string> usernames, List<int> vehicleIds)
{
List<OperationResponse> results = new List<OperationResponse>();
foreach(string username in usernames)
foreach (string username in usernames)
{
foreach(int vehicleId in vehicleIds)
foreach (int vehicleId in vehicleIds)
{
var result = _userLogic.AddCollaboratorToVehicle(vehicleId, username);
results.Add(result);
@ -261,6 +261,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Succeed());
}
[HttpPost]
[TypeFilter(typeof(StrictCollaboratorFilter), Arguments = new object[] { true, true })]
public IActionResult RemoveCollaboratorsFromVehicles(List<string> usernames, List<int> vehicleIds)
{
List<OperationResponse> results = new List<OperationResponse>();
@ -279,37 +280,6 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Succeed());
}
[HttpPost]
public IActionResult DuplicateVehicleCollaborators(int sourceVehicleId, int destVehicleId)
{
try
{
//retrieve collaborators for both source and destination vehicle id.
if (_userLogic.UserCanEditVehicle(GetUserID(), sourceVehicleId) && _userLogic.UserCanEditVehicle(GetUserID(), destVehicleId))
{
var sourceCollaborators = _userLogic.GetCollaboratorsForVehicle(sourceVehicleId).Select(x => x.UserVehicle.UserId).ToList();
var destCollaborators = _userLogic.GetCollaboratorsForVehicle(destVehicleId).Select(x => x.UserVehicle.UserId).ToList();
sourceCollaborators.RemoveAll(x => destCollaborators.Contains(x));
if (sourceCollaborators.Any())
{
foreach (int collaboratorId in sourceCollaborators)
{
_userLogic.AddUserAccessToVehicle(collaboratorId, destVehicleId);
}
}
else
{
return Json(OperationResponse.Failed("Both vehicles already have identical collaborators"));
}
}
return Json(OperationResponse.Succeed("Collaborators Copied"));
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return Json(OperationResponse.Failed());
}
}
#region "Shared Methods"
[HttpPost]
@ -341,7 +311,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ServiceRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.ServiceRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
@ -354,7 +324,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.RepairRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.RepairRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
@ -367,7 +337,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.UpgradeRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.UpgradeRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
@ -393,7 +363,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.SupplyRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.SupplyRecord, Description = $"{x.Date.ToShortDateString()} - {x.Description}" }));
@ -406,7 +376,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.PlanRecord, Description = $"{x.DateCreated.ToShortDateString()} - {x.Description}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.PlanRecord, Description = $"{x.DateCreated.ToShortDateString()} - {x.Description}" }));
@ -419,7 +389,7 @@ namespace CarCareTracker.Controllers
if (caseSensitive)
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.OdometerRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" }));
}
}
else
{
searchResults.AddRange(results.Where(x => JsonSerializer.Serialize(x).ToLower().Contains(searchQuery)).Select(x => new SearchResult { Id = x.Id, RecordType = ImportMode.OdometerRecord, Description = $"{x.Date.ToShortDateString()} - {x.Mileage}" }));

View File

@ -65,7 +65,7 @@ namespace CarCareTracker.External.Implementations
return true;
}
/// <summary>
/// Delee all access records when a user is deleted.
/// Delete all access records when a user is deleted.
/// </summary>
/// <param name="userId"></param>
/// <returns></returns>

View File

@ -0,0 +1,76 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
namespace CarCareTracker.External.Implementations
{
public class UserHouseholdDataAccess : IUserHouseholdDataAccess
{
private ILiteDBHelper _liteDB { get; set; }
private static string tableName = "userhouseholdrecords";
public UserHouseholdDataAccess(ILiteDBHelper liteDB)
{
_liteDB = liteDB;
}
public List<UserHousehold> GetUserHouseholdByParentUserId(int parentUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
return table.Find(x => x.Id.ParentUserId == parentUserId).ToList();
}
public List<UserHousehold> GetUserHouseholdByChildUserId(int childUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
return table.Find(x => x.Id.ChildUserId == childUserId).ToList();
}
public UserHousehold GetUserHouseholdByParentAndChildUserId(int parentUserId, int childUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
return table.Find(x => x.Id.ParentUserId == parentUserId && x.Id.ChildUserId == childUserId).FirstOrDefault();
}
public bool SaveUserHousehold(UserHousehold userHousehold)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
table.Upsert(userHousehold);
db.Checkpoint();
return true;
}
public bool DeleteUserHousehold(int parentUserId, int childUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
table.DeleteMany(x => x.Id.ParentUserId == parentUserId && x.Id.ChildUserId == childUserId);
db.Checkpoint();
return true;
}
/// <summary>
/// Delete all household records when a parent user is deleted.
/// </summary>
/// <param name="parentUserId"></param>
/// <returns></returns>
public bool DeleteAllHouseholdRecordsByParentUserId(int parentUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
table.DeleteMany(x => x.Id.ParentUserId == parentUserId);
db.Checkpoint();
return true;
}
/// <summary>
/// Delete all household records when a child user is deleted.
/// </summary>
/// <param name="childUserId"></param>
/// <returns></returns>
public bool DeleteAllHouseholdRecordsByChildUserId(int childUserId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<UserHousehold>(tableName);
table.DeleteMany(x => x.Id.ChildUserId == childUserId);
db.Checkpoint();
return true;
}
}
}

View File

@ -0,0 +1,207 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Npgsql;
namespace CarCareTracker.External.Implementations
{
public class PGUserHouseholdDataAccess : IUserHouseholdDataAccess
{
private NpgsqlDataSource pgDataSource;
private readonly ILogger<PGUserHouseholdDataAccess> _logger;
private static string tableName = "userhouseholdrecords";
public PGUserHouseholdDataAccess(IConfiguration config, ILogger<PGUserHouseholdDataAccess> logger)
{
pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]);
_logger = logger;
try
{
//create table if not exist.
string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (parentUserId INT, childUserId INT, PRIMARY KEY(parentUserId, childUserId))";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
public List<UserHousehold> GetUserHouseholdByParentUserId(int parentUserId)
{
try
{
string cmd = $"SELECT parentUserId, childUserId FROM app.{tableName} WHERE parentUserId = @parentUserId";
var results = new List<UserHousehold>();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", parentUserId);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
UserHousehold result = new UserHousehold()
{
Id = new HouseholdAccess
{
ParentUserId = int.Parse(reader["parentUserId"].ToString()),
ChildUserId = int.Parse(reader["childUserId"].ToString())
}
};
results.Add(result);
}
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new List<UserHousehold>();
}
}
public List<UserHousehold> GetUserHouseholdByChildUserId(int childUserId)
{
try
{
string cmd = $"SELECT parentUserId, childUserId FROM app.{tableName} WHERE childUserId = @childUserId";
var results = new List<UserHousehold>();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("childUserId", childUserId);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
UserHousehold result = new UserHousehold()
{
Id = new HouseholdAccess
{
ParentUserId = int.Parse(reader["parentUserId"].ToString()),
ChildUserId = int.Parse(reader["childUserId"].ToString())
}
};
results.Add(result);
}
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new List<UserHousehold>();
}
}
public UserHousehold GetUserHouseholdByParentAndChildUserId(int parentUserId, int childUserId)
{
try
{
string cmd = $"SELECT parentUserId, childUserId FROM app.{tableName} WHERE parentUserId = @parentUserId AND childUserId = @childUserId";
UserHousehold result = null;
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", parentUserId);
ctext.Parameters.AddWithValue("childUserId", childUserId);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
result = new UserHousehold()
{
Id = new HouseholdAccess
{
ParentUserId = int.Parse(reader["parentUserId"].ToString()),
ChildUserId = int.Parse(reader["childUserId"].ToString())
}
};
return result;
}
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new UserHousehold();
}
}
public bool SaveUserHousehold(UserHousehold userHousehold)
{
try
{
string cmd = $"INSERT INTO app.{tableName} (parentUserId, childUserId) VALUES(@parentUserId, @childUserId)";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", userHousehold.Id.ParentUserId);
ctext.Parameters.AddWithValue("childUserId", userHousehold.Id.ChildUserId);
return ctext.ExecuteNonQuery() > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteUserHousehold(int parentUserId, int childUserId)
{
try
{
string cmd = $"DELETE FROM app.{tableName} WHERE parentUserId = @parentUserId AND childUserId = @childUserId";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", parentUserId);
ctext.Parameters.AddWithValue("childUserId", childUserId);
return ctext.ExecuteNonQuery() > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
/// <summary>
/// Delete all household records when a parent user is deleted.
/// </summary>
/// <param name="parentUserId"></param>
/// <returns></returns>
public bool DeleteAllHouseholdRecordsByParentUserId(int parentUserId)
{
try
{
string cmd = $"DELETE FROM app.{tableName} WHERE parentUserId = @parentUserId";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("parentUserId", parentUserId);
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
/// <summary>
/// Delete all household records when a child user is deleted.
/// </summary>
/// <param name="childUserId"></param>
/// <returns></returns>
public bool DeleteAllHouseholdRecordsByChildUserId(int childUserId)
{
try
{
string cmd = $"DELETE FROM app.{tableName} WHERE childUserId = @childUserId";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("childUserId", childUserId);
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}

View File

@ -0,0 +1,15 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IUserHouseholdDataAccess
{
List<UserHousehold> GetUserHouseholdByParentUserId(int parentUserId);
List<UserHousehold> GetUserHouseholdByChildUserId(int childUserId);
UserHousehold GetUserHouseholdByParentAndChildUserId(int parentUserId, int childUserId);
bool SaveUserHousehold(UserHousehold userHousehold);
bool DeleteUserHousehold(int parentUserId, int childUserId);
bool DeleteAllHouseholdRecordsByParentUserId(int parentUserId);
bool DeleteAllHouseholdRecordsByChildUserId(int childUserId);
}
}

View File

@ -0,0 +1,71 @@
using CarCareTracker.Helper;
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System.Security.Claims;
namespace CarCareTracker.Filter
{
public class StrictCollaboratorFilter: ActionFilterAttribute
{
private readonly IUserLogic _userLogic;
private readonly IConfigHelper _config;
private readonly bool _multiple;
private readonly bool _jsonResponse;
public StrictCollaboratorFilter(IUserLogic userLogic, IConfigHelper config, bool? multiple = false, bool? jsonResponse = false) {
_userLogic = userLogic;
_config = config;
_multiple = multiple ?? false;
_jsonResponse = jsonResponse ?? false;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.IsInRole(nameof(UserData.IsRootUser)))
{
List<int> vehicleIds = new List<int>();
if (!_multiple && filterContext.ActionArguments.ContainsKey("vehicleId"))
{
vehicleIds.Add(int.Parse(filterContext.ActionArguments["vehicleId"].ToString()));
}
else if (_multiple && filterContext.ActionArguments.ContainsKey("vehicleIds"))
{
vehicleIds.AddRange(filterContext.ActionArguments["vehicleIds"] as List<int>);
}
if (vehicleIds.Any())
{
foreach (int vehicleId in vehicleIds)
{
if (vehicleId != default)
{
var userId = int.Parse(filterContext.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier));
if (!_userLogic.UserCanDirectlyEditVehicle(userId, vehicleId))
{
filterContext.Result = _jsonResponse ? new JsonResult(OperationResponse.Failed("Access Denied")) : new RedirectResult("/Error/Unauthorized");
}
}
else
{
var shopSupplyEndpoints = new List<string> { "ImportToVehicleIdFromCsv", "GetSupplyRecordsByVehicleId", "ExportFromVehicleToCsv" };
if (shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()) && !_config.GetServerEnableShopSupplies())
{
//user trying to access shop supplies but shop supplies is not enabled by root user.
filterContext.Result = _jsonResponse ? new JsonResult(OperationResponse.Failed("Access Denied")) : new RedirectResult("/Error/Unauthorized");
}
else if (!shopSupplyEndpoints.Contains(filterContext.RouteData.Values["action"].ToString()))
{
//user trying to access any other endpoints using 0 as vehicle id.
filterContext.Result = _jsonResponse ? new JsonResult(OperationResponse.Failed("Access Denied")) : new RedirectResult("/Error/Unauthorized");
}
}
}
}
else
{
filterContext.Result = _jsonResponse ? new JsonResult(OperationResponse.Failed("Access Denied")) : new RedirectResult("/Error/Unauthorized");
}
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace CarCareTracker.Helper
/// </summary>
public static class StaticHelper
{
public const string VersionNumber = "1.5.4";
public const string VersionNumber = "1.5.5";
public const string DbName = "data/cartracker.db";
public const string UserConfigPath = "data/config/userConfig.json";
public const string ServerConfigPath = "data/config/serverConfig.json";

View File

@ -1,5 +1,4 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
namespace CarCareTracker.Logic
@ -13,17 +12,26 @@ namespace CarCareTracker.Logic
OperationResponse AddCollaboratorToVehicle(int vehicleId, string username);
List<Vehicle> FilterUserVehicles(List<Vehicle> results, int userId);
bool UserCanEditVehicle(int userId, int vehicleId);
bool UserCanDirectlyEditVehicle(int userId, int vehicleId);
bool DeleteAllAccessToVehicle(int vehicleId);
bool DeleteAllAccessToUser(int userId);
List<UserHouseholdViewModel> GetHouseholdForParentUserId(int parentUserId);
OperationResponse AddUserToHousehold(int parentUserId, string childUsername);
bool DeleteUserFromHousehold(int parentUserId, int childUserId);
bool DeleteAllHouseholdByParentUserId(int parentUserId);
bool DeleteAllHouseholdByChildUserId(int childUserId);
}
public class UserLogic: IUserLogic
{
private readonly IUserAccessDataAccess _userAccess;
private readonly IUserRecordDataAccess _userData;
private readonly IUserHouseholdDataAccess _userHouseholdData;
public UserLogic(IUserAccessDataAccess userAccess,
IUserRecordDataAccess userData) {
IUserRecordDataAccess userData,
IUserHouseholdDataAccess userHouseholdData) {
_userAccess = userAccess;
_userData = userData;
_userHouseholdData = userHouseholdData;
}
public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId)
{
@ -108,10 +116,24 @@ namespace CarCareTracker.Logic
{
return results;
}
var accessibleVehicles = _userAccess.GetUserAccessByUserId(userId);
if (accessibleVehicles.Any())
List<int> userIds = new List<int> { userId };
List<int> vehicleIds = new List<int>();
var userHouseholds = _userHouseholdData.GetUserHouseholdByChildUserId(userId);
if (userHouseholds.Any())
{
//add parent's user ids
userIds.AddRange(userHouseholds.Select(x => x.Id.ParentUserId));
}
foreach(int userIdToCheck in userIds)
{
var accessibleVehicles = _userAccess.GetUserAccessByUserId(userIdToCheck);
if (accessibleVehicles.Any())
{
vehicleIds.AddRange(accessibleVehicles.Select(x => x.Id.VehicleId));
}
}
if (vehicleIds.Any())
{
var vehicleIds = accessibleVehicles.Select(x => x.Id.VehicleId);
return results.Where(x => vehicleIds.Contains(x.Id)).ToList();
}
else
@ -120,13 +142,36 @@ namespace CarCareTracker.Logic
}
}
public bool UserCanEditVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
List<int> userIds = new List<int> { userId };
var userHouseholds = _userHouseholdData.GetUserHouseholdByChildUserId(userId);
if (userHouseholds.Any())
{
//add parent's user ids
userIds.AddRange(userHouseholds.Select(x => x.Id.ParentUserId));
}
foreach (int userIdToCheck in userIds)
{
var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(userIdToCheck, vehicleId);
if (userAccess != null && userAccess.Id.UserId == userIdToCheck && userAccess.Id.VehicleId == vehicleId)
{
return true;
}
}
return false;
}
public bool UserCanDirectlyEditVehicle(int userId, int vehicleId)
{
if (userId == -1)
{
return true;
}
var userAccess = _userAccess.GetUserAccessByVehicleAndUserId(userId, vehicleId);
if (userAccess != null)
if (userAccess != null && userAccess.Id.UserId == userId && userAccess.Id.VehicleId == vehicleId)
{
return true;
}
@ -142,5 +187,74 @@ namespace CarCareTracker.Logic
var result = _userAccess.DeleteAllAccessRecordsByUserId(userId);
return result;
}
public List<UserHouseholdViewModel> GetHouseholdForParentUserId(int parentUserId)
{
var result = _userHouseholdData.GetUserHouseholdByParentUserId(parentUserId);
var convertedResult = new List<UserHouseholdViewModel>();
//convert useraccess to usercollaborator
foreach (UserHousehold userHouseholdAccess in result)
{
var userCollaborator = new UserHouseholdViewModel
{
UserName = _userData.GetUserRecordById(userHouseholdAccess.Id.ChildUserId).UserName,
UserHousehold = userHouseholdAccess.Id
};
convertedResult.Add(userCollaborator);
}
return convertedResult;
}
public OperationResponse AddUserToHousehold(int parentUserId, string childUsername)
{
//attempting to add to root user
if (parentUserId == -1)
{
return OperationResponse.Failed("Root user household not allowed");
}
//try to find existing user.
var existingUser = _userData.GetUserRecordByUserName(childUsername);
if (existingUser.Id != default)
{
//user exists.
//check if user is trying to add themselves
if (parentUserId == existingUser.Id)
{
return OperationResponse.Failed("Cannot add user to their own household");
}
//check if user already belongs to the household
var householdAccess = _userHouseholdData.GetUserHouseholdByParentAndChildUserId(parentUserId, existingUser.Id);
if (householdAccess != null && householdAccess.Id.ChildUserId == existingUser.Id && householdAccess.Id.ParentUserId == parentUserId)
{
return OperationResponse.Failed("User already belongs to this household");
}
//check if a circular dependency will exist
var circularHouseholdAccess = _userHouseholdData.GetUserHouseholdByParentAndChildUserId(existingUser.Id, parentUserId);
if (circularHouseholdAccess != null && circularHouseholdAccess.Id.ChildUserId == parentUserId && circularHouseholdAccess.Id.ParentUserId == existingUser.Id)
{
return OperationResponse.Failed("Circular dependency is not allowed");
}
var result = _userHouseholdData.SaveUserHousehold(new UserHousehold { Id = new HouseholdAccess { ParentUserId = parentUserId, ChildUserId = existingUser.Id} });
if (result)
{
return OperationResponse.Succeed("User Added to Household");
}
return OperationResponse.Failed();
}
return OperationResponse.Failed($"Unable to find user {childUsername} in the system");
}
public bool DeleteUserFromHousehold(int parentUserId, int childUserId)
{
var result = _userHouseholdData.DeleteUserHousehold(parentUserId, childUserId);
return result;
}
public bool DeleteAllHouseholdByParentUserId(int parentUserId)
{
var result = _userHouseholdData.DeleteAllHouseholdRecordsByParentUserId(parentUserId);
return result;
}
public bool DeleteAllHouseholdByChildUserId(int childUserId)
{
var result = _userHouseholdData.DeleteAllHouseholdRecordsByChildUserId(childUserId);
return result;
}
}
}

View File

@ -8,7 +8,7 @@
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>();
public VehicleCollaboratorViewModel Collaborators { get; set; } = new VehicleCollaboratorViewModel();
public bool CustomWidgetsConfigured { get; set; } = false;
public List<ImportMode> AvailableMetrics { get; set; } = new List<ImportMode>();
public bool HasVehicleImageMap { get; set; } = false;

View File

@ -6,4 +6,4 @@
public List<string> CommonCollaborators { get; set; } = new List<string>();
public List<string> PartialCollaborators { get; set; } = new List<string>();
}
}
}

View File

@ -0,0 +1,12 @@
namespace CarCareTracker.Models
{
public class HouseholdAccess
{
public int ParentUserId { get; set; }
public int ChildUserId { get; set; }
}
public class UserHousehold
{
public HouseholdAccess Id { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class UserHouseholdAdminViewModel
{
public List<UserHouseholdViewModel> Households { get; set; }
public int ParentUserId { get; set; }
}
}

View File

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

View File

@ -0,0 +1,8 @@
namespace CarCareTracker.Models
{
public class VehicleCollaboratorViewModel
{
public List<UserCollaborator> Collaborators { get; set; }
public bool CanModifyCollaborators { get; set; } = true;
}
}

View File

@ -60,6 +60,7 @@ if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){
builder.Services.AddSingleton<IExtraFieldDataAccess, PGExtraFieldDataAccess>();
builder.Services.AddSingleton<IInspectionRecordDataAccess, PGInspectionRecordDataAccess>();
builder.Services.AddSingleton<IInspectionRecordTemplateDataAccess, PGInspectionRecordTemplateDataAccess>();
builder.Services.AddSingleton<IUserHouseholdDataAccess, PGUserHouseholdDataAccess>();
}
else
{
@ -82,6 +83,7 @@ else
builder.Services.AddSingleton<IExtraFieldDataAccess, ExtraFieldDataAccess>();
builder.Services.AddSingleton<IInspectionRecordDataAccess, InspectionRecordDataAccess>();
builder.Services.AddSingleton<IInspectionRecordTemplateDataAccess, InspectionRecordTemplateDataAccess>();
builder.Services.AddSingleton<IUserHouseholdDataAccess, UserHouseholdDataAccess>();
}
//configure helpers

View File

@ -95,6 +95,12 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="householdModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content" id="householdModalContent">
</div>
</div>
</div>
</div>
<script>
function showTokenModal() {
@ -140,6 +146,7 @@
});
}
function deleteUser(userId) {
event.stopPropagation();
Swal.fire({
title: "Confirm Deletion?",
text: "Deleted Users cannot be restored.",
@ -186,4 +193,48 @@
}
});
}
function loadUserHousehold(userId){
$.get(`/Admin/GetUserHouseholdModal?userId=${userId}`, function(data){
$('#householdModalContent').html(data);
$('#householdModal').modal('show');
})
}
function adminRemoveUserFromHousehold(parentUserId, childUserId){
$.post('/Admin/RemoveUserFromHousehold', {parentUserId: parentUserId, childUserId: childUserId}, function(data){
if (data) {
successToast('User Removed');
loadUserHousehold(parentUserId);
} else {
errorToast(genericErrorMessage())
}
});
}
function adminAddUserToHousehold(parentUserId){
Swal.fire({
title: 'Add User',
html: `
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username" onkeydown="handleSwalEnter(event)">
`,
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) {
$.post('/Admin/AddUserToHousehold', { parentUserId: parentUserId, username: result.value.userName }, function (data) {
if (data.success) {
loadUserHousehold(parentUserId);
successToast('User Added');
} else {
errorToast(data.message);
}
});
}
});
}
</script>

View File

@ -0,0 +1,40 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model UserHouseholdAdminViewModel
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<div class="d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Manage Household")</span>
</div>
<div class="d-flex align-items-center ms-auto">
<button onclick="adminAddUserToHousehold(@Model.ParentUserId)" class="btn btn-primary">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add User")
</button>
<button type="button" class="btn-close ms-2" onclick="hideHouseholdModal()" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-10">@translator.Translate(userLanguage, "Username")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody id="tokenTable">
@foreach(UserHouseholdViewModel viewModel in Model.Households)
{
<tr class="d-flex">
<td class="col-10">@viewModel.UserName</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="adminRemoveUserFromHousehold(@viewModel.UserHousehold.ParentUserId, @viewModel.UserHousehold.ChildUserId)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -2,10 +2,10 @@
@model List<UserData>
@foreach (UserData userData in Model)
{
<tr class="d-flex" style="cursor:pointer;">
<tr class="d-flex" style="cursor:pointer;" onclick="loadUserHousehold(@userData.Id)">
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.UserName)</td>
<td class="col-4 d-flex align-items-center">@StaticHelper.TruncateStrings(userData.EmailAddress)</td>
<td class="col-2 d-flex align-items-center"><input class="form-check-input" type="checkbox" value="" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "") /></td>
<td class="col-2 d-flex align-items-center"><input class="form-check-input" type="checkbox" value="" onclick="noPropagation()" onchange="updateUserAdmin(@userData.Id, this)" @(userData.IsAdmin ? "checked" : "") /></td>
<td class="col-2 d-flex align-items-center"><button type="button" class="btn btn-danger" onclick="deleteUser(@userData.Id, this)"><i class="bi bi-trash"></i></button></td>
</tr>
}

View File

@ -68,7 +68,11 @@
<li>
<button class="dropdown-item" onclick="showAccountInformationModal()"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</button>
</li>
<li>
<button class="dropdown-item" onclick="showHouseholdModal()"><i class="bi bi-house me-2"></i>@translator.Translate(userLanguage, "Household")</button>
</li>
}
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item" onclick="performLogOut()"><i class="bi bi-box-arrow-right me-2"></i>@translator.Translate(userLanguage, "Logout")</button>
</li>
@ -121,6 +125,9 @@
<li>
<button class="nav-link" onclick="showAccountInformationModal()"><span class="display-3 ms-2"><i class="bi bi-person-gear me-2"></i>@translator.Translate(userLanguage, "Profile")</span></button>
</li>
<li>
<button class="nav-link" onclick="showHouseholdModal()"><span class="display-3 ms-2"><i class="bi bi-house me-2"></i>@translator.Translate(userLanguage, "Household")</span></button>
</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>@translator.Translate(userLanguage,"Logout")</span></button>
@ -159,6 +166,12 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="householdModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content" id="householdModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="attachmentPreviewModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-fullscreen" role="document">
<div class="modal-content frosted" id="attachmentPreviewModalContent">

View File

@ -406,15 +406,20 @@
title: 'Setup Credentials',
html: `
<input type="text" id="authUsername" class="swal2-input" placeholder="Username">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password" onkeydown="handleSwalEnter(event)">
<input type="password" id="authPassword" class="swal2-input" placeholder="Password">
<input type="password" id="authPasswordVerify" class="swal2-input" placeholder="Confirm Password" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Setup',
focusConfirm: false,
preConfirm: () => {
const username = $("#authUsername").val();
const password = $("#authPassword").val();
const passwordConfirm = $("#authPasswordVerify").val();
if (!username || !password) {
Swal.showValidationMessage(`Please enter username and password`)
Swal.showValidationMessage(`Please enter username and password`);
}
if (password != passwordConfirm){
Swal.showValidationMessage(`Passwords must match`);
}
return { username, password }
},

View File

@ -0,0 +1,40 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<UserHouseholdViewModel>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<div class="d-flex align-items-center">
<span class="lead">@translator.Translate(userLanguage, "Manage Household")</span>
</div>
<div class="d-flex align-items-center ms-auto">
<button onclick="addUserToHousehold()" class="btn btn-primary">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add User")
</button>
<button type="button" class="btn-close ms-2" onclick="hideHouseholdModal()" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-10">@translator.Translate(userLanguage, "Username")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Delete")</th>
</tr>
</thead>
<tbody id="tokenTable">
@foreach(UserHouseholdViewModel viewModel in Model)
{
<tr class="d-flex">
<td class="col-10">@viewModel.UserName</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="removeUserFromHousehold(@viewModel.UserHousehold.ChildUserId, this)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>

View File

@ -5,34 +5,43 @@
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
@model List<UserCollaborator>
@model VehicleCollaboratorViewModel
<div class="row">
<div class="col-8">
<div class="@(Model.CanModifyCollaborators ? "col-8" : "col-12")">
<span class="lead">@translator.Translate(userLanguage, "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>
@if (Model.CanModifyCollaborators)
{
<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">@translator.Translate(userLanguage, "Username")</th>
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Delete")</th>
<th scope="col" class="@(Model.CanModifyCollaborators ? "col-8" : "col-12")">@translator.Translate(userLanguage, "Username")</th>
@if (Model.CanModifyCollaborators)
{
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Delete")</th>
}
</tr>
</thead>
<tbody>
@foreach (UserCollaborator user in Model)
@foreach (UserCollaborator user in Model.Collaborators)
{
<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>
<td class="@(Model.CanModifyCollaborators ? "col-8" : "col-12")">@user.UserName</td>
@if (Model.CanModifyCollaborators)
{
<td class="col-4">
@if (User.Identity.Name != user.UserName && Model.CanModifyCollaborators)
{
<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>

View File

@ -168,7 +168,7 @@
</div>
<hr />
}
<div class="row flex-grow-1 flex-shrink-1">
<div class="row stickerNoteContainer">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(genericRecord.Notes)
@ -264,7 +264,7 @@
}
</div>
<hr />
<div class="row flex-grow-1 flex-shrink-1">
<div class="row stickerNoteContainer">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(supplyRecord.Notes)

View File

@ -105,4 +105,4 @@
@:vehiclesToEdit.push(@recordId);
}
adjustCollaboratorsModalSize(@showTwoColumns.ToString().ToLower());
</script>
</script>

View File

@ -275,7 +275,7 @@
</div>
<hr />
}
<div class="row flex-grow-1 flex-shrink-1">
<div class="row stickerNoteContainer">
<div class="col-12">
<div class="stickerNote ms-1 me-1 p-1">
@(genericRecord.Notes)

View File

@ -101,12 +101,13 @@ html {
page-break-after: always;
}
.recordSticker {
height: 100%;
page-break-after: always;
}
.stickerNote {
height:100%;
}
.stickerNoteContainer {
max-height:100vh;
max-width:100vw;
}
body {
background-color: #fff !important;

File diff suppressed because one or more lines are too long

View File

@ -365,9 +365,12 @@ function deleteVehicles(vehicleIds) {
}).then((result) => {
if (result.isConfirmed) {
$.post('/Vehicle/DeleteVehicles', { vehicleIds: vehicleIds }, function (data) {
if (data) {
if (data.success) {
loadGarage();
}
else {
errorToast(data.message);
}
})
}
});
@ -377,7 +380,9 @@ function manageCollaborators(vehicleIds) {
return;
}
$.post('/Vehicle/GetVehiclesCollaborators', { vehicleIds: vehicleIds }, function (data) {
if (data) {
if (isOperationResponse(data)) {
return;
} else if (data) {
$("#userCollaboratorsModalContent").html(data);
$("#userCollaboratorsModal").modal('show');
}
@ -576,6 +581,54 @@ function sortVehicles(desc) {
sortedRow.push($('.garage-item-add'))
$('.vehiclesContainer').html(sortedRow);
}
function showHouseholdModal() {
$.get('/Home/GetHouseholdModal', function (data) {
$("#householdModalContent").html(data);
$("#householdModal").modal('show');
})
}
function hideHouseholdModal() {
$("#householdModal").modal('hide');
}
function removeUserFromHousehold(userId) {
$.post('/Home/RemoveUserFromHousehold', { userId: userId }, function (data) {
if (data) {
successToast('User Removed');
showHouseholdModal();
} else {
errorToast(genericErrorMessage())
}
})
}
function addUserToHousehold() {
Swal.fire({
title: 'Add User',
html: `
<input type="text" id="inputUserName" class="swal2-input" placeholder="Username" onkeydown="handleSwalEnter(event)">
`,
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) {
$.post('/Home/AddUserToHousehold', { username: result.value.userName }, function (data) {
if (data.success) {
showHouseholdModal();
successToast('User Added');
} else {
errorToast(data.message);
}
});
}
});
}
function showAccountInformationModal() {
$.get('/Home/GetUserAccountInformationModal', function (data) {
$('#accountInformationModalContent').html(data);

View File

@ -1812,4 +1812,13 @@ function handleAttachmentCopyLink(e) {
let textToCopy = $(e).attr('data-link');
navigator.clipboard.writeText(textToCopy);
successToast("Copied Link to Clipboard");
}
function isOperationResponse(result) {
//checks if response from controller is operationresponse
if (result.success != undefined && result.message != undefined) {
if (!result.success) {
errorToast(result.message);
}
return true;
}
}

View File

@ -219,8 +219,10 @@ function deleteVehicle(vehicleId) {
}).then((result) => {
if (result.isConfirmed) {
$.post('/Vehicle/DeleteVehicle', { vehicleId: vehicleId }, function (data) {
if (data) {
if (data.success) {
window.location.href = '/Home';
} else {
errorToast(data.message);
}
})
}