account lockout

This commit is contained in:
2025-02-09 13:21:57 +08:00
parent e7c92f252f
commit a43957a7c2
8 changed files with 199 additions and 14 deletions

View File

@@ -27,8 +27,8 @@ export default function LoginView({ onSignup }: { onSignup: () => void }) {
try { try {
const response = await http.post("/User/login", loginRequest); const response = await http.post("/User/login", loginRequest);
if (response.status !== 200) { if (response.status === 401) {
throw new Error("Login failed"); throw new Error("Invalid email or password.");
} }
const { token } = response.data; const { token } = response.data;

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback } from "react"; import React, { useState, useEffect, useCallback } from "react";
import { ToastContainer, toast } from "react-toastify"; import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css"; import "react-toastify/dist/ReactToastify.css";
interface SessionTimeoutProps { interface SessionTimeoutProps {
@@ -47,11 +47,7 @@ const SessionTimeout: React.FC<SessionTimeoutProps> = ({
return () => clearInterval(interval); return () => clearInterval(interval);
}, [lastActivityTime, timeout, onLogout, notified]); }, [lastActivityTime, timeout, onLogout, notified]);
return ( return <></>;
<>
<ToastContainer />
</>
);
}; };
export default SessionTimeout; export default SessionTimeout;

View File

@@ -33,10 +33,10 @@ http.interceptors.response.use(
function (error) { function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger // Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error // Do something with response error
if (error.response.status === 401 || error.response.status === 403) { // if (error.response.status === 401 || error.response.status === 403) {
localStorage.clear(); // localStorage.clear();
window.location.assign("/error"); // window.location.assign("/error");
} // }
return Promise.reject(error); return Promise.reject(error);
} }
); );

View File

@@ -70,11 +70,33 @@ namespace AceJobAgency.Controllers
public IActionResult Login([FromBody] LoginRequest request) public IActionResult Login([FromBody] LoginRequest request)
{ {
var user = _context.Users.FirstOrDefault(u => u.Email == request.Email && u.IsActive == 1); var user = _context.Users.FirstOrDefault(u => u.Email == request.Email && u.IsActive == 1);
if (user == null || !BCrypt.Net.BCrypt.Verify(request.Password, user.Password)) if (user == null)
{ {
return Unauthorized("Invalid email or password."); return Unauthorized("Invalid email or password.");
} }
if (user.IsLockedOut && user.LockoutEndTime > DateTime.Now)
{
return Unauthorized("Account is locked. Try again later.");
}
if (!BCrypt.Net.BCrypt.Verify(request.Password, user.Password))
{
user.FailedLoginAttempts++;
if (user.FailedLoginAttempts >= 5)
{
user.IsLockedOut = true;
user.LockoutEndTime = DateTime.Now.AddMinutes(1);
}
_context.SaveChanges();
return Unauthorized("Invalid email or password.");
}
user.FailedLoginAttempts = 0;
user.IsLockedOut = false;
user.LockoutEndTime = null;
_context.SaveChanges();
var token = GenerateJwtToken(user); var token = GenerateJwtToken(user);
return Ok(new { token }); return Ok(new { token });
} }
@@ -184,7 +206,7 @@ namespace AceJobAgency.Controllers
new Claim(ClaimTypes.NameIdentifier, user.Id), new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email) new Claim(ClaimTypes.Email, user.Email)
]), ]),
Expires = DateTime.UtcNow.AddHours(2), Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature) SecurityAlgorithms.HmacSha256Signature)
}; };

View File

@@ -50,5 +50,12 @@ namespace AceJobAgency.Entities
[DataType(DataType.Date)] [DataType(DataType.Date)]
public DateTime UpdatedAt { get; set; } = DateTime.Now; public DateTime UpdatedAt { get; set; } = DateTime.Now;
public int FailedLoginAttempts { get; set; } = 0;
public bool IsLockedOut { get; set; } = false;
[DataType(DataType.DateTime)]
public DateTime? LockoutEndTime { get; set; }
} }
} }

View File

@@ -0,0 +1,100 @@
// <auto-generated />
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("20250209045614_AddedAccountLockoutFields")]
partial class AddedAccountLockoutFields
{
/// <inheritdoc />
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.User", b =>
{
b.Property<string>("Id")
.HasMaxLength(36)
.HasColumnType("varchar(36)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("datetime(6)");
b.Property<DateTime>("DateOfBirth")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<int>("FailedLoginAttempts")
.HasColumnType("int");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<int>("Gender")
.HasColumnType("int");
b.Property<int>("IsActive")
.HasColumnType("int");
b.Property<bool>("IsLockedOut")
.HasColumnType("tinyint(1)");
b.Property<string>("LastName")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("varchar(50)");
b.Property<DateTime?>("LockoutEndTime")
.HasColumnType("datetime(6)");
b.Property<string>("NationalRegistrationIdentityCardNumber")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.Property<string>("Password")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<string>("ResumeName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("varchar(128)");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("datetime(6)");
b.Property<string>("WhoAmI")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("varchar(255)");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace AceJobAgency.Migrations
{
/// <inheritdoc />
public partial class AddedAccountLockoutFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "FailedLoginAttempts",
table: "Users",
type: "int",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "IsLockedOut",
table: "Users",
type: "tinyint(1)",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "LockoutEndTime",
table: "Users",
type: "datetime(6)",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "FailedLoginAttempts",
table: "Users");
migrationBuilder.DropColumn(
name: "IsLockedOut",
table: "Users");
migrationBuilder.DropColumn(
name: "LockoutEndTime",
table: "Users");
}
}
}

View File

@@ -39,6 +39,9 @@ namespace AceJobAgency.Migrations
.HasMaxLength(128) .HasMaxLength(128)
.HasColumnType("varchar(128)"); .HasColumnType("varchar(128)");
b.Property<int>("FailedLoginAttempts")
.HasColumnType("int");
b.Property<string>("FirstName") b.Property<string>("FirstName")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
@@ -50,11 +53,17 @@ namespace AceJobAgency.Migrations
b.Property<int>("IsActive") b.Property<int>("IsActive")
.HasColumnType("int"); .HasColumnType("int");
b.Property<bool>("IsLockedOut")
.HasColumnType("tinyint(1)");
b.Property<string>("LastName") b.Property<string>("LastName")
.IsRequired() .IsRequired()
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("varchar(50)"); .HasColumnType("varchar(50)");
b.Property<DateTime?>("LockoutEndTime")
.HasColumnType("datetime(6)");
b.Property<string>("NationalRegistrationIdentityCardNumber") b.Property<string>("NationalRegistrationIdentityCardNumber")
.IsRequired() .IsRequired()
.HasMaxLength(255) .HasMaxLength(255)