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 {
const response = await http.post("/User/login", loginRequest);
if (response.status !== 200) {
throw new Error("Login failed");
if (response.status === 401) {
throw new Error("Invalid email or password.");
}
const { token } = response.data;

View File

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

View File

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

View File

@@ -70,11 +70,33 @@ namespace AceJobAgency.Controllers
public IActionResult Login([FromBody] LoginRequest request)
{
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.");
}
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);
return Ok(new { token });
}
@@ -184,7 +206,7 @@ namespace AceJobAgency.Controllers
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Email, user.Email)
]),
Expires = DateTime.UtcNow.AddHours(2),
Expires = DateTime.UtcNow.AddMinutes(15),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha256Signature)
};

View File

@@ -50,5 +50,12 @@ namespace AceJobAgency.Entities
[DataType(DataType.Date)]
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)
.HasColumnType("varchar(128)");
b.Property<int>("FailedLoginAttempts")
.HasColumnType("int");
b.Property<string>("FirstName")
.IsRequired()
.HasMaxLength(50)
@@ -50,11 +53,17 @@ namespace AceJobAgency.Migrations
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)