Merge pull request #1223 from hargata/Hargata/855

Hargata/855
This commit is contained in:
Hargata Softworks 2026-01-25 13:50:35 -07:00 committed by GitHub
commit 7e94ccfdf4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 777 additions and 39 deletions

View File

@ -78,11 +78,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/equipmentrecords/add")]
[Consumes("application/json")]
public IActionResult AddEquipmentRecordJson(int vehicleId, [FromBody] EquipmentRecordExportModel input) => AddEquipmentRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/equipmentrecords/add")]
@ -129,6 +131,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/equipmentrecords/delete")]
public IActionResult DeleteEquipmentRecord(int id)
@ -163,10 +166,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Equipment Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/equipmentrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateEquipmentRecordJson([FromBody] EquipmentRecordExportModel input) => UpdateEquipmentRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/equipmentrecords/update")]
public IActionResult UpdateEquipmentRecord(EquipmentRecordExportModel input)

View File

@ -121,11 +121,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/gasrecords/add")]
[Consumes("application/json")]
public IActionResult AddGasRecordJson(int vehicleId, [FromBody] GasRecordExportModel input) => AddGasRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/gasrecords/add")]
@ -193,6 +195,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/gasrecords/delete")]
public IActionResult DeleteGasRecord(int id)
@ -216,10 +219,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Gas Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/gasrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateGasRecordJson([FromBody] GasRecordExportModel input) => UpdateGasRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/gasrecords/update")]
public IActionResult UpdateGasRecord(GasRecordExportModel input)

View File

@ -21,6 +21,7 @@ namespace CarCareTracker.Controllers
var result = _vehicleLogic.GetMaxMileage(vehicleId);
return Json(result);
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/odometerrecords/recalculate")]
@ -123,11 +124,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/odometerrecords/add")]
[Consumes("application/json")]
public IActionResult AddOdometerRecordJson(int vehicleId, [FromBody] OdometerRecordExportModel input) => AddOdometerRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/odometerrecords/add")]
@ -207,6 +210,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/odometerrecords/delete")]
public IActionResult DeleteOdometerRecord(int id)
@ -230,10 +234,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Odometer Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/odometerrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateOdometerRecordJson([FromBody] OdometerRecordExportModel input) => UpdateOdometerRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/odometerrecords/update")]
public IActionResult UpdateOdometerRecord(OdometerRecordExportModel input)

View File

@ -105,11 +105,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
[Consumes("application/json")]
public IActionResult AddPlanRecordJson(int vehicleId, [FromBody] PlanRecordExportModel input) => AddPlanRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/planrecords/add")]
@ -182,6 +184,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/planrecords/delete")]
public IActionResult DeletePlanRecord(int id)
@ -210,10 +213,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Plan Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
[Consumes("application/json")]
public IActionResult UpdatePlanRecordJson([FromBody] PlanRecordExportModel input) => UpdatePlanRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/planrecords/update")]
public IActionResult UpdatePlanRecord(PlanRecordExportModel input)

View File

@ -88,11 +88,13 @@ namespace CarCareTracker.Controllers
return Json(results);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
[Consumes("application/json")]
public IActionResult AddReminderRecordJson(int vehicleId, [FromBody] ReminderExportModel input) => AddReminderRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/reminders/add")]
@ -162,10 +164,12 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/reminders/update")]
[Consumes("application/json")]
public IActionResult UpdateReminderRecordJson([FromBody] ReminderExportModel input) => UpdateReminderRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/reminders/update")]
public IActionResult UpdateReminderRecord(ReminderExportModel input)
@ -242,6 +246,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/reminders/delete")]
public IActionResult DeleteReminderRecord(int id)

View File

@ -89,11 +89,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/repairrecords/add")]
[Consumes("application/json")]
public IActionResult AddRepairRecordJson(int vehicleId, [FromBody] GenericRecordExportModel input) => AddRepairRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/repairrecords/add")]
@ -157,6 +159,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/repairrecords/delete")]
public IActionResult DeleteRepairRecord(int id)
@ -185,10 +188,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Repair Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/repairrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateRepairRecordJson([FromBody] GenericRecordExportModel input) => UpdateRepairRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/repairrecords/update")]
public IActionResult UpdateRepairRecord(GenericRecordExportModel input)

View File

@ -89,11 +89,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/servicerecords/add")]
[Consumes("application/json")]
public IActionResult AddServiceRecordJson(int vehicleId, [FromBody] GenericRecordExportModel input) => AddServiceRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/servicerecords/add")]
@ -156,6 +158,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/servicerecords/delete")]
public IActionResult DeleteServiceRecord(int id)
@ -184,10 +187,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Service Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/servicerecords/update")]
[Consumes("application/json")]
public IActionResult UpdateServiceRecordJson([FromBody] GenericRecordExportModel input) => UpdateServiceRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/servicerecords/update")]
public IActionResult UpdateServiceRecord(GenericRecordExportModel input)

View File

@ -121,11 +121,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/supplyrecords/add")]
[Consumes("application/json")]
public IActionResult AddSupplyRecordJson(int vehicleId, [FromBody] SupplyRecordExportModel input) => AddSupplyRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/supplyrecords/add")]
@ -178,6 +180,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/supplyrecords/delete")]
public IActionResult DeleteSupplyRecord(int id)
@ -211,10 +214,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Supply Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/supplyrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateSupplyRecordJson([FromBody] SupplyRecordExportModel input) => UpdateSupplyRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/supplyrecords/update")]
public IActionResult UpdateSupplyRecord(SupplyRecordExportModel input)

View File

@ -125,11 +125,13 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed($"No Recurring Taxes Updated Due To Error: {ex.Message}"));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/taxrecords/add")]
[Consumes("application/json")]
public IActionResult AddTaxRecordJson(int vehicleId, [FromBody] TaxRecordExportModel input) => AddTaxRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/taxrecords/add")]
@ -179,6 +181,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/taxrecords/delete")]
public IActionResult DeleteTaxRecord(int id)
@ -202,10 +205,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Tax Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/taxrecords/update")]
[Consumes("application/json")]
public IActionResult UpdateTaxRecordJson([FromBody] TaxRecordExportModel input) => UpdateTaxRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/taxrecords/update")]
public IActionResult UpdateTaxRecord(TaxRecordExportModel input)

View File

@ -89,11 +89,13 @@ namespace CarCareTracker.Controllers
return Json(result);
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/upgraderecords/add")]
[Consumes("application/json")]
public IActionResult AddUpgradeRecordJson(int vehicleId, [FromBody] GenericRecordExportModel input) => AddUpgradeRecord(vehicleId, input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[TypeFilter(typeof(CollaboratorFilter), Arguments = new object[] { false, true, HouseholdPermission.Edit })]
[HttpPost]
[Route("/api/vehicle/upgraderecords/add")]
@ -156,6 +158,7 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Delete })]
[HttpDelete]
[Route("/api/vehicle/upgraderecords/delete")]
public IActionResult DeleteUpgradeRecord(int id)
@ -184,10 +187,12 @@ namespace CarCareTracker.Controllers
}
return Json(OperationResponse.Conditional(result, "Upgrade Record Deleted"));
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/upgraderecords/update")]
[Consumes("application/json")]
public IActionResult UpdateUpgradeRecordJson([FromBody] GenericRecordExportModel input) => UpdateUpgradeRecord(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicle/upgraderecords/update")]
public IActionResult UpdateUpgradeRecord(GenericRecordExportModel input)

View File

@ -317,10 +317,12 @@ namespace CarCareTracker.Controllers
return Json(OperationResponse.Failed(ex.Message));
}
}
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicles/update")]
[Consumes("application/json")]
public IActionResult UpdateVehicleJson([FromBody] VehicleImportModel input) => UpdateVehicle(input);
[TypeFilter(typeof(APIKeyFilter), Arguments = new object[] { HouseholdPermission.Edit })]
[HttpPut]
[Route("/api/vehicles/update")]
public IActionResult UpdateVehicle(VehicleImportModel input)

View File

@ -76,7 +76,8 @@ namespace CarCareTracker.Controllers
&& _configHelper.DeleteUserConfig(userId)
&& _loginLogic.DeleteUser(userId)
&& _userLogic.DeleteAllHouseholdByChildUserId(userId)
&& _userLogic.DeleteAllHouseholdByParentUserId(userId);
&& _userLogic.DeleteAllHouseholdByParentUserId(userId)
&& _userLogic.DeleteAllAPIKeysByUserId(userId);
return Json(result);
}
[HttpPost]

View File

@ -336,6 +336,29 @@ namespace CarCareTracker.Controllers
var result = _userLogic.AddUserToHousehold(GetUserID(), username);
return Json(result);
}
[HttpGet]
public IActionResult GetUserAPIKeys()
{
var result = _userLogic.GetAPIKeysByUserId(GetUserID());
return PartialView("_UserApiKeysModal", result);
}
[HttpGet]
public IActionResult GetCreateApiKeyModal()
{
return PartialView("_CreateApiKeyModal");
}
[HttpPost]
public IActionResult CreateAPIKeyForUser(string keyName, List<HouseholdPermission> permissions)
{
var result = _userLogic.CreateAPIKey(GetUserID(), keyName, permissions);
return Json(result);
}
[HttpPost]
public IActionResult DeleteAPIKeyForUser(int keyId)
{
var result = _userLogic.DeleteAPIKeyByKeyIdAndUserId(keyId, GetUserID());
return Json(OperationResponse.Conditional(result, "API Key Deleted", StaticHelper.GenericErrorMessage));
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetRootAccountInformationModal()

View File

@ -56,7 +56,8 @@ namespace CarCareTracker.Controllers
"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.equipmentrecords (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, data jsonb not null, PRIMARY KEY(parentUserId, childUserId))"
"CREATE TABLE IF NOT EXISTS app.userhouseholdrecords (parentUserId INT, childUserId INT, data jsonb not null, PRIMARY KEY(parentUserId, childUserId))",
"CREATE TABLE IF NOT EXISTS app.apikeyrecords (id INT GENERATED BY DEFAULT AS IDENTITY primary key, userId INT not null, apiKey TEXT not null, data jsonb not null)"
};
foreach(string cmd in cmds)
{
@ -108,6 +109,7 @@ namespace CarCareTracker.Controllers
var equipmentrecords = new List<EquipmentRecord>();
var userhouseholdrecords = new List<UserHousehold>();
var apikeyrecords = new List<APIKey>();
#region "Part1"
string cmd = $"SELECT data FROM app.vehicles";
using (var ctext = pgDataSource.CreateCommand(cmd))
@ -499,6 +501,25 @@ namespace CarCareTracker.Controllers
}
;
}
cmd = $"SELECT data FROM app.apikeyrecords";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
APIKey result = JsonSerializer.Deserialize<APIKey>(reader["data"] as string);
apikeyrecords.Add(result);
}
}
foreach (var record in apikeyrecords)
{
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<APIKey>("apikeyrecords");
table.Upsert(record);
}
;
}
#endregion
var destFilePath = $"{fullFolderPath}.zip";
ZipFile.CreateFromDirectory(fullFolderPath, destFilePath);
@ -552,6 +573,7 @@ namespace CarCareTracker.Controllers
var equipmentrecords = new List<EquipmentRecord>();
var userhouseholdrecords = new List<UserHousehold>();
var apikeyrecords = new List<APIKey>();
#region "Part1"
using (var db = new LiteDatabase(fullFileName))
{
@ -899,6 +921,24 @@ namespace CarCareTracker.Controllers
ctext.ExecuteNonQuery();
}
}
using (var db = new LiteDatabase(fullFileName))
{
var table = db.GetCollection<APIKey>("apikeyrecords");
apikeyrecords = table.FindAll().ToList();
}
;
foreach (var record in apikeyrecords)
{
string cmd = $"INSERT INTO app.apikeyrecords (id, userId, apiKey, data) VALUES(@id, @userId, @apiKey, CAST(@data AS jsonb)); SELECT setval('app.apikeyrecords_id_seq', (SELECT MAX(id) from app.apikeyrecords));";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", record.Id);
ctext.Parameters.AddWithValue("userId", record.UserId);
ctext.Parameters.AddWithValue("apiKey", record.Key);
ctext.Parameters.AddWithValue("data", JsonSerializer.Serialize(record));
ctext.ExecuteNonQuery();
}
}
#endregion
return Json(OperationResponse.Succeed("Data Imported Successfully"));
}

View File

@ -0,0 +1,61 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using LiteDB;
using CarCareTracker.Helper;
namespace CarCareTracker.External.Implementations
{
public class ApiKeyRecordDataAccess : IApiKeyRecordDataAccess
{
private ILiteDBHelper _liteDB { get; set; }
private static string tableName = "apikeyrecords";
public ApiKeyRecordDataAccess(ILiteDBHelper liteDB)
{
_liteDB = liteDB;
}
public List<APIKey> GetAPIKeyRecordsByUserId(int userId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
var apiKeyRecords = table.Find(Query.EQ(nameof(APIKey.UserId), userId));
return apiKeyRecords.ToList() ?? new List<APIKey>();
}
public APIKey GetAPIKeyById(int apiKeyId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
return table.FindById(apiKeyId);
}
public APIKey GetAPIKeyByKey(string hashedKey)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
var apiKeyRecord = table.FindOne(Query.EQ(nameof(APIKey.Key), hashedKey));
return apiKeyRecord ?? new APIKey();
}
public bool SaveAPIKey(APIKey apiKey)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
table.Upsert(apiKey);
db.Checkpoint();
return true;
}
public bool DeleteAPIKeyById(int apiKeyId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
table.Delete(apiKeyId);
db.Checkpoint();
return true;
}
public bool DeleteAllAPIKeysByUserId(int userId)
{
var db = _liteDB.GetLiteDB();
var table = db.GetCollection<APIKey>(tableName);
var apiKeyRecords = table.DeleteMany(Query.EQ(nameof(APIKey.UserId), userId));
db.Checkpoint();
return true;
}
}
}

View File

@ -0,0 +1,186 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Models;
using Npgsql;
using System.Text.Json;
namespace CarCareTracker.External.Implementations
{
public class PGApiKeyRecordDataAccess : IApiKeyRecordDataAccess
{
private NpgsqlDataSource pgDataSource;
private readonly ILogger<PGApiKeyRecordDataAccess> _logger;
private static string tableName = "apikeyrecords";
public PGApiKeyRecordDataAccess(IConfiguration config, ILogger<PGApiKeyRecordDataAccess> 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} (id INT GENERATED BY DEFAULT AS IDENTITY primary key, userId INT not null, apiKey TEXT not null, data jsonb not null)";
using (var ctext = pgDataSource.CreateCommand(initCMD))
{
ctext.ExecuteNonQuery();
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
}
}
public List<APIKey> GetAPIKeyRecordsByUserId(int userId)
{
try
{
string cmd = $"SELECT data FROM app.{tableName} WHERE userId = @userId";
var results = new List<APIKey>();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("userId", userId);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
APIKey apiKeyRecord = JsonSerializer.Deserialize<APIKey>(reader["data"] as string);
results.Add(apiKeyRecord);
}
}
return results;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new List<APIKey>();
}
}
public APIKey GetAPIKeyById(int apiKeyId)
{
try
{
string cmd = $"SELECT data FROM app.{tableName} WHERE id = @id";
var result = new APIKey();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", apiKeyId);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
APIKey apiKeyRecord = JsonSerializer.Deserialize<APIKey>(reader["data"] as string);
result = apiKeyRecord;
}
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new APIKey();
}
}
public APIKey GetAPIKeyByKey(string hashedKey)
{
try
{
string cmd = $"SELECT data FROM app.{tableName} WHERE apiKey = @apiKey";
var result = new APIKey();
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("apiKey", hashedKey);
using (NpgsqlDataReader reader = ctext.ExecuteReader())
while (reader.Read())
{
APIKey apiKeyRecord = JsonSerializer.Deserialize<APIKey>(reader["data"] as string);
result = apiKeyRecord;
}
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new APIKey();
}
}
public bool DeleteAPIKeyById(int apiKeyId)
{
try
{
string cmd = $"DELETE FROM app.{tableName} WHERE id = @id";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", apiKeyId);
return ctext.ExecuteNonQuery() > 0;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool SaveAPIKey(APIKey apiKey)
{
try
{
if (apiKey.Id == default)
{
string cmd = $"INSERT INTO app.{tableName} (userId, apiKey, data) VALUES(@userId, @apiKey, CAST(@data AS jsonb)) RETURNING id";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("userId", apiKey.UserId);
ctext.Parameters.AddWithValue("apiKey", apiKey.Key);
ctext.Parameters.AddWithValue("data", "{}");
apiKey.Id = Convert.ToInt32(ctext.ExecuteScalar());
//update json data
if (apiKey.Id != default)
{
string cmdU = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id";
using (var ctextU = pgDataSource.CreateCommand(cmdU))
{
var serializedData = JsonSerializer.Serialize(apiKey);
ctextU.Parameters.AddWithValue("id", apiKey.Id);
ctextU.Parameters.AddWithValue("data", serializedData);
return ctextU.ExecuteNonQuery() > 0;
}
}
return apiKey.Id != default;
}
}
else
{
string cmd = $"UPDATE app.{tableName} SET data = CAST(@data AS jsonb) WHERE id = @id";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
var serializedData = JsonSerializer.Serialize(apiKey);
ctext.Parameters.AddWithValue("id", apiKey.Id);
ctext.Parameters.AddWithValue("data", serializedData);
return ctext.ExecuteNonQuery() > 0;
}
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
public bool DeleteAllAPIKeysByUserId(int userId)
{
try
{
string cmd = $"DELETE FROM app.{tableName} WHERE userId = @id";
using (var ctext = pgDataSource.CreateCommand(cmd))
{
ctext.Parameters.AddWithValue("id", userId);
ctext.ExecuteNonQuery();
return true;
}
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return false;
}
}
}
}

View File

@ -0,0 +1,14 @@
using CarCareTracker.Models;
namespace CarCareTracker.External.Interfaces
{
public interface IApiKeyRecordDataAccess
{
public List<APIKey> GetAPIKeyRecordsByUserId(int userId);
public APIKey GetAPIKeyById(int apiKeyId);
public APIKey GetAPIKeyByKey(string hashedKey);
public bool SaveAPIKey(APIKey apiKey);
public bool DeleteAPIKeyById(int apiKeyId);
public bool DeleteAllAPIKeysByUserId(int userId);
}
}

37
Filter/APIKeyFilter.cs Normal file
View File

@ -0,0 +1,37 @@
using CarCareTracker.Logic;
using CarCareTracker.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace CarCareTracker.Filter
{
public class APIKeyFilter: ActionFilterAttribute
{
private readonly IUserLogic _userLogic;
private readonly HouseholdPermission _permission;
public APIKeyFilter(IUserLogic userLogic, HouseholdPermission? permission = HouseholdPermission.View) {
_userLogic = userLogic;
_permission = permission ?? HouseholdPermission.View;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (filterContext.HttpContext.User.IsInRole("APIKeyAuth"))
{
var apikey_header = filterContext.HttpContext.Request.Headers["x-api-key"];
if (string.IsNullOrWhiteSpace(apikey_header))
{
apikey_header = filterContext.HttpContext.Request.Query["apiKey"];
}
if (!string.IsNullOrWhiteSpace(apikey_header))
{
var permissions = _userLogic.GetAPIKeyPermissions(apikey_header);
if (!permissions.Contains(_permission))
{
filterContext.Result = new JsonResult(OperationResponse.Failed("Access Denied"));
filterContext.HttpContext.Response.StatusCode = 401;
}
}
}
}
}
}

View File

@ -992,5 +992,20 @@ namespace CarCareTracker.Helper
return "primary";
}
}
public static string GetHash(string value)
{
StringBuilder Sb = new StringBuilder();
using (var hash = SHA256.Create())
{
Encoding enc = Encoding.UTF8;
byte[] result = hash.ComputeHash(enc.GetBytes(value));
foreach (byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
}
}

View File

@ -24,6 +24,7 @@ namespace CarCareTracker.Logic
OperationResponse SendRegistrationToken(LoginModel credentials);
UserData ValidateUserCredentials(LoginModel credentials);
UserData ValidateOpenIDUser(LoginModel credentials);
UserData ValidateAPIKey(string apiKey);
bool CheckIfUserIsValid(int userId);
bool CreateRootUserCredentials(LoginModel credentials);
bool DeleteRootUserCredentials();
@ -36,17 +37,20 @@ namespace CarCareTracker.Logic
{
private readonly IUserRecordDataAccess _userData;
private readonly ITokenRecordDataAccess _tokenData;
private readonly IApiKeyRecordDataAccess _apiKeyData;
private readonly IMailHelper _mailHelper;
private readonly IConfigHelper _configHelper;
private IMemoryCache _cache;
public LoginLogic(IUserRecordDataAccess userData,
ITokenRecordDataAccess tokenData,
IApiKeyRecordDataAccess apiKeyData,
IMailHelper mailHelper,
IConfigHelper configHelper,
IMemoryCache memoryCache)
{
_userData = userData;
_tokenData = tokenData;
_apiKeyData = apiKeyData;
_mailHelper = mailHelper;
_configHelper = configHelper;
_cache = memoryCache;
@ -103,7 +107,7 @@ namespace CarCareTracker.Logic
if (!string.IsNullOrWhiteSpace(credentials.Password))
{
//update password
existingUser.Password = GetHash(credentials.Password);
existingUser.Password = StaticHelper.GetHash(credentials.Password);
}
//delete token
_tokenData.DeleteToken(existingToken.Id);
@ -136,7 +140,7 @@ namespace CarCareTracker.Logic
var newUser = new UserData()
{
UserName = credentials.UserName,
Password = GetHash(NewToken()), //generate a password for OpenID User
Password = StaticHelper.GetHash(NewToken()), //generate a password for OpenID User
EmailAddress = credentials.EmailAddress
};
var result = _userData.SaveUserRecord(newUser);
@ -178,7 +182,7 @@ namespace CarCareTracker.Logic
var newUser = new UserData()
{
UserName = credentials.UserName,
Password = GetHash(credentials.Password),
Password = StaticHelper.GetHash(credentials.Password),
EmailAddress = credentials.EmailAddress
};
var result = _userData.SaveUserRecord(newUser);
@ -235,7 +239,7 @@ namespace CarCareTracker.Logic
{
return OperationResponse.Failed("Unable to locate user");
}
existingUser.Password = GetHash(credentials.Password);
existingUser.Password = StaticHelper.GetHash(credentials.Password);
var result = _userData.SaveUserRecord(existingUser);
//delete token
_tokenData.DeleteToken(existingToken.Id);
@ -262,7 +266,7 @@ namespace CarCareTracker.Logic
{
//authenticate via DB.
var result = _userData.GetUserRecordByUserName(credentials.UserName);
if (GetHash(credentials.Password) == result.Password)
if (StaticHelper.GetHash(credentials.Password) == result.Password)
{
result.Password = string.Empty;
return result;
@ -293,6 +297,26 @@ namespace CarCareTracker.Logic
return new UserData();
}
}
public UserData ValidateAPIKey(string apiKey)
{
var hashedAPIKey = StaticHelper.GetHash(apiKey);
var apiKeyUser = _apiKeyData.GetAPIKeyByKey(hashedAPIKey);
if (apiKeyUser.UserId != default)
{
if (apiKeyUser.UserId == -1)
{
var rootUserData = GetRootUserData(apiKeyUser.Name);
return rootUserData;
}
var result = _userData.GetUserRecordById(apiKeyUser.UserId);
if (result.Id != default)
{
result.Password = string.Empty;
return result;
}
}
return new UserData();
}
#region "Admin Functions"
public bool MakeUserAdmin(int userId, bool isAdmin)
{
@ -376,7 +400,7 @@ namespace CarCareTracker.Logic
return OperationResponse.Failed("Unable to find user");
}
var newPassword = Guid.NewGuid().ToString().Substring(0, 8);
existingUser.Password = GetHash(newPassword);
existingUser.Password = StaticHelper.GetHash(newPassword);
var result = _userData.SaveUserRecord(existingUser);
if (result)
{
@ -399,8 +423,8 @@ namespace CarCareTracker.Logic
if (existingUserConfig is not null)
{
//create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
var hashedUserName = StaticHelper.GetHash(credentials.UserName);
var hashedPassword = StaticHelper.GetHash(credentials.Password);
//copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName;
@ -412,8 +436,8 @@ namespace CarCareTracker.Logic
var newUserConfig = new UserConfig()
{
EnableAuth = true,
UserNameHash = GetHash(credentials.UserName),
UserPasswordHash = GetHash(credentials.Password)
UserNameHash = StaticHelper.GetHash(credentials.UserName),
UserPasswordHash = StaticHelper.GetHash(credentials.Password)
};
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig));
}
@ -438,8 +462,8 @@ namespace CarCareTracker.Logic
}
private bool UserIsRoot(LoginModel credentials)
{
var hashedUserName = GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password);
var hashedUserName = StaticHelper.GetHash(credentials.UserName);
var hashedPassword = StaticHelper.GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
}
private UserData GetRootUserData(string username)
@ -454,21 +478,7 @@ namespace CarCareTracker.Logic
};
}
#endregion
private static string GetHash(string value)
{
StringBuilder Sb = new StringBuilder();
using (var hash = SHA256.Create())
{
Encoding enc = Encoding.UTF8;
byte[] result = hash.ComputeHash(enc.GetBytes(value));
foreach (byte b in result)
Sb.Append(b.ToString("x2"));
}
return Sb.ToString();
}
private string NewToken()
{
return Guid.NewGuid().ToString().Substring(0, 8);

View File

@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models;
namespace CarCareTracker.Logic
@ -22,18 +23,26 @@ namespace CarCareTracker.Logic
bool DeleteUserFromHousehold(int parentUserId, int childUserId);
bool DeleteAllHouseholdByParentUserId(int parentUserId);
bool DeleteAllHouseholdByChildUserId(int childUserId);
OperationResponse CreateAPIKey(int userId, string keyName, List<HouseholdPermission> permisions);
List<APIKey> GetAPIKeysByUserId(int userId);
List<HouseholdPermission> GetAPIKeyPermissions(string apiKey);
bool DeleteAPIKeyByKeyIdAndUserId(int keyId, int userId);
bool DeleteAllAPIKeysByUserId(int userId);
}
public class UserLogic: IUserLogic
{
private readonly IUserAccessDataAccess _userAccess;
private readonly IUserRecordDataAccess _userData;
private readonly IUserHouseholdDataAccess _userHouseholdData;
private readonly IApiKeyRecordDataAccess _apiKeyData;
public UserLogic(IUserAccessDataAccess userAccess,
IUserRecordDataAccess userData,
IUserHouseholdDataAccess userHouseholdData) {
IUserHouseholdDataAccess userHouseholdData,
IApiKeyRecordDataAccess apiKeyData) {
_userAccess = userAccess;
_userData = userData;
_userHouseholdData = userHouseholdData;
_apiKeyData = apiKeyData;
}
public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId)
{
@ -287,5 +296,60 @@ namespace CarCareTracker.Logic
var result = _userHouseholdData.DeleteAllHouseholdRecordsByChildUserId(childUserId);
return result;
}
public OperationResponse CreateAPIKey(int userId, string keyName, List<HouseholdPermission> permisions)
{
//check if user already has an API key by that name.
var existingApiKeys = _apiKeyData.GetAPIKeyRecordsByUserId(userId);
if (existingApiKeys.Any(x=>x.Name.ToLower() == keyName.ToLower()))
{
return OperationResponse.Failed("An API Key with that name already exists");
}
//generate key pair
var unhashedKey = Guid.NewGuid().ToString().Replace("-", string.Empty);
var hashedKey = StaticHelper.GetHash(unhashedKey);
var keyToSave = new APIKey
{
UserId = userId,
Name = keyName,
Permissions = permisions,
Key = hashedKey
};
var result = _apiKeyData.SaveAPIKey(keyToSave);
if (result && keyToSave.Id != default)
{
return OperationResponse.Succeed("API Key Created", new {apiKey = unhashedKey});
}
return OperationResponse.Failed("Unable to create API Key");
}
public List<APIKey> GetAPIKeysByUserId(int userId)
{
var result = _apiKeyData.GetAPIKeyRecordsByUserId(userId);
return result;
}
public List<HouseholdPermission> GetAPIKeyPermissions(string apiKey)
{
var hashedKey = StaticHelper.GetHash(apiKey);
var existingKey = _apiKeyData.GetAPIKeyByKey(hashedKey);
if (existingKey.Id != default)
{
return existingKey.Permissions;
}
return new List<HouseholdPermission>();
}
public bool DeleteAPIKeyByKeyIdAndUserId(int keyId, int userId)
{
var existingKey = _apiKeyData.GetAPIKeyById(keyId);
if (existingKey.Id != default && existingKey.UserId == userId)
{
var result = _apiKeyData.DeleteAPIKeyById(keyId);
return result;
}
return false;
}
public bool DeleteAllAPIKeysByUserId(int userId)
{
var result = _apiKeyData.DeleteAllAPIKeysByUserId(userId);
return result;
}
}
}

View File

@ -53,7 +53,13 @@ namespace CarCareTracker.Middleware
var access_token = _httpContext.HttpContext.Request.Cookies[StaticHelper.LoginCookieName];
//auth using Basic Auth for API.
var request_header = _httpContext.HttpContext.Request.Headers["Authorization"];
if (string.IsNullOrWhiteSpace(access_token) && string.IsNullOrWhiteSpace(request_header))
//auth using API Key for API.
var apikey_header = _httpContext.HttpContext.Request.Headers["x-api-key"];
if (string.IsNullOrWhiteSpace(apikey_header))
{
apikey_header = _httpContext.HttpContext.Request.Query["apiKey"];
}
if (string.IsNullOrWhiteSpace(access_token) && string.IsNullOrWhiteSpace(request_header) && string.IsNullOrWhiteSpace(apikey_header))
{
return AuthenticateResult.Fail("Cookie is invalid or does not exist.");
}
@ -146,20 +152,45 @@ namespace CarCareTracker.Middleware
{
return AuthenticateResult.Fail("Corrupted credentials");
}
}
else if (!string.IsNullOrWhiteSpace(apikey_header) && _httpContext.HttpContext.Request.Path.StartsWithSegments("/api"))
{
//only do API Key Auth for API methods
var userData = _loginLogic.ValidateAPIKey(apikey_header);
if (userData.Id != default)
{
var appIdentity = new ClaimsIdentity("Custom");
var userIdentity = new List<Claim>
{
new(ClaimTypes.Name, userData.UserName),
new(ClaimTypes.NameIdentifier, userData.Id.ToString()),
new(ClaimTypes.Email, userData.EmailAddress),
new(ClaimTypes.Role, "APIAuth"),
new(ClaimTypes.Role, "APIKeyAuth")
};
if (userData.IsAdmin)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsAdmin)));
}
if (userData.IsRootUser)
{
userIdentity.Add(new(ClaimTypes.Role, nameof(UserData.IsRootUser)));
}
appIdentity.AddClaims(userIdentity);
AuthenticationTicket ticket = new AuthenticationTicket(new ClaimsPrincipal(appIdentity), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
return AuthenticateResult.Fail("Invalid credentials");
}
}
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
{
if (Request.RouteValues.TryGetValue("controller", out object value))
if (Request.RouteValues.TryGetValue("controller", out object value) && value?.ToString()?.ToLower() == "api")
{
if (value.ToString().ToLower() == "api")
{
Response.StatusCode = 401;
Response.Headers.Append("WWW-Authenticate", "Basic");
return Task.CompletedTask;
}
Response.StatusCode = 401;
Response.Headers.Append("WWW-Authenticate", "Basic");
return Task.CompletedTask;
}
if (Request.Path.Value == "/Vehicle/Index" && Request.QueryString.HasValue)
{

11
Models/API/APIKey.cs Normal file
View File

@ -0,0 +1,11 @@
namespace CarCareTracker.Models
{
public class APIKey
{
public int Id { get; set; }
public int UserId { get; set; }
public string Name { get; set; }
public string Key { get; set; }
public List<HouseholdPermission> Permissions { get; set; } = new List<HouseholdPermission>();
}
}

View File

@ -62,6 +62,7 @@ if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){
builder.Services.AddSingleton<IInspectionRecordTemplateDataAccess, PGInspectionRecordTemplateDataAccess>();
builder.Services.AddSingleton<IEquipmentRecordDataAccess, PGEquipmentRecordDataAccess>();
builder.Services.AddSingleton<IUserHouseholdDataAccess, PGUserHouseholdDataAccess>();
builder.Services.AddSingleton<IApiKeyRecordDataAccess, PGApiKeyRecordDataAccess>();
}
else
{
@ -86,6 +87,7 @@ else
builder.Services.AddSingleton<IInspectionRecordTemplateDataAccess, InspectionRecordTemplateDataAccess>();
builder.Services.AddSingleton<IEquipmentRecordDataAccess, EquipmentRecordDataAccess>();
builder.Services.AddSingleton<IUserHouseholdDataAccess, UserHouseholdDataAccess>();
builder.Services.AddSingleton<IApiKeyRecordDataAccess, ApiKeyRecordDataAccess>();
}
//configure helpers

View File

@ -202,6 +202,18 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="createApiKeyModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content" id="createApiKeyModalContent">
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="userApiKeyModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content" id="userApiKeyModalContent">
</div>
</div>
</div>
<div class="stickerPrintContainer hideOnPrint">
</div>
<script>

View File

@ -35,6 +35,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning me-auto" onclick="showUserApiKeyModalFromUserModal()">@translator.Translate(userLanguage, "Manage API Keys")</button>
<button type="button" class="btn btn-secondary" onclick="hideAccountInformationModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="validateAndSaveUserAccount()" class="btn btn-primary">@translator.Translate(userLanguage, "Update")</button>
</div>

View File

@ -0,0 +1,30 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="createApiKeyLabel">@translator.Translate(userLanguage, "Create API Key")</h5>
<button type="button" class="btn-close" onclick="hideCreateApiKeyModal()" aria-label="Close"></button>
</div>
<div class="modal-body" onkeydown="handleEnter(this)">
<form class="form-inline">
<div class="form-group">
<input type="text" id="workAroundInput" style="height:0px; width:0px; display:none;">
<label for="inputApiKeyName">@translator.Translate(userLanguage, "Name")</label>
<input type="text" id="inputApiKeyName" class="form-control" placeholder="@translator.Translate(userLanguage, "Name for API Key")">
<label for="inputApiKeyRole">@translator.Translate(userLanguage, "Role")</label>
<select class="form-select" id="inputApiKeyRole">
<!option value="viewer">@translator.Translate(userLanguage, "Viewer")</!option>
<!option value="editor">@translator.Translate(userLanguage, "Editor")</!option>
<!option value="manager">@translator.Translate(userLanguage, "Manager")</!option>
</select>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideCreateApiKeyModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="createApiKey()" class="btn btn-primary">@translator.Translate(userLanguage, "Create")</button>
</div>

View File

@ -26,6 +26,7 @@
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning me-auto" onclick="showUserApiKeyModalFromUserModal()">@translator.Translate(userLanguage, "Manage API Keys")</button>
<button type="button" class="btn btn-secondary" onclick="hideAccountInformationModal()">@translator.Translate(userLanguage, "Cancel")</button>
<button type="button" onclick="validateAndSaveRootUserAccount()" class="btn btn-primary">@translator.Translate(userLanguage, "Update")</button>
</div>

View File

@ -0,0 +1,60 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model List<APIKey>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<div class="d-flex align-items-center">
<h5 class="modal-title">@translator.Translate(userLanguage, "Manage API Keys")</h5>
</div>
<div class="d-flex align-items-center ms-auto">
<button onclick="showCreateApiKeyModal()" class="btn btn-primary">
<i class="bi bi-pencil-square me-2"></i>@translator.Translate(userLanguage, "Add API Key")
</button>
<button type="button" class="btn-close ms-2" onclick="hideUserApiKeyModal()" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
@if (Model.Any())
{
<div style="max-height:30vh; overflow-y:auto;">
<table class="table table-hover">
<thead class="sticky-top">
<tr class="d-flex">
<th scope="col" class="col-6">@translator.Translate(userLanguage, "Name")</th>
<th scope="col" class="col-4">@translator.Translate(userLanguage, "Role")</th>
<th scope="col" class="col-2">@translator.Translate(userLanguage, "Remove")</th>
</tr>
</thead>
<tbody>
@foreach (APIKey viewModel in Model)
{
<tr class="d-flex">
<td class="col-6">@viewModel.Name</td>
<td class="col-4">
@if (!viewModel.Permissions.Contains(HouseholdPermission.Edit) && !viewModel.Permissions.Contains(HouseholdPermission.Delete))
{
@translator.Translate(userLanguage, "Viewer")
}
else if (viewModel.Permissions.Contains(HouseholdPermission.Edit) && !viewModel.Permissions.Contains(HouseholdPermission.Delete))
{
@translator.Translate(userLanguage, "Editor")
}
else if (viewModel.Permissions.Contains(HouseholdPermission.Edit) && viewModel.Permissions.Contains(HouseholdPermission.Delete))
{
@translator.Translate(userLanguage, "Manager")
}
</td>
<td class="col-2">
<button type="button" class="btn btn-danger" onclick="deleteApiKey(@viewModel.Id)"><i class="bi bi-trash"></i></button>
</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>

File diff suppressed because one or more lines are too long

View File

@ -661,6 +661,87 @@ function addUserToHousehold() {
});
}
function showUserApiKeyModalFromUserModal() {
$('#accountInformationModal').modal('hide');
showUserApiKeyModal();
}
function showUserApiKeyModal() {
$.get('/Home/GetUserAPIKeys', function (data) {
$('#userApiKeyModalContent').html(data);
$("#userApiKeyModal").modal('show');
});
}
function hideUserApiKeyModal() {
$('#userApiKeyModal').modal('hide');
}
function showCreateApiKeyModal() {
$.get('/Home/GetCreateApiKeyModal', function (data) {
$('#createApiKeyModalContent').html(data);
hideUserApiKeyModal();
$("#createApiKeyModal").modal('show');
});
}
function hideCreateApiKeyModal() {
$("#createApiKeyModal").modal('hide');
showUserApiKeyModal();
}
function createApiKey() {
let apiKeyName = $("#inputApiKeyName").val();
let apiKeyRole = $("#inputApiKeyRole").val();
//validate
if (apiKeyName.trim() == '') {
$("#inputApiKeyName").addClass('is-invalid');
return;
}
else {
$("#inputApiKeyName").removeClass('is-invalid');
}
let permissions = [];
switch (apiKeyRole) {
case 'editor':
permissions.push('Edit');
break;
case 'manager':
permissions.push('Edit');
permissions.push('Delete');
break;
}
$.post('/Home/CreateAPIKeyForUser', { keyName: apiKeyName, permissions: permissions }, function (data) {
if (data.success) {
$("#createApiKeyModal").modal('hide');
showUserApiKeyModal();
Swal.fire({
title: data.message,
icon: 'success',
html: `<div class="input-group"><input type="text" class="form-control" readonly value="${data.additionalData.apiKey}"><div class="input-group-text"><button type="button" class="btn btn-sm text-secondary password-visible-button" onclick="copyApiKey(this)"><i class="bi bi-copy"></i></button></div></div>`
})
} else {
errorToast(data.message);
}
});
}
function copyApiKey(elem) {
let textToCopy = $(elem).parent().siblings("input").val();
navigator.clipboard.writeText(textToCopy);
Swal.showValidationMessage(`API Key Copied to Clipboard`);
}
function deleteApiKey(keyId) {
$.post('/Home/DeleteAPIKeyForUser', { keyId: keyId }, function (data) {
if (data.success) {
successToast(data.message);
showUserApiKeyModal();
} else {
errorToast(data.message);
}
});
}
function showAccountInformationModal() {
$.get('/Home/GetUserAccountInformationModal', function (data) {
$('#accountInformationModalContent').html(data);

View File

@ -74,6 +74,11 @@ function executeAPIEndpoint(sender) {
if (hasError) {
return;
}
let currentParams = new URLSearchParams(window.location.search);
let apiKey = currentParams.get('apiKey');
if (apiKey != null) {
apiPath = `${apiPath}?apiKey=${apiKey}`;
}
let ajaxConfig = {
url: apiPath,
type: apiMethodType,