account lockout
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
100
AceJobAgency/Migrations/20250209045614_AddedAccountLockoutFields.Designer.cs
generated
Normal file
100
AceJobAgency/Migrations/20250209045614_AddedAccountLockoutFields.Designer.cs
generated
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user