diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 02dd32c..068e787 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; @@ -92,6 +92,26 @@ namespace CarCareTracker.Controllers return int.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)); } [HttpGet] + [Route("/api/whoami")] + public IActionResult WhoAmI() + { + var result = new UserExportModel + { + Username = User.FindFirstValue(ClaimTypes.Name), + 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() + }; + if (_config.GetInvariantApi() || Request.Headers.ContainsKey("culture-invariant")) + { + return Json(result, StaticHelper.GetInvariantOption()); + } + else + { + return Json(result); + } + } + [HttpGet] [Route("/api/vehicles")] public IActionResult Vehicles() { @@ -153,6 +173,210 @@ 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); + } + } + [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)); + } + } + [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))] [HttpGet] 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/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"; 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/Models/Shared/ImportModel.cs b/Models/Shared/ImportModel.cs index 07909c8..cbb424a 100644 --- a/Models/Shared/ImportModel.cs +++ b/Models/Shared/ImportModel.cs @@ -126,14 +126,29 @@ 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(); + } + 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 99d3126..8477a4e 100644 --- a/Views/API/Index.cshtml +++ b/Views/API/Index.cshtml @@ -26,6 +26,20 @@
Parameters
+
+
+ GET +
+
+ /api/whoami +
+
+ Returns information for current user +
+
+ No Params +
+
GET @@ -159,6 +173,83 @@ Id - Id of Odometer Record
+
+
+ GET +
+
+ /api/vehicle/planrecords +
+
+ Returns a list of plan records for the vehicle +
+
+ 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 @@ -408,7 +499,7 @@
GET
-
+
/api/vehicle/taxrecords/check
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")) {