From cd0a35537bc85aac559bb6cb2e0c7dfb61ff4c86 Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Mon, 3 Feb 2025 13:20:08 -0700 Subject: [PATCH 1/7] API endpoint for Planner --- Controllers/APIController.cs | 35 +++++++++++++++++++++++++++++++++++ Models/Shared/ImportModel.cs | 6 ++++++ Views/API/Index.cshtml | 2 +- 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 02dd32c..a569556 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -153,6 +153,41 @@ namespace CarCareTracker.Controllers return Json(convertedOdometer); } } + #region PlanRecord + [TypeFilter(typeof(CollaboratorFilter))] + [HttpGet] + [Route("/api/vehicle/planrecords")] + public IActionResult PlanRecords(int vehicleId) + { + if (vehicleId == default) + { + var response = OperationResponse.Failed("Must provide a valid vehicle id"); + Response.StatusCode = 400; + return Json(response); + } + var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); + var result = vehicleRecords.Select(x => new PlanRecordExportModel { + Id = x.Id.ToString(), + DateCreated = x.DateCreated.ToShortDateString(), + DateModified = x.DateModified.ToShortDateString(), + Description = x.Description, + Cost = x.Cost.ToString(), + Notes = x.Notes, + Type = x.ImportMode.ToString(), + Priority = x.Priority.ToString(), + Progress = x.Progress.ToString(), + ExtraFields = x.ExtraFields, + Files = x.Files }); + if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) + { + return Json(result, StaticHelper.GetInvariantOption()); + } + else + { + return Json(result); + } + } + #endregion #region ServiceRecord [TypeFilter(typeof(CollaboratorFilter))] [HttpGet] diff --git a/Models/Shared/ImportModel.cs b/Models/Shared/ImportModel.cs index 07909c8..2411a0f 100644 --- a/Models/Shared/ImportModel.cs +++ b/Models/Shared/ImportModel.cs @@ -126,14 +126,20 @@ namespace CarCareTracker.Models } public class PlanRecordExportModel { + [JsonConverter(typeof(FromIntOptional))] + public string Id { get; set; } + [JsonConverter(typeof(FromDateOptional))] public string DateCreated { get; set; } + [JsonConverter(typeof(FromDateOptional))] public string DateModified { get; set; } public string Description { get; set; } public string Notes { get; set; } public string Type { get; set; } public string Priority { get; set; } public string Progress { get; set; } + [JsonConverter(typeof(FromDecimalOptional))] public string Cost { get; set; } public List ExtraFields { get; set; } = new List(); + public List Files { get; set; } = new List(); } } diff --git a/Views/API/Index.cshtml b/Views/API/Index.cshtml index 99d3126..de11d4e 100644 --- a/Views/API/Index.cshtml +++ b/Views/API/Index.cshtml @@ -408,7 +408,7 @@
GET
-
+
/api/vehicle/taxrecords/check
From c0f73080d2b02e1521e0515d33497d45e71aad5c Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Mon, 3 Feb 2025 13:34:50 -0700 Subject: [PATCH 2/7] Add APIAuth Role --- Controllers/ErrorController.cs | 2 +- Middleware/Authen.cs | 4 +++- Views/Home/Index.cshtml | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Controllers/ErrorController.cs b/Controllers/ErrorController.cs index bd753f2..aaeb7c6 100644 --- a/Controllers/ErrorController.cs +++ b/Controllers/ErrorController.cs @@ -6,7 +6,7 @@ namespace CarCareTracker.Controllers { public IActionResult Unauthorized() { - if (!User.IsInRole("CookieAuth")) + if (User.IsInRole("APIAuth")) { Response.StatusCode = 403; return new EmptyResult(); diff --git a/Middleware/Authen.cs b/Middleware/Authen.cs index aca0d0b..32e86ab 100644 --- a/Middleware/Authen.cs +++ b/Middleware/Authen.cs @@ -75,7 +75,9 @@ namespace CarCareTracker.Middleware var userIdentity = new List { new(ClaimTypes.Name, splitString[0]), - new(ClaimTypes.NameIdentifier, userData.Id.ToString()) + new(ClaimTypes.NameIdentifier, userData.Id.ToString()), + new(ClaimTypes.Email, userData.EmailAddress), + new(ClaimTypes.Role, "APIAuth") }; if (userData.IsAdmin) { diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 16c843d..2738f67 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -33,7 +33,7 @@ - @if (User.IsInRole("CookieAuth")) + @if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth")) { @if (User.IsInRole(nameof(UserData.IsAdmin))) { @@ -84,7 +84,7 @@ - @if (User.IsInRole("CookieAuth")) + @if (User.IsInRole("CookieAuth") || User.IsInRole("APIAuth")) {
+
+
+ GET +
+
+ /api/whoami +
+
+ Returns information for current user +
+
+ No Params +
+
GET From beb94983999ad78dbda047689c85f41d84bee2ff Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Mon, 3 Feb 2025 16:50:39 -0700 Subject: [PATCH 4/7] standardize this stuff. --- Controllers/APIController.cs | 17 ++++++++++++----- Models/API/ApiUser.cs | 10 ---------- Models/Shared/ImportModel.cs | 9 +++++++++ Views/API/Index.cshtml | 14 ++++++++++++++ 4 files changed, 35 insertions(+), 15 deletions(-) delete mode 100644 Models/API/ApiUser.cs diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index efc81ad..581fa3b 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -95,14 +95,21 @@ namespace CarCareTracker.Controllers [Route("/api/whoami")] public IActionResult WhoAmI() { - var result = new ApiUser + var result = new UserExportModel { Username = User.FindFirstValue(ClaimTypes.Name), - EmailAddress = User.FindFirstValue(ClaimTypes.Email), - IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)), - IsRoot = User.IsInRole(nameof(UserData.IsRootUser)) + EmailAddress = User.IsInRole(nameof(UserData.IsRootUser)) ? _config.GetUserConfig(User).DefaultReminderEmail : User.FindFirstValue(ClaimTypes.Email), + IsAdmin = User.IsInRole(nameof(UserData.IsAdmin)).ToString(), + IsRoot = User.IsInRole(nameof(UserData.IsRootUser)).ToString() }; - return Json(result); + if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) + { + return Json(result, StaticHelper.GetInvariantOption()); + } + else + { + return Json(result); + } } [HttpGet] [Route("/api/vehicles")] diff --git a/Models/API/ApiUser.cs b/Models/API/ApiUser.cs deleted file mode 100644 index 7364205..0000000 --- a/Models/API/ApiUser.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace CarCareTracker.Models -{ - public class ApiUser - { - public string Username { get; set; } - public string EmailAddress { get; set; } - public bool IsAdmin { get; set; } - public bool IsRoot { get; set; } - } -} diff --git a/Models/Shared/ImportModel.cs b/Models/Shared/ImportModel.cs index 2411a0f..cbb424a 100644 --- a/Models/Shared/ImportModel.cs +++ b/Models/Shared/ImportModel.cs @@ -142,4 +142,13 @@ namespace CarCareTracker.Models public List ExtraFields { get; set; } = new List(); public List Files { get; set; } = new List(); } + public class UserExportModel + { + public string Username { get; set; } + public string EmailAddress { get; set; } + [JsonConverter(typeof(FromBoolOptional))] + public string IsAdmin { get; set; } + [JsonConverter(typeof(FromBoolOptional))] + public string IsRoot { get; set; } + } } diff --git a/Views/API/Index.cshtml b/Views/API/Index.cshtml index 3324517..4000fc1 100644 --- a/Views/API/Index.cshtml +++ b/Views/API/Index.cshtml @@ -173,6 +173,20 @@ Id - Id of Odometer Record
+
+
+ GET +
+
+ /api/vehicle/planrecords +
+
+ Returns a list of plan records for the vehicle +
+
+ vehicleId - Id of Vehicle +
+
GET From c317c0a0580433450c23e9e84ccfe4a068d0fcff Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Tue, 4 Feb 2025 10:07:13 -0700 Subject: [PATCH 5/7] Add POST API Endpoint to add plan records. --- Controllers/APIController.cs | 80 +++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 6 deletions(-) diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 581fa3b..2e8e965 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -57,7 +57,7 @@ namespace CarCareTracker.Controllers IUserLogic userLogic, IVehicleLogic vehicleLogic, IOdometerLogic odometerLogic, - IWebHostEnvironment webEnv) + IWebHostEnvironment webEnv) { _dataAccess = dataAccess; _noteDataAccess = noteDataAccess; @@ -186,17 +186,17 @@ namespace CarCareTracker.Controllers return Json(response); } var vehicleRecords = _planRecordDataAccess.GetPlanRecordsByVehicleId(vehicleId); - var result = vehicleRecords.Select(x => new PlanRecordExportModel { - Id = x.Id.ToString(), + var result = vehicleRecords.Select(x => new PlanRecordExportModel { + Id = x.Id.ToString(), DateCreated = x.DateCreated.ToShortDateString(), DateModified = x.DateModified.ToShortDateString(), - Description = x.Description, - Cost = x.Cost.ToString(), + Description = x.Description, + Cost = x.Cost.ToString(), Notes = x.Notes, Type = x.ImportMode.ToString(), Priority = x.Priority.ToString(), Progress = x.Progress.ToString(), - ExtraFields = x.ExtraFields, + ExtraFields = x.ExtraFields, Files = x.Files }); if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) { @@ -207,6 +207,74 @@ namespace CarCareTracker.Controllers return Json(result); } } + [TypeFilter(typeof(CollaboratorFilter))] + [HttpPost] + [Route("/api/vehicle/planrecords/add")] + [Consumes("application/json")] + public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input); + [TypeFilter(typeof(CollaboratorFilter))] + [HttpPost] + [Route("/api/vehicle/planrecords/add")] + public IActionResult AddPlanRecord(int vehicleId, PlanRecordExportModel input) + { + if (vehicleId == default) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Must provide a valid vehicle id")); + } + if (string.IsNullOrWhiteSpace(input.Description) || + string.IsNullOrWhiteSpace(input.Cost) || + string.IsNullOrWhiteSpace(input.Type) || + string.IsNullOrWhiteSpace(input.Priority) || + string.IsNullOrWhiteSpace(input.Progress)) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Description, Cost, Type, Priority, and Progress cannot be empty.")); + } + bool validType = Enum.TryParse(input.Type, out ImportMode parsedType); + bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority); + bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress); + if (!validType || !validPriority || !validProgress) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid.")); + } + if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord")); + } + if (parsedProgress == PlanProgress.Done) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done.")); + } + try + { + var planRecord = new PlanRecord() + { + VehicleId = vehicleId, + DateCreated = DateTime.Now, + DateModified = DateTime.Now, + Description = input.Description, + Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes, + Cost = decimal.Parse(input.Cost), + ImportMode = parsedType, + Priority = parsedPriority, + Progress = parsedProgress, + ExtraFields = input.ExtraFields, + Files = input.Files + }; + _planRecordDataAccess.SavePlanRecordToVehicle(planRecord); + StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(planRecord, "planrecord.add.api", User.Identity.Name)); + return Json(OperationResponse.Succeed("Plan Record Added")); + } + catch (Exception ex) + { + Response.StatusCode = 500; + return Json(OperationResponse.Failed(ex.Message)); + } + } #endregion #region ServiceRecord [TypeFilter(typeof(CollaboratorFilter))] From 125bc44d2eed7a7f809f08c337d278b150f3f300 Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Tue, 4 Feb 2025 13:15:57 -0700 Subject: [PATCH 6/7] add put and delete api endpoints. --- Controllers/APIController.cs | 101 +++++++++++++++++++++++++++++++++++ Views/API/Index.cshtml | 63 ++++++++++++++++++++++ 2 files changed, 164 insertions(+) diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 2e8e965..068e787 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -275,6 +275,107 @@ namespace CarCareTracker.Controllers return Json(OperationResponse.Failed(ex.Message)); } } + [HttpDelete] + [Route("/api/vehicle/planrecords/delete")] + public IActionResult DeletePlanRecord(int id) + { + var existingRecord = _planRecordDataAccess.GetPlanRecordById(id); + if (existingRecord == null || existingRecord.Id == default) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Invalid Record Id")); + } + //security check. + if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) + { + Response.StatusCode = 401; + return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); + } + //restore any requisitioned supplies. + if (existingRecord.RequisitionHistory.Any()) + { + _vehicleLogic.RestoreSupplyRecordsByUsage(existingRecord.RequisitionHistory, existingRecord.Description); + } + var result = _planRecordDataAccess.DeletePlanRecordById(existingRecord.Id); + if (result) + { + StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.delete.api", User.Identity.Name)); + } + return Json(OperationResponse.Conditional(result, "Plan Record Deleted")); + } + [HttpPut] + [Route("/api/vehicle/planrecords/update")] + [Consumes("application/json")] + public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input); + [HttpPut] + [Route("/api/vehicle/planrecords/update")] + public IActionResult UpdatePlanRecord(PlanRecordExportModel input) + { + if (string.IsNullOrWhiteSpace(input.Id) || + string.IsNullOrWhiteSpace(input.Description) || + string.IsNullOrWhiteSpace(input.Cost) || + string.IsNullOrWhiteSpace(input.Type) || + string.IsNullOrWhiteSpace(input.Priority) || + string.IsNullOrWhiteSpace(input.Progress)) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Id, Description, Cost, Type, Priority, and Progress cannot be empty.")); + } + bool validType = Enum.TryParse(input.Type, out ImportMode parsedType); + bool validPriority = Enum.TryParse(input.Priority, out PlanPriority parsedPriority); + bool validProgress = Enum.TryParse(input.Progress, out PlanProgress parsedProgress); + if (!validType || !validPriority || !validProgress) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, values for Type(ServiceRecord, RepairRecord, UpgradeRecord), Priority(Critical, Normal, Low), or Progress(Backlog, InProgress, Testing) is invalid.")); + } + if (parsedType != ImportMode.ServiceRecord && parsedType != ImportMode.RepairRecord && parsedType != ImportMode.UpgradeRecord) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Type can only ServiceRecord, RepairRecord, or UpgradeRecord")); + } + if (parsedProgress == PlanProgress.Done) + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Input object invalid, Progress cannot be set to Done.")); + } + try + { + //retrieve existing record + var existingRecord = _planRecordDataAccess.GetPlanRecordById(int.Parse(input.Id)); + if (existingRecord != null && existingRecord.Id == int.Parse(input.Id)) + { + //check if user has access to the vehicleId + if (!_userLogic.UserCanEditVehicle(GetUserID(), existingRecord.VehicleId)) + { + Response.StatusCode = 401; + return Json(OperationResponse.Failed("Access Denied, you don't have access to this vehicle.")); + } + existingRecord.DateModified = DateTime.Now; + existingRecord.Description = input.Description; + existingRecord.Notes = string.IsNullOrWhiteSpace(input.Notes) ? "" : input.Notes; + existingRecord.Cost = decimal.Parse(input.Cost); + existingRecord.ImportMode = parsedType; + existingRecord.Priority = parsedPriority; + existingRecord.Progress = parsedProgress; + existingRecord.Files = input.Files; + existingRecord.ExtraFields = input.ExtraFields; + _planRecordDataAccess.SavePlanRecordToVehicle(existingRecord); + StaticHelper.NotifyAsync(_config.GetWebHookUrl(), WebHookPayload.FromPlanRecord(existingRecord, "planrecord.update.api", User.Identity.Name)); + } + else + { + Response.StatusCode = 400; + return Json(OperationResponse.Failed("Invalid Record Id")); + } + return Json(OperationResponse.Succeed("Plan Record Updated")); + } + catch (Exception ex) + { + Response.StatusCode = 500; + return Json(OperationResponse.Failed(ex.Message)); + } + } #endregion #region ServiceRecord [TypeFilter(typeof(CollaboratorFilter))] diff --git a/Views/API/Index.cshtml b/Views/API/Index.cshtml index 4000fc1..8477a4e 100644 --- a/Views/API/Index.cshtml +++ b/Views/API/Index.cshtml @@ -187,6 +187,69 @@ vehicleId - Id of Vehicle
+
+
+ POST +
+
+ /api/vehicle/planrecords/add +
+
+ Adds Plan Record to the vehicle +
+
+ vehicleId - Id of Vehicle +
+ Body(form-data): {
+ description - Description
+ cost - Cost
+ type - ServiceRecord/RepairRecord/UpgradeRecord
+ priority - Low/Normal/Critical
+ progress - Backlog/InProgress/Testing
+ notes - notes(optional)
+ extrafields - extrafields(optional)
+ files - attachments(optional)
+ } +
+
+
+
+ PUT +
+
+ /api/vehicle/planrecords/update +
+
+ Updates Plan Record +
+
+ Body(form-data): {
+ Id - Id of Plan Record
+ description - Description
+ cost - Cost
+ type - ServiceRecord/RepairRecord/UpgradeRecord
+ priority - Low/Normal/Critical
+ progress - Backlog/InProgress/Testing
+ notes - notes(optional)
+ extrafields - extrafields(optional)
+ files - attachments(optional)
+ } +
+
+
+
+ DELETE +
+
+ /api/vehicle/planrecords/delete +
+
+ Deletes Plan Record +
+
+ Id - Id of Plan Record +
+
GET From c1500b6ed06713ce83da23ea1e23eca937cb3d8d Mon Sep 17 00:00:00 2001 From: "DESKTOP-T0O5CDB\\DESK-555BD" Date: Tue, 4 Feb 2025 13:22:03 -0700 Subject: [PATCH 7/7] bump version --- Helper/StaticHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs index 5cc2112..7c30a9b 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.4.4"; + public const string VersionNumber = "1.4.5"; public const string DbName = "data/cartracker.db"; public const string UserConfigPath = "data/config/userConfig.json"; public const string LegacyUserConfigPath = "config/userConfig.json";