diff --git a/.idea/.idea.sanAntonioSeniorGolf/.idea/sshConfigs.xml b/.idea/.idea.sanAntonioSeniorGolf/.idea/sshConfigs.xml index 86fbd84..a4f279d 100644 --- a/.idea/.idea.sanAntonioSeniorGolf/.idea/sshConfigs.xml +++ b/.idea/.idea.sanAntonioSeniorGolf/.idea/sshConfigs.xml @@ -2,7 +2,7 @@ - + diff --git a/API/Authentication/GrantNames/UserGrantNames.cs b/API/Authentication/GrantNames/UserGrantNames.cs index 1110a96..1946aba 100644 --- a/API/Authentication/GrantNames/UserGrantNames.cs +++ b/API/Authentication/GrantNames/UserGrantNames.cs @@ -14,5 +14,7 @@ namespace API.Authentication.GrantNames public const string CanUpdatePermission = "api.user.update.permission"; public const string CanDeleteAny = "api.user.delete.any"; public const string CanDelete = "api.user.delete"; + public const string CanChangePasswordSelf = "api.user.update.password.self"; + public const string CanChangePasswordOthers = "api.user.update.password.others"; } } diff --git a/API/Authentication/Interfaces/IUserAuthentication.cs b/API/Authentication/Interfaces/IUserAuthentication.cs index 91a7ed5..2fe1b50 100644 --- a/API/Authentication/Interfaces/IUserAuthentication.cs +++ b/API/Authentication/Interfaces/IUserAuthentication.cs @@ -5,5 +5,6 @@ namespace API.Authentication.Interfaces { public interface IUserAuthentication : IGenericAuthentication { + bool canChangePassword(User destUser, User changingUser, bool oldPasswordMatchNew); } } diff --git a/API/Authentication/UserAuthentication.cs b/API/Authentication/UserAuthentication.cs index efee03e..9e5c8e2 100644 --- a/API/Authentication/UserAuthentication.cs +++ b/API/Authentication/UserAuthentication.cs @@ -39,7 +39,6 @@ namespace API.Authentication { if (!_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateSelf) || !_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateAny) - || !_grantManager.getULongValues(user.permissionId, UserGrantNames.CanUpdate).Exists(x => x == model.id) ) return false; @@ -82,5 +81,10 @@ namespace API.Authentication _grantManager.getULongValues(user.permissionId, UserGrantNames.CanDelete).Exists(x => x == model.id)) && model.id != user.id; } + public bool canChangePassword(User destUser, User changingUser, bool oldPasswordMatchNew) + { + return (destUser.id == changingUser.id && _grantManager.hasGrant(changingUser.permissionId, UserGrantNames.CanChangePasswordSelf) && oldPasswordMatchNew) || + _grantManager.hasGrant(changingUser.permissionId, UserGrantNames.CanChangePasswordOthers); + } } } diff --git a/API/Controllers/AuthController.cs b/API/Controllers/AuthController.cs index 82d4ff8..c31be8f 100644 --- a/API/Controllers/AuthController.cs +++ b/API/Controllers/AuthController.cs @@ -1,4 +1,5 @@ using API.DTO.Base; +using API.DTO.Base.Update; using API.DTO.Login; using API.Errors; using API.Services; @@ -78,6 +79,20 @@ namespace API.Controllers } } + [HttpPut("changePassword")] + public ActionResult changePassword(UserPasswordUpdateDTO passwordUpdateDTO) + { + User? user = getUser(User); + if (user == null) + return Unauthorized(); + + UserDTO? result = _userManager.changePassword(passwordUpdateDTO, user); + if (result == null) + return Forbid(); + + return result; + } + [NonAction] public User? getUser(ClaimsPrincipal user) { diff --git a/API/DTO/Base/Update/UserPasswordUpdateDTO.cs b/API/DTO/Base/Update/UserPasswordUpdateDTO.cs new file mode 100644 index 0000000..1b30b9b --- /dev/null +++ b/API/DTO/Base/Update/UserPasswordUpdateDTO.cs @@ -0,0 +1,16 @@ +using DAL.Values; +using System.ComponentModel.DataAnnotations; + +namespace API.DTO.Base.Update +{ + public class UserPasswordUpdateDTO + { + public PhoneNumber phoneNumber { get; set; } = null!; + + [MaxLength(100)] + public string? oldPassword { get; set; } + + [MaxLength(100)] + public string newPassword { get; set; } = null!; + } +} diff --git a/API/Services/Interfaces/IUserManager.cs b/API/Services/Interfaces/IUserManager.cs index 16fdb7e..83c3e50 100644 --- a/API/Services/Interfaces/IUserManager.cs +++ b/API/Services/Interfaces/IUserManager.cs @@ -1,4 +1,5 @@ using API.DTO.Base; +using API.DTO.Base.Update; using API.DTO.Login; using DAL.Models; @@ -9,5 +10,7 @@ namespace API.Services.Interfaces UserDTO? authenticateUser(UserLoginDTO loginDTO); UserDTO? registerUser(UserRegisterDTO registerDTO, User? user = null, ulong? permissionId = null); + + UserDTO? changePassword(UserPasswordUpdateDTO passwordUpdateDTO, User changingUser); } } diff --git a/API/Services/UserManager.cs b/API/Services/UserManager.cs index 2aa89f8..8c83e3d 100644 --- a/API/Services/UserManager.cs +++ b/API/Services/UserManager.cs @@ -1,4 +1,5 @@ using API.DTO.Base; +using API.DTO.Base.Update; using API.DTO.Login; using API.Hashing.Interfaces; using API.Services.Interfaces; @@ -84,5 +85,49 @@ namespace API.Services return dto; } + + public UserDTO? changePassword(UserPasswordUpdateDTO userPasswordUpdateDTO, User changingUser) + { + User? destUser = _userService.getNoAuthentication(x => x.phoneNumber.Equals(userPasswordUpdateDTO.phoneNumber)).FirstOrDefault(); + if (destUser == null) + return null; + + IHashingAlgorithm? hashingAlgorithm = _hashingFactory.getAlgorithm(_preferredHashingType); + if (hashingAlgorithm == null){ + _logger.Log(LogLevel.Error, "Preferred hashing type '{hashingType}' that isn't recognized by factory '{factory}'.", _preferredHashingType, nameof(_hashingFactory)); + return null; + } + + byte[] newSalt; + string hashedNewPassword = hashingAlgorithm.hash(userPasswordUpdateDTO.newPassword, out newSalt); + + bool oldPasswordMatchNew = false; + if (userPasswordUpdateDTO.oldPassword != null) + { + IHashingAlgorithm? userHashingAlgorithm = _hashingFactory.getAlgorithm(destUser.hashingType); + if (userHashingAlgorithm == null) + { + _logger.Log(LogLevel.Warning, "User id '{id}' has a hashing type '{hashingType}' that isn't recognized by factory '{factory}'. Not logging in.", destUser.id, destUser.hashingType, nameof(_hashingFactory)); + return null; + } + + string hashedOldPassword = userHashingAlgorithm.hash(userPasswordUpdateDTO.oldPassword, destUser.salt); + + if (hashedOldPassword.Equals(destUser.password)) + { + oldPasswordMatchNew = true; + } + } + + User? updatedUser = _userService.changePassword(destUser, changingUser, hashedNewPassword, newSalt, oldPasswordMatchNew); + + if (updatedUser == null) + return null; + + UserDTO dto = new UserDTO(); + dto.adaptFromModel(updatedUser); + + return dto; + } } } diff --git a/API/Services/UserService.cs b/API/Services/UserService.cs index 7a5ede0..ce793d9 100644 --- a/API/Services/UserService.cs +++ b/API/Services/UserService.cs @@ -1,5 +1,6 @@ using API.Authentication.Interfaces; using API.DTO.Base; +using API.DTO.Base.Update; using API.DTO.Login; using DAL.Contexts; using DAL.Models; @@ -56,5 +57,16 @@ namespace API.Services return model; } + + public User? changePassword(User destUser, User changingUser, string hashedNewPassword, byte[] newSalt, bool oldPasswordMatchNew) + { + if (!_auth.canChangePassword(destUser, changingUser, oldPasswordMatchNew)) + return null; + + destUser.password = hashedNewPassword; + destUser.salt = newSalt; + + return update(destUser, changingUser); + } } } diff --git a/Setup/Filler/Grants.sql b/Setup/Filler/Grants.sql index d58fe1e..df8b3f9 100644 --- a/Setup/Filler/Grants.sql +++ b/Setup/Filler/Grants.sql @@ -207,4 +207,20 @@ VALUES ('api.signup.delete', 1, NOW(), 1); INSERT INTO grants (name, permissionId, updated, updater) VALUES ('api.signup.add.others', 1, NOW(), 1); +INSERT INTO grants (name, permissionId, updated, updater) +VALUES ('api.user.update.password.self', 1, NOW(), 1); + +INSERT INTO grants (name, permissionId, updated, updater) +VALUES ('api.user.update.password.others', 1, NOW(), 1); + + +INSERT INTO grants (name, permissionId, updated, updater) +VALUES ('api.signup.delete.self', 1, NOW(), 1); + +INSERT INTO grants (name, permissionId, updated, updater) +VALUES ('api.user.update.password.self', 1, NOW(), 1); + +INSERT INTO grants (name, permissionId, updated, updater) +VALUES ('api.user.update.password.others', 1, NOW(), 1); + SET FOREIGN_KEY_CHECKS = 1; diff --git a/sanAntonioSeniorGolf.sln.DotSettings.user b/sanAntonioSeniorGolf.sln.DotSettings.user index 3ce4b9c..891349b 100644 --- a/sanAntonioSeniorGolf.sln.DotSettings.user +++ b/sanAntonioSeniorGolf.sln.DotSettings.user @@ -1,4 +1,5 @@  + ForceIncluded ForceIncluded ForceIncluded <SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">