diff --git a/Controllers/AdminController.cs b/Controllers/AdminController.cs index b49a0e3..2156b90 100644 --- a/Controllers/AdminController.cs +++ b/Controllers/AdminController.cs @@ -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] diff --git a/External/Implementations/Litedb/ApiKeyRecordDataAccess.cs b/External/Implementations/Litedb/ApiKeyRecordDataAccess.cs new file mode 100644 index 0000000..3e270bb --- /dev/null +++ b/External/Implementations/Litedb/ApiKeyRecordDataAccess.cs @@ -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 GetAPIKeyRecordsByUserId(int userId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + var apiKeyRecords = table.Find(Query.EQ(nameof(APIKey.UserId), userId)); + return apiKeyRecords.ToList() ?? new List(); + } + public APIKey GetAPIKeyById(int apiKeyId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(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(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(tableName); + table.Upsert(apiKey); + db.Checkpoint(); + return true; + } + public bool DeleteAPIKeyById(int apiKeyId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + table.Delete(apiKeyId); + db.Checkpoint(); + return true; + } + public bool DeleteAllAPIKeysByUserId(int userId) + { + var db = _liteDB.GetLiteDB(); + var table = db.GetCollection(tableName); + var apiKeyRecords = table.DeleteMany(Query.EQ(nameof(APIKey.UserId), userId)); + db.Checkpoint(); + return true; + } + } +} \ No newline at end of file diff --git a/External/Implementations/Postgres/ApiKeyRecordDataAccess.cs b/External/Implementations/Postgres/ApiKeyRecordDataAccess.cs new file mode 100644 index 0000000..56ea429 --- /dev/null +++ b/External/Implementations/Postgres/ApiKeyRecordDataAccess.cs @@ -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 _logger; + private static string tableName = "apikeyrecords"; + public PGApiKeyRecordDataAccess(IConfiguration config, ILogger logger) + { + pgDataSource = NpgsqlDataSource.Create(config["POSTGRES_CONNECTION"]); + + _logger = logger; + try + { + //create table if not exist. + string initCMD = $"CREATE SCHEMA IF NOT EXISTS app; CREATE TABLE IF NOT EXISTS app.{tableName} (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 GetAPIKeyRecordsByUserId(int userId) + { + try + { + string cmd = $"SELECT data FROM app.{tableName} WHERE userId = @userId"; + var results = new List(); + using (var ctext = pgDataSource.CreateCommand(cmd)) + { + ctext.Parameters.AddWithValue("userId", userId); + using (NpgsqlDataReader reader = ctext.ExecuteReader()) + while (reader.Read()) + { + APIKey apiKeyRecord = JsonSerializer.Deserialize(reader["data"] as string); + results.Add(apiKeyRecord); + } + } + return results; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return new List(); + } + } + 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(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(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; + } + } + } +} diff --git a/External/Interfaces/IApiKeyRecordDataAccess.cs b/External/Interfaces/IApiKeyRecordDataAccess.cs index 6ae8b88..3e7fc72 100644 --- a/External/Interfaces/IApiKeyRecordDataAccess.cs +++ b/External/Interfaces/IApiKeyRecordDataAccess.cs @@ -7,7 +7,8 @@ namespace CarCareTracker.External.Interfaces public List GetAPIKeyRecordsByUserId(int userId); public APIKey GetAPIKeyById(int apiKeyId); public APIKey GetAPIKeyByKey(string hashedKey); - public bool CreateNewAPIKey(APIKey apiKey); - public bool DeleteAPIKey(int apiKeyId); + public bool SaveAPIKey(APIKey apiKey); + public bool DeleteAPIKeyById(int apiKeyId); + public bool DeleteAllAPIKeysByUserId(int userId); } } \ No newline at end of file diff --git a/Helper/StaticHelper.cs b/Helper/StaticHelper.cs index 0cbee98..973a676 100644 --- a/Helper/StaticHelper.cs +++ b/Helper/StaticHelper.cs @@ -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(); + } } } diff --git a/Logic/LoginLogic.cs b/Logic/LoginLogic.cs index 22d2491..8f545dc 100644 --- a/Logic/LoginLogic.cs +++ b/Logic/LoginLogic.cs @@ -103,7 +103,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 +136,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 +178,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 +235,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 +262,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; @@ -376,7 +376,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 +399,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 +412,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 +438,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 +454,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); diff --git a/Logic/UserLogic.cs b/Logic/UserLogic.cs index 727403f..1156682 100644 --- a/Logic/UserLogic.cs +++ b/Logic/UserLogic.cs @@ -1,4 +1,5 @@ using CarCareTracker.External.Interfaces; +using CarCareTracker.Helper; using CarCareTracker.Models; namespace CarCareTracker.Logic @@ -22,18 +23,24 @@ namespace CarCareTracker.Logic bool DeleteUserFromHousehold(int parentUserId, int childUserId); bool DeleteAllHouseholdByParentUserId(int parentUserId); bool DeleteAllHouseholdByChildUserId(int childUserId); + OperationResponse CreateAPIKey(int userId, string keyName, List permisions); + bool DeleteAPIKeyByKeyId(int keyId); + 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 GetCollaboratorsForVehicle(int vehicleId) { @@ -287,5 +294,33 @@ namespace CarCareTracker.Logic var result = _userHouseholdData.DeleteAllHouseholdRecordsByChildUserId(childUserId); return result; } + public OperationResponse CreateAPIKey(int userId, string keyName, List 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; + } } } diff --git a/Program.cs b/Program.cs index 9c73676..f7ce5bf 100644 --- a/Program.cs +++ b/Program.cs @@ -62,6 +62,7 @@ if (!string.IsNullOrWhiteSpace(builder.Configuration["POSTGRES_CONNECTION"])){ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } else { @@ -86,6 +87,7 @@ else builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); } //configure helpers