diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index 6e911b1..5f4742e 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -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); + } } } diff --git a/Controllers/HomeController.cs b/Controllers/HomeController.cs index 16f42ce..64d8b1c 100644 --- a/Controllers/HomeController.cs +++ b/Controllers/HomeController.cs @@ -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() diff --git a/Controllers/LoginController.cs b/Controllers/LoginController.cs index d58b834..c4811ea 100644 --- a/Controllers/LoginController.cs +++ b/Controllers/LoginController.cs @@ -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}")); diff --git a/Controllers/MigrationController.cs b/Controllers/MigrationController.cs index a66d95f..8acb149 100644 --- a/Controllers/MigrationController.cs +++ b/Controllers/MigrationController.cs @@ -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(); var inspectionrecords = new List(); var inspectionrecordtemplates = new List(); + var userhouseholdrecords = new List(); #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("userhouseholdrecords"); + table.Upsert(record); + } + ; + } #endregion var destFilePath = $"{fullFolderPath}.zip"; ZipFile.CreateFromDirectory(fullFolderPath, destFilePath); @@ -505,6 +533,7 @@ namespace CarCareTracker.Controllers var extrafields = new List(); var inspectionrecords = new List(); var inspectionrecordtemplates = new List(); + var userhouseholdrecords = new List(); #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("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")); } diff --git a/Controllers/Vehicle/ReportController.cs b/Controllers/Vehicle/ReportController.cs index a2626be..e962716 100644 --- a/Controllers/Vehicle/ReportController.cs +++ b/Controllers/Vehicle/ReportController.cs @@ -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) { diff --git a/Controllers/VehicleController.cs b/Controllers/VehicleController.cs index f509d02..00b390b 100644 --- a/Controllers/VehicleController.cs +++ b/Controllers/VehicleController.cs @@ -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 vehicleIds) { List results = new List(); - 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 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 allCollaborators = new List(); @@ -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 usernames, List vehicleIds) { List results = new List(); - 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 usernames, List vehicleIds) { List results = new List(); @@ -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}" })); diff --git a/External/Implementations/Litedb/UserAccessDataAcces.cs b/External/Implementations/Litedb/UserAccessDataAcces.cs index 15fa08e..6dedc74 100644 --- a/External/Implementations/Litedb/UserAccessDataAcces.cs +++ b/External/Implementations/Litedb/UserAccessDataAcces.cs @@ -65,7 +65,7 @@ namespace CarCareTracker.External.Implementations return true; } /// - /// Delee all access records when a user is deleted. + /// Delete all access records when a user is deleted. /// /// /// diff --git a/External/Implementations/Litedb/UserHouseholdDataAcces.cs b/External/Implementations/Litedb/UserHouseholdDataAcces.cs new file mode 100644 index 0000000..e0d530a --- /dev/null +++ b/External/Implementations/Litedb/UserHouseholdDataAcces.cs @@ -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 GetUserHouseholdByParentUserId(int parentUserId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + return table.Find(x => x.Id.ParentUserId == parentUserId).ToList(); + } + public List GetUserHouseholdByChildUserId(int childUserId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(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(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(tableName); + table.Upsert(userHousehold); + db.Checkpoint(); + return true; + } + public bool DeleteUserHousehold(int parentUserId, int childUserId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + table.DeleteMany(x => x.Id.ParentUserId == parentUserId && x.Id.ChildUserId == childUserId); + db.Checkpoint(); + return true; + } + /// + /// Delete all household records when a parent user is deleted. + /// + /// + /// + public bool DeleteAllHouseholdRecordsByParentUserId(int parentUserId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + table.DeleteMany(x => x.Id.ParentUserId == parentUserId); + db.Checkpoint(); + return true; + } + /// + /// Delete all household records when a child user is deleted. + /// + /// + /// + public bool DeleteAllHouseholdRecordsByChildUserId(int childUserId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + table.DeleteMany(x => x.Id.ChildUserId == childUserId); + db.Checkpoint(); + return true; + } + } +} \ No newline at end of file diff --git a/External/Implementations/Postgres/UserHouseholdDataAccess.cs b/External/Implementations/Postgres/UserHouseholdDataAccess.cs new file mode 100644 index 0000000..74b6337 --- /dev/null +++ b/External/Implementations/Postgres/UserHouseholdDataAccess.cs @@ -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 _logger; + private static string tableName = "userhouseholdrecords"; + public PGUserHouseholdDataAccess(IConfiguration config, ILogger 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 GetUserHouseholdByParentUserId(int parentUserId) + { + try + { + string cmd = $"SELECT parentUserId, childUserId FROM app.{tableName} WHERE parentUserId = @parentUserId"; + var results = new List(); + 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(); + } + } + public List GetUserHouseholdByChildUserId(int childUserId) + { + try + { + string cmd = $"SELECT parentUserId, childUserId FROM app.{tableName} WHERE childUserId = @childUserId"; + var results = new List(); + 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(); + } + } + 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; + } + } + /// + /// Delete all household records when a parent user is deleted. + /// + /// + /// + 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; + } + } + /// + /// Delete all household records when a child user is deleted. + /// + /// + /// + 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; + } + } + } +} \ No newline at end of file diff --git a/External/Interfaces/IUserHouseholdDataAccess.cs b/External/Interfaces/IUserHouseholdDataAccess.cs new file mode 100644 index 0000000..585bf3f --- /dev/null +++ b/External/Interfaces/IUserHouseholdDataAccess.cs @@ -0,0 +1,15 @@ +using CarCareTracker.Models; + +namespace CarCareTracker.External.Interfaces +{ + public interface IUserHouseholdDataAccess + { + List GetUserHouseholdByParentUserId(int parentUserId); + List 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); + } +} \ No newline at end of file diff --git a/Filter/StrictCollaboratorFilter.cs b/Filter/StrictCollaboratorFilter.cs new file mode 100644 index 0000000..5746543 --- /dev/null +++ b/Filter/StrictCollaboratorFilter.cs @@ -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 vehicleIds = new List(); + 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); + } + + 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 { "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"); + } + } + } + } +} \ No newline at end of file diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs index bdf22be..65ef7fa 100644 --- a/Helper/StaticHelper.cs +++ b/Helper/StaticHelper.cs @@ -12,7 +12,7 @@ namespace CarCareTracker.Helper /// 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"; diff --git a/Logic/UserLogic.cs b/Logic/UserLogic.cs index 59a6f9f..bd90dc2 100644 --- a/Logic/UserLogic.cs +++ b/Logic/UserLogic.cs @@ -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 FilterUserVehicles(List results, int userId); bool UserCanEditVehicle(int userId, int vehicleId); + bool UserCanDirectlyEditVehicle(int userId, int vehicleId); bool DeleteAllAccessToVehicle(int vehicleId); bool DeleteAllAccessToUser(int userId); + List 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 GetCollaboratorsForVehicle(int vehicleId) { @@ -108,10 +116,24 @@ namespace CarCareTracker.Logic { return results; } - var accessibleVehicles = _userAccess.GetUserAccessByUserId(userId); - if (accessibleVehicles.Any()) + List userIds = new List { userId }; + List vehicleIds = new List(); + 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 userIds = new List { 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 GetHouseholdForParentUserId(int parentUserId) + { + var result = _userHouseholdData.GetUserHouseholdByParentUserId(parentUserId); + var convertedResult = new List(); + //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; + } } } diff --git a/Models/Report/ReportViewModel.cs b/Models/Report/ReportViewModel.cs index c2c6343..813bffd 100644 --- a/Models/Report/ReportViewModel.cs +++ b/Models/Report/ReportViewModel.cs @@ -8,7 +8,7 @@ public CostMakeUpForVehicle CostMakeUpForVehicle { get; set; } = new CostMakeUpForVehicle(); public ReminderMakeUpForVehicle ReminderMakeUpForVehicle { get; set; } = new ReminderMakeUpForVehicle(); public List Years { get; set; } = new List(); - public List Collaborators { get; set; } = new List(); + public VehicleCollaboratorViewModel Collaborators { get; set; } = new VehicleCollaboratorViewModel(); public bool CustomWidgetsConfigured { get; set; } = false; public List AvailableMetrics { get; set; } = new List(); public bool HasVehicleImageMap { get; set; } = false; diff --git a/Models/User/UserCollaboratorViewModel.cs b/Models/User/UserCollaboratorViewModel.cs index 8ab37ee..1f70be3 100644 --- a/Models/User/UserCollaboratorViewModel.cs +++ b/Models/User/UserCollaboratorViewModel.cs @@ -6,4 +6,4 @@ public List CommonCollaborators { get; set; } = new List(); public List PartialCollaborators { get; set; } = new List(); } -} +} \ No newline at end of file diff --git a/Models/User/UserHousehold.cs b/Models/User/UserHousehold.cs new file mode 100644 index 0000000..74c39a8 --- /dev/null +++ b/Models/User/UserHousehold.cs @@ -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; } + } +} diff --git a/Models/User/UserHouseholdAdminViewModel.cs b/Models/User/UserHouseholdAdminViewModel.cs new file mode 100644 index 0000000..5a2bdc4 --- /dev/null +++ b/Models/User/UserHouseholdAdminViewModel.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class UserHouseholdAdminViewModel + { + public List Households { get; set; } + public int ParentUserId { get; set; } + } +} \ No newline at end of file diff --git a/Models/User/UserHouseholdViewModel.cs b/Models/User/UserHouseholdViewModel.cs new file mode 100644 index 0000000..f7cb33a --- /dev/null +++ b/Models/User/UserHouseholdViewModel.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class UserHouseholdViewModel + { + public string UserName { get; set; } + public HouseholdAccess UserHousehold { get; set; } + } +} \ No newline at end of file diff --git a/Models/User/VehicleCollaboratorViewModel.cs b/Models/User/VehicleCollaboratorViewModel.cs new file mode 100644 index 0000000..4db004b --- /dev/null +++ b/Models/User/VehicleCollaboratorViewModel.cs @@ -0,0 +1,8 @@ +namespace CarCareTracker.Models +{ + public class VehicleCollaboratorViewModel + { + public List Collaborators { get; set; } + public bool CanModifyCollaborators { get; set; } = true; + } +} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 68a6be3..b11db61 100644 --- a/Program.cs +++ b/Program.cs @@ -60,6 +60,7 @@ if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } else { @@ -82,6 +83,7 @@ else builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } //configure helpers diff --git a/Views/Admin/Index.cshtml b/Views/Admin/Index.cshtml index 14fd546..68fa951 100644 --- a/Views/Admin/Index.cshtml +++ b/Views/Admin/Index.cshtml @@ -95,6 +95,12 @@ + \ No newline at end of file diff --git a/Views/Admin/_AdminUserHouseholdModal.cshtml b/Views/Admin/_AdminUserHouseholdModal.cshtml new file mode 100644 index 0000000..04decd5 --- /dev/null +++ b/Views/Admin/_AdminUserHouseholdModal.cshtml @@ -0,0 +1,40 @@ +@using CarCareTracker.Helper +@inject IConfigHelper config +@inject ITranslationHelper translator +@model UserHouseholdAdminViewModel +@{ + var userConfig = config.GetUserConfig(User); + var userLanguage = userConfig.UserLanguage; +} + + \ No newline at end of file diff --git a/Views/Admin/_Users.cshtml b/Views/Admin/_Users.cshtml index b156ec8..a3bddfd 100644 --- a/Views/Admin/_Users.cshtml +++ b/Views/Admin/_Users.cshtml @@ -2,10 +2,10 @@ @model List @foreach (UserData userData in Model) { - + @StaticHelper.TruncateStrings(userData.UserName) @StaticHelper.TruncateStrings(userData.EmailAddress) - + } \ No newline at end of file diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index ee49e7d..ff112f9 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -68,7 +68,11 @@
  • +
  • + +
  • } +
  • @@ -121,6 +125,9 @@
  • +
  • + +
  • }