logic layer to manage api keys

This commit is contained in:
DESKTOP-T0O5CDB\DESK-555BD 2026-01-24 10:24:10 -07:00
parent 8151747b66
commit 9403ea437e
8 changed files with 319 additions and 31 deletions

View File

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

View File

@ -0,0 +1,62 @@
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);
var apiKeyRecord = table.FindOne(Query.EQ(nameof(APIKey.Id), apiKeyId));
return apiKeyRecord ?? new APIKey();
}
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

@ -7,7 +7,8 @@ namespace CarCareTracker.External.Interfaces
public List<APIKey> GetAPIKeyRecordsByUserId(int userId); public List<APIKey> GetAPIKeyRecordsByUserId(int userId);
public APIKey GetAPIKeyById(int apiKeyId); public APIKey GetAPIKeyById(int apiKeyId);
public APIKey GetAPIKeyByKey(string hashedKey); public APIKey GetAPIKeyByKey(string hashedKey);
public bool CreateNewAPIKey(APIKey apiKey); public bool SaveAPIKey(APIKey apiKey);
public bool DeleteAPIKey(int apiKeyId); public bool DeleteAPIKeyById(int apiKeyId);
public bool DeleteAllAPIKeysByUserId(int userId);
} }
} }

View File

@ -992,5 +992,20 @@ namespace CarCareTracker.Helper
return "primary"; 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

@ -103,7 +103,7 @@ namespace CarCareTracker.Logic
if (!string.IsNullOrWhiteSpace(credentials.Password)) if (!string.IsNullOrWhiteSpace(credentials.Password))
{ {
//update password //update password
existingUser.Password = GetHash(credentials.Password); existingUser.Password = StaticHelper.GetHash(credentials.Password);
} }
//delete token //delete token
_tokenData.DeleteToken(existingToken.Id); _tokenData.DeleteToken(existingToken.Id);
@ -136,7 +136,7 @@ namespace CarCareTracker.Logic
var newUser = new UserData() var newUser = new UserData()
{ {
UserName = credentials.UserName, 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 EmailAddress = credentials.EmailAddress
}; };
var result = _userData.SaveUserRecord(newUser); var result = _userData.SaveUserRecord(newUser);
@ -178,7 +178,7 @@ namespace CarCareTracker.Logic
var newUser = new UserData() var newUser = new UserData()
{ {
UserName = credentials.UserName, UserName = credentials.UserName,
Password = GetHash(credentials.Password), Password = StaticHelper.GetHash(credentials.Password),
EmailAddress = credentials.EmailAddress EmailAddress = credentials.EmailAddress
}; };
var result = _userData.SaveUserRecord(newUser); var result = _userData.SaveUserRecord(newUser);
@ -235,7 +235,7 @@ namespace CarCareTracker.Logic
{ {
return OperationResponse.Failed("Unable to locate user"); return OperationResponse.Failed("Unable to locate user");
} }
existingUser.Password = GetHash(credentials.Password); existingUser.Password = StaticHelper.GetHash(credentials.Password);
var result = _userData.SaveUserRecord(existingUser); var result = _userData.SaveUserRecord(existingUser);
//delete token //delete token
_tokenData.DeleteToken(existingToken.Id); _tokenData.DeleteToken(existingToken.Id);
@ -262,7 +262,7 @@ namespace CarCareTracker.Logic
{ {
//authenticate via DB. //authenticate via DB.
var result = _userData.GetUserRecordByUserName(credentials.UserName); var result = _userData.GetUserRecordByUserName(credentials.UserName);
if (GetHash(credentials.Password) == result.Password) if (StaticHelper.GetHash(credentials.Password) == result.Password)
{ {
result.Password = string.Empty; result.Password = string.Empty;
return result; return result;
@ -376,7 +376,7 @@ namespace CarCareTracker.Logic
return OperationResponse.Failed("Unable to find user"); return OperationResponse.Failed("Unable to find user");
} }
var newPassword = Guid.NewGuid().ToString().Substring(0, 8); var newPassword = Guid.NewGuid().ToString().Substring(0, 8);
existingUser.Password = GetHash(newPassword); existingUser.Password = StaticHelper.GetHash(newPassword);
var result = _userData.SaveUserRecord(existingUser); var result = _userData.SaveUserRecord(existingUser);
if (result) if (result)
{ {
@ -399,8 +399,8 @@ namespace CarCareTracker.Logic
if (existingUserConfig is not null) if (existingUserConfig is not null)
{ {
//create hashes of the login credentials. //create hashes of the login credentials.
var hashedUserName = GetHash(credentials.UserName); var hashedUserName = StaticHelper.GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password); var hashedPassword = StaticHelper.GetHash(credentials.Password);
//copy over settings that are off limits on the settings page. //copy over settings that are off limits on the settings page.
existingUserConfig.EnableAuth = true; existingUserConfig.EnableAuth = true;
existingUserConfig.UserNameHash = hashedUserName; existingUserConfig.UserNameHash = hashedUserName;
@ -412,8 +412,8 @@ namespace CarCareTracker.Logic
var newUserConfig = new UserConfig() var newUserConfig = new UserConfig()
{ {
EnableAuth = true, EnableAuth = true,
UserNameHash = GetHash(credentials.UserName), UserNameHash = StaticHelper.GetHash(credentials.UserName),
UserPasswordHash = GetHash(credentials.Password) UserPasswordHash = StaticHelper.GetHash(credentials.Password)
}; };
File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig)); File.WriteAllText(StaticHelper.UserConfigPath, JsonSerializer.Serialize(newUserConfig));
} }
@ -438,8 +438,8 @@ namespace CarCareTracker.Logic
} }
private bool UserIsRoot(LoginModel credentials) private bool UserIsRoot(LoginModel credentials)
{ {
var hashedUserName = GetHash(credentials.UserName); var hashedUserName = StaticHelper.GetHash(credentials.UserName);
var hashedPassword = GetHash(credentials.Password); var hashedPassword = StaticHelper.GetHash(credentials.Password);
return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword); return _configHelper.AuthenticateRootUser(hashedUserName, hashedPassword);
} }
private UserData GetRootUserData(string username) private UserData GetRootUserData(string username)
@ -454,21 +454,7 @@ namespace CarCareTracker.Logic
}; };
} }
#endregion #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() private string NewToken()
{ {
return Guid.NewGuid().ToString().Substring(0, 8); return Guid.NewGuid().ToString().Substring(0, 8);

View File

@ -1,4 +1,5 @@
using CarCareTracker.External.Interfaces; using CarCareTracker.External.Interfaces;
using CarCareTracker.Helper;
using CarCareTracker.Models; using CarCareTracker.Models;
namespace CarCareTracker.Logic namespace CarCareTracker.Logic
@ -22,18 +23,24 @@ namespace CarCareTracker.Logic
bool DeleteUserFromHousehold(int parentUserId, int childUserId); bool DeleteUserFromHousehold(int parentUserId, int childUserId);
bool DeleteAllHouseholdByParentUserId(int parentUserId); bool DeleteAllHouseholdByParentUserId(int parentUserId);
bool DeleteAllHouseholdByChildUserId(int childUserId); bool DeleteAllHouseholdByChildUserId(int childUserId);
OperationResponse CreateAPIKey(int userId, string keyName, List<HouseholdPermission> permisions);
bool DeleteAPIKeyByKeyId(int keyId);
bool DeleteAllAPIKeysByUserId(int userId);
} }
public class UserLogic: IUserLogic public class UserLogic: IUserLogic
{ {
private readonly IUserAccessDataAccess _userAccess; private readonly IUserAccessDataAccess _userAccess;
private readonly IUserRecordDataAccess _userData; private readonly IUserRecordDataAccess _userData;
private readonly IUserHouseholdDataAccess _userHouseholdData; private readonly IUserHouseholdDataAccess _userHouseholdData;
private readonly IApiKeyRecordDataAccess _apiKeyData;
public UserLogic(IUserAccessDataAccess userAccess, public UserLogic(IUserAccessDataAccess userAccess,
IUserRecordDataAccess userData, IUserRecordDataAccess userData,
IUserHouseholdDataAccess userHouseholdData) { IUserHouseholdDataAccess userHouseholdData,
IApiKeyRecordDataAccess apiKeyData) {
_userAccess = userAccess; _userAccess = userAccess;
_userData = userData; _userData = userData;
_userHouseholdData = userHouseholdData; _userHouseholdData = userHouseholdData;
_apiKeyData = apiKeyData;
} }
public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId) public List<UserCollaborator> GetCollaboratorsForVehicle(int vehicleId)
{ {
@ -287,5 +294,33 @@ namespace CarCareTracker.Logic
var result = _userHouseholdData.DeleteAllHouseholdRecordsByChildUserId(childUserId); var result = _userHouseholdData.DeleteAllHouseholdRecordsByChildUserId(childUserId);
return result; return result;
} }
public OperationResponse CreateAPIKey(int userId, string keyName, List<HouseholdPermission> permisions)
{
//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
};
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 bool DeleteAPIKeyByKeyId(int keyId)
{
var result = _apiKeyData.DeleteAPIKeyById(keyId);
return result;
}
public bool DeleteAllAPIKeysByUserId(int userId)
{
var result = _apiKeyData.DeleteAllAPIKeysByUserId(userId);
return result;
}
} }
} }

View File

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