Added functionality to change password

This commit is contained in:
quentin 2024-12-19 19:33:45 -06:00
parent 0b88ebd2e8
commit 93ee75d021
11 changed files with 117 additions and 2 deletions

View File

@ -2,7 +2,7 @@
<project version="4"> <project version="4">
<component name="SshConfigs"> <component name="SshConfigs">
<configs> <configs>
<sshConfig host="4.72.148.132.host.secureserver.net" id="803b7675-648e-4d0f-8433-0abf8725c35f" keyPath="$USER_HOME$/downloads/id" port="22" nameFormat="DESCRIPTIVE" username="dotnet" useOpenSSHConfig="false" /> <sshConfig host="4.72.148.132.host.secureserver.net" id="803b7675-648e-4d0f-8433-0abf8725c35f" keyPath="$USER_HOME$/.ssh/dotnetSanAntonioSeniorGolf" port="22" nameFormat="DESCRIPTIVE" username="dotnet" useOpenSSHConfig="false" />
<sshConfig host="4.72.148.132.host.secureserver.net" id="7db7f912-3e0a-4cfc-90fe-563abe88fa2c" keyPath="$USER_HOME$/.ssh/sanantonioseniorgolf" port="22" nameFormat="DESCRIPTIVE" username="oaksana" useOpenSSHConfig="true" /> <sshConfig host="4.72.148.132.host.secureserver.net" id="7db7f912-3e0a-4cfc-90fe-563abe88fa2c" keyPath="$USER_HOME$/.ssh/sanantonioseniorgolf" port="22" nameFormat="DESCRIPTIVE" username="oaksana" useOpenSSHConfig="true" />
<sshConfig host="192.168.1.52" id="e58264ea-75c0-4f9e-aa5c-b6ece113fffb" keyPath="$USER_HOME$/.ssh/dotnet" port="22" nameFormat="DESCRIPTIVE" username="dotnet" /> <sshConfig host="192.168.1.52" id="e58264ea-75c0-4f9e-aa5c-b6ece113fffb" keyPath="$USER_HOME$/.ssh/dotnet" port="22" nameFormat="DESCRIPTIVE" username="dotnet" />
</configs> </configs>

View File

@ -14,5 +14,7 @@ namespace API.Authentication.GrantNames
public const string CanUpdatePermission = "api.user.update.permission"; public const string CanUpdatePermission = "api.user.update.permission";
public const string CanDeleteAny = "api.user.delete.any"; public const string CanDeleteAny = "api.user.delete.any";
public const string CanDelete = "api.user.delete"; 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";
} }
} }

View File

@ -5,5 +5,6 @@ namespace API.Authentication.Interfaces
{ {
public interface IUserAuthentication : IGenericAuthentication<UserDTO, User> public interface IUserAuthentication : IGenericAuthentication<UserDTO, User>
{ {
bool canChangePassword(User destUser, User changingUser, bool oldPasswordMatchNew);
} }
} }

View File

@ -39,7 +39,6 @@ namespace API.Authentication
{ {
if (!_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateSelf) if (!_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateSelf)
|| !_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateAny) || !_grantManager.hasGrant(user.permissionId, UserGrantNames.CanUpdateAny)
|| !_grantManager.getULongValues(user.permissionId, UserGrantNames.CanUpdate).Exists(x => x == model.id)
) )
return false; return false;
@ -82,5 +81,10 @@ namespace API.Authentication
_grantManager.getULongValues(user.permissionId, UserGrantNames.CanDelete).Exists(x => x == model.id)) _grantManager.getULongValues(user.permissionId, UserGrantNames.CanDelete).Exists(x => x == model.id))
&& model.id != user.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);
}
} }
} }

View File

@ -1,4 +1,5 @@
using API.DTO.Base; using API.DTO.Base;
using API.DTO.Base.Update;
using API.DTO.Login; using API.DTO.Login;
using API.Errors; using API.Errors;
using API.Services; using API.Services;
@ -78,6 +79,20 @@ namespace API.Controllers
} }
} }
[HttpPut("changePassword")]
public ActionResult<UserDTO> 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] [NonAction]
public User? getUser(ClaimsPrincipal user) public User? getUser(ClaimsPrincipal user)
{ {

View File

@ -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!;
}
}

View File

@ -1,4 +1,5 @@
using API.DTO.Base; using API.DTO.Base;
using API.DTO.Base.Update;
using API.DTO.Login; using API.DTO.Login;
using DAL.Models; using DAL.Models;
@ -9,5 +10,7 @@ namespace API.Services.Interfaces
UserDTO? authenticateUser(UserLoginDTO loginDTO); UserDTO? authenticateUser(UserLoginDTO loginDTO);
UserDTO? registerUser(UserRegisterDTO registerDTO, User? user = null, ulong? permissionId = null); UserDTO? registerUser(UserRegisterDTO registerDTO, User? user = null, ulong? permissionId = null);
UserDTO? changePassword(UserPasswordUpdateDTO passwordUpdateDTO, User changingUser);
} }
} }

View File

@ -1,4 +1,5 @@
using API.DTO.Base; using API.DTO.Base;
using API.DTO.Base.Update;
using API.DTO.Login; using API.DTO.Login;
using API.Hashing.Interfaces; using API.Hashing.Interfaces;
using API.Services.Interfaces; using API.Services.Interfaces;
@ -84,5 +85,49 @@ namespace API.Services
return dto; 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;
}
} }
} }

View File

@ -1,5 +1,6 @@
using API.Authentication.Interfaces; using API.Authentication.Interfaces;
using API.DTO.Base; using API.DTO.Base;
using API.DTO.Base.Update;
using API.DTO.Login; using API.DTO.Login;
using DAL.Contexts; using DAL.Contexts;
using DAL.Models; using DAL.Models;
@ -56,5 +57,16 @@ namespace API.Services
return model; 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);
}
} }
} }

View File

@ -207,4 +207,20 @@ VALUES ('api.signup.delete', 1, NOW(), 1);
INSERT INTO grants (name, permissionId, updated, updater) INSERT INTO grants (name, permissionId, updated, updater)
VALUES ('api.signup.add.others', 1, NOW(), 1); 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; SET FOREIGN_KEY_CHECKS = 1;

View File

@ -1,4 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AActionMethodExecutor_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fdb7395f4add94e6d10e515b3e55373f2821f8323de7dc8e314d78feefacf5584_003FActionMethodExecutor_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHttpContextExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Feb8b15bbf6ed38ef49b0b77ac383bba3c22ce8dbcff5a4798cdebe3013d9ea_003FAuthenticationHttpContextExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHttpContextExtensions_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003Feb8b15bbf6ed38ef49b0b77ac383bba3c22ce8dbcff5a4798cdebe3013d9ea_003FAuthenticationHttpContextExtensions_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3bd4df5aff92cabbc4d630be64227073db1b8539b3a1e47786b4b189d7cdb7_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADbContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2024_002E2_003Fresharper_002Dhost_003FSourcesCache_003F3bd4df5aff92cabbc4d630be64227073db1b8539b3a1e47786b4b189d7cdb7_003FDbContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d51071ba_002D6946_002D464f_002Db1ff_002D8183035b48e5/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt; <s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d51071ba_002D6946_002D464f_002Db1ff_002D8183035b48e5/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="Test1" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;