diff --git a/AceJobAgency/Controllers/UserController.cs b/AceJobAgency/Controllers/UserController.cs index bed6426..731ca1e 100644 --- a/AceJobAgency/Controllers/UserController.cs +++ b/AceJobAgency/Controllers/UserController.cs @@ -178,12 +178,29 @@ namespace AceJobAgency.Controllers return BadRequest("Current password is incorrect."); } + if (BCrypt.Net.BCrypt.Verify(request.NewPassword, user.Password)) + { + new ActivityLogController(_context).LogUserActivity(user.Id, "Change password failed: Tried changing to the same password", ipAddress); + return BadRequest("New password can't be the same as the old password."); + } + if (!AccountManagement.IsPasswordComplex(request.NewPassword)) { new ActivityLogController(_context).LogUserActivity(user.Id, "Change password failed: Password not complex enough", ipAddress); return BadRequest("Password must be at least 12 characters long and include uppercase, lowercase, number, and special character."); } + if ((!string.IsNullOrEmpty(user.Password) && BCrypt.Net.BCrypt.Verify(request.NewPassword, user.Password)) || + (!string.IsNullOrEmpty(user.PreviousPassword1) && BCrypt.Net.BCrypt.Verify(request.NewPassword, user.PreviousPassword1)) || + (!string.IsNullOrEmpty(user.PreviousPassword2) && BCrypt.Net.BCrypt.Verify(request.NewPassword, user.PreviousPassword2))) + { + new ActivityLogController(_context).LogUserActivity(user.Id, "Change password failed: New password matches one of the previous passwords", ipAddress); + return BadRequest("New password cannot be one of the last two passwords."); + } + + user.PreviousPassword2 = user.PreviousPassword1; + user.PreviousPassword1 = user.Password; + user.Password = BCrypt.Net.BCrypt.HashPassword(request.NewPassword); user.UpdatedAt = DateTime.Now; _context.Users.Update(user); diff --git a/AceJobAgency/Entities/User.cs b/AceJobAgency/Entities/User.cs index f38ab36..638f0df 100644 --- a/AceJobAgency/Entities/User.cs +++ b/AceJobAgency/Entities/User.cs @@ -57,5 +57,13 @@ namespace AceJobAgency.Entities [DataType(DataType.DateTime)] public DateTime? LockoutEndTime { get; set; } + + [DataType(DataType.Password)] + [MaxLength(128)] + public string PreviousPassword1 { get; set; } = string.Empty; + + [DataType(DataType.Password)] + [MaxLength(128)] + public string PreviousPassword2 { get; set; } = string.Empty; } } \ No newline at end of file diff --git a/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.Designer.cs b/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.Designer.cs new file mode 100644 index 0000000..2acd4eb --- /dev/null +++ b/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.Designer.cs @@ -0,0 +1,139 @@ +// +using System; +using AceJobAgency.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AceJobAgency.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250211015120_AddedTwoPasswordHistories")] + partial class AddedTwoPasswordHistories + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("AceJobAgency.Entities.ActivityLog", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + + b.Property("Activity") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IpAddress") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("varchar(15)"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + + b.HasKey("Id"); + + b.ToTable("ActivityLogs"); + }); + + modelBuilder.Entity("AceJobAgency.Entities.User", b => + { + b.Property("Id") + .HasMaxLength(36) + .HasColumnType("varchar(36)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("FailedLoginAttempts") + .HasColumnType("int"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Gender") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("int"); + + b.Property("IsLockedOut") + .HasColumnType("tinyint(1)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LockoutEndTime") + .HasColumnType("datetime(6)"); + + b.Property("NationalRegistrationIdentityCardNumber") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("PreviousPassword1") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("PreviousPassword2") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("ResumeName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WhoAmI") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("varchar(2048)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.cs b/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.cs new file mode 100644 index 0000000..1eb73b4 --- /dev/null +++ b/AceJobAgency/Migrations/20250211015120_AddedTwoPasswordHistories.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AceJobAgency.Migrations +{ + /// + public partial class AddedTwoPasswordHistories : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "PreviousPassword1", + table: "Users", + type: "varchar(128)", + maxLength: 128, + nullable: false, + defaultValue: "") + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "PreviousPassword2", + table: "Users", + type: "varchar(128)", + maxLength: 128, + nullable: false, + defaultValue: "") + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "PreviousPassword1", + table: "Users"); + + migrationBuilder.DropColumn( + name: "PreviousPassword2", + table: "Users"); + } + } +} diff --git a/AceJobAgency/Migrations/DataContextModelSnapshot.cs b/AceJobAgency/Migrations/DataContextModelSnapshot.cs index 720725e..828df83 100644 --- a/AceJobAgency/Migrations/DataContextModelSnapshot.cs +++ b/AceJobAgency/Migrations/DataContextModelSnapshot.cs @@ -103,6 +103,16 @@ namespace AceJobAgency.Migrations .HasMaxLength(128) .HasColumnType("varchar(128)"); + b.Property("PreviousPassword1") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("PreviousPassword2") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + b.Property("ResumeName") .IsRequired() .HasMaxLength(128)