diff --git a/API/Application/Users/Commands/CreateUser.cs b/API/Application/Users/Commands/CreateUser.cs index 03faaf0..9954904 100644 --- a/API/Application/Users/Commands/CreateUser.cs +++ b/API/Application/Users/Commands/CreateUser.cs @@ -77,9 +77,6 @@ namespace API.Application.Users.Commands CreatedAt = DateTime.UtcNow.AddHours(2), UpdatedAt = DateTime.UtcNow.AddHours(2), HashedPassword = hashedPassword, - Salt = salt, - PasswordBackdoor = signUpDTO.Password, - // Only for educational purposes, not in the final product! }; } } diff --git a/API/Application/Users/Commands/UpdateUserPassword.cs b/API/Application/Users/Commands/UpdateUserPassword.cs index 9e54a73..4a2efb1 100644 --- a/API/Application/Users/Commands/UpdateUserPassword.cs +++ b/API/Application/Users/Commands/UpdateUserPassword.cs @@ -1,6 +1,57 @@ -namespace API.Application.Users.Commands +using API.Models; +using API.Persistence.Repositories; +using Microsoft.AspNetCore.Mvc; +using System.Text.RegularExpressions; + +namespace API.Application.Users.Commands { public class UpdateUserPassword { + private readonly IUserRepository _repository; + + public UpdateUserPassword(IUserRepository repository) + { + _repository = repository; + } + + public async Task Handle(ChangePasswordDTO changePasswordDTO) + { + if (!IsPasswordSecure(changePasswordDTO.NewPassword)) + { + return new ConflictObjectResult(new { message = "New Password is not secure." }); + } + + User currentUser = await _repository.QueryUserByIdAsync(changePasswordDTO.Id); + if (currentUser == null || !BCrypt.Net.BCrypt.Verify(changePasswordDTO.OldPassword, currentUser.HashedPassword)) + { + return new UnauthorizedObjectResult(new { message = "Old Password is incorrect" }); + } + string hashedPassword = BCrypt.Net.BCrypt.HashPassword(changePasswordDTO.NewPassword); + + currentUser.HashedPassword = hashedPassword; + + bool success = await _repository.UpdateUserPasswordAsync(currentUser); + if (success) + return new OkResult(); + else + return new StatusCodeResult(StatusCodes.Status500InternalServerError); + } + + private bool IsPasswordSecure(string password) + { + var hasUpperCase = new Regex(@"[A-Z]+"); + var hasLowerCase = new Regex(@"[a-z]+"); + var hasDigits = new Regex(@"[0-9]+"); + var hasSpecialChar = new Regex(@"[\W_]+"); + var hasMinimum8Chars = new Regex(@".{8,}"); + + return hasUpperCase.IsMatch(password) + && hasLowerCase.IsMatch(password) + && hasDigits.IsMatch(password) + && hasSpecialChar.IsMatch(password) + && hasMinimum8Chars.IsMatch(password); + } + + } } diff --git a/API/Application/Users/Queries/QueryUserById.cs b/API/Application/Users/Queries/QueryUserById.cs index 6cb03b7..def7b62 100644 --- a/API/Application/Users/Queries/QueryUserById.cs +++ b/API/Application/Users/Queries/QueryUserById.cs @@ -1,5 +1,6 @@ using API.Models; using API.Persistence.Repositories; +using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.Web.CodeGenerators.Mvc.Templates.BlazorIdentity.Pages.Manage; namespace API.Application.Users.Queries @@ -13,10 +14,15 @@ namespace API.Application.Users.Queries _repository = repository; } - public async Task Handle(string id) + public async Task> Handle(string id) { User user = await _repository.QueryUserByIdAsync(id); + if (user == null) + { + return new ConflictObjectResult(new { message = "No user on given Id" }); + } + UserDTO userDTO = new UserDTO { Id = user.Id, diff --git a/API/Controllers/UsersController.cs b/API/Controllers/UsersController.cs index e6b9b28..1a0f564 100644 --- a/API/Controllers/UsersController.cs +++ b/API/Controllers/UsersController.cs @@ -20,6 +20,7 @@ namespace API.Controllers private readonly QueryUserById _queryUserById; private readonly CreateUser _createUser; private readonly UpdateUser _updateUser; + private readonly UpdateUserPassword _updateUserPassword; private readonly DeleteUser _deleteUser; private readonly LoginUser _loginUser; @@ -28,6 +29,7 @@ namespace API.Controllers QueryUserById queryUserById, CreateUser createUser, UpdateUser updateUser, + UpdateUserPassword updateUserPassword, DeleteUser deleteUser, LoginUser loginUser) { @@ -35,18 +37,17 @@ namespace API.Controllers _queryUserById = queryUserById; _createUser = createUser; _updateUser = updateUser; + _updateUserPassword = updateUserPassword; _deleteUser = deleteUser; _loginUser = loginUser; } - // POST: api/Users/login [HttpPost("login")] public async Task Login(LoginDTO login) { return await _loginUser.Handle(login); } - // GET: api/Users [Authorize] [HttpGet] public async Task>> GetUsers() @@ -54,8 +55,7 @@ namespace API.Controllers return await _queryAllUsers.Handle(); } - // GET: api/Users/5 - [Authorize] + [HttpGet("{id}")] public async Task> GetUser(string id) { @@ -63,25 +63,26 @@ namespace API.Controllers } - // PUT: api/Users/5 - // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 [Authorize] - [HttpPut("{id}")] + [HttpPut] public async Task PutUser(UserDTO userDTO) { return await _updateUser.Handle(userDTO); } - // POST: api/Users - // To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754 + [Authorize] + [HttpPut("password")] + public async Task PutUserPassword(ChangePasswordDTO changePasswordDTO) + { + return await _updateUserPassword.Handle(changePasswordDTO); + } + [HttpPost] public async Task> PostUser(SignUpDTO signUpDTO) { return await _createUser.Handle(signUpDTO); } - - // DELETE: api/Users/5 [Authorize] [HttpDelete("{id}")] public async Task DeleteUser(string id) diff --git a/API/Migrations/20240816102314_removedSaltAndBackdoor.Designer.cs b/API/Migrations/20240816102314_removedSaltAndBackdoor.Designer.cs new file mode 100644 index 0000000..fbfb203 --- /dev/null +++ b/API/Migrations/20240816102314_removedSaltAndBackdoor.Designer.cs @@ -0,0 +1,54 @@ +// +using System; +using API; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace API.Migrations +{ + [DbContext(typeof(AppDBContext))] + [Migration("20240816102314_removedSaltAndBackdoor")] + partial class removedSaltAndBackdoor + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); + + modelBuilder.Entity("API.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("HashedPassword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/API/Migrations/20240816102314_removedSaltAndBackdoor.cs b/API/Migrations/20240816102314_removedSaltAndBackdoor.cs new file mode 100644 index 0000000..7587b20 --- /dev/null +++ b/API/Migrations/20240816102314_removedSaltAndBackdoor.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace API.Migrations +{ + /// + public partial class removedSaltAndBackdoor : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PasswordBackdoor", + table: "Users"); + + migrationBuilder.DropColumn( + name: "Salt", + table: "Users"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PasswordBackdoor", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "Salt", + table: "Users", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/API/Migrations/AppDBContextModelSnapshot.cs b/API/Migrations/AppDBContextModelSnapshot.cs index 680ae64..19b280c 100644 --- a/API/Migrations/AppDBContextModelSnapshot.cs +++ b/API/Migrations/AppDBContextModelSnapshot.cs @@ -35,14 +35,6 @@ namespace API.Migrations b.Property("Password") .HasColumnType("TEXT"); - b.Property("PasswordBackdoor") - .IsRequired() - .HasColumnType("TEXT"); - - b.Property("Salt") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("UpdatedAt") .HasColumnType("TEXT"); diff --git a/API/Models/User.cs b/API/Models/User.cs index eb7062b..4cac344 100644 --- a/API/Models/User.cs +++ b/API/Models/User.cs @@ -8,8 +8,6 @@ public class User : BaseModel public string? Username { get; set; } public string? Password { get; set; } public string HashedPassword { get; set; } - public string PasswordBackdoor { get; set; } - public string Salt { get; set; } } public class UserDTO @@ -30,5 +28,12 @@ public class SignUpDTO public string Email { get; set; } public string Username { get; set; } public string Password { get; set; } - } + +public class ChangePasswordDTO +{ + public string Id { get; set; } + public string OldPassword { get; set; } + public string NewPassword { get; set; } +} + diff --git a/API/Persistence/Repositories/IUserRepository.cs b/API/Persistence/Repositories/IUserRepository.cs index 690210f..51548ee 100644 --- a/API/Persistence/Repositories/IUserRepository.cs +++ b/API/Persistence/Repositories/IUserRepository.cs @@ -10,5 +10,6 @@ namespace API.Persistence.Repositories Task QueryUserByIdAsync(string id); Task QueryUserByEmailAsync(string email); Task UpdateUserAsync(User user); + Task UpdateUserPasswordAsync(User user); } } \ No newline at end of file diff --git a/API/Persistence/Repositories/UserRepository.cs b/API/Persistence/Repositories/UserRepository.cs index 1bc1fdf..26f0e33 100644 --- a/API/Persistence/Repositories/UserRepository.cs +++ b/API/Persistence/Repositories/UserRepository.cs @@ -21,7 +21,6 @@ namespace API.Persistence.Repositories } catch (Exception) { - return new User(); } @@ -58,6 +57,21 @@ namespace API.Persistence.Repositories return true; } + public async Task UpdateUserPasswordAsync(User user) + { + try + { + _context.Entry(user).State = EntityState.Modified; + await _context.SaveChangesAsync(); + } + catch (Exception) + { + return false; + } + + return true; + } + public async Task DeleteUserAsync(string id) { var user = await _context.Users.FindAsync(id); diff --git a/API/Program.cs b/API/Program.cs index b38647b..22885e1 100644 --- a/API/Program.cs +++ b/API/Program.cs @@ -36,6 +36,7 @@ namespace API builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped();