User activity logs
This commit is contained in:
27
AceJobAgency/Controllers/ActivityLogController.cs
Normal file
27
AceJobAgency/Controllers/ActivityLogController.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using AceJobAgency.Data;
|
||||||
|
using AceJobAgency.Entities;
|
||||||
|
|
||||||
|
namespace AceJobAgency.Controllers;
|
||||||
|
|
||||||
|
public class ActivityLogController
|
||||||
|
{
|
||||||
|
private readonly DataContext _context;
|
||||||
|
|
||||||
|
public ActivityLogController(DataContext context)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void LogUserActivity(string userId, string activity, string ipAddress)
|
||||||
|
{
|
||||||
|
var logEntry = new ActivityLog
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString(),
|
||||||
|
UserId = userId,
|
||||||
|
Activity = activity,
|
||||||
|
IpAddress = ipAddress
|
||||||
|
};
|
||||||
|
_context.ActivityLogs.Add(logEntry);
|
||||||
|
_context.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,8 @@ 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);
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return Unauthorized("Invalid email or password.");
|
return Unauthorized("Invalid email or password.");
|
||||||
@@ -77,6 +79,7 @@ namespace AceJobAgency.Controllers
|
|||||||
|
|
||||||
if (user.IsLockedOut && user.LockoutEndTime > DateTime.Now)
|
if (user.IsLockedOut && user.LockoutEndTime > DateTime.Now)
|
||||||
{
|
{
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Login rejected: Locked out", ipAddress);
|
||||||
return Unauthorized("Account is locked. Try again later.");
|
return Unauthorized("Account is locked. Try again later.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,8 +90,10 @@ namespace AceJobAgency.Controllers
|
|||||||
{
|
{
|
||||||
user.IsLockedOut = true;
|
user.IsLockedOut = true;
|
||||||
user.LockoutEndTime = DateTime.Now.AddMinutes(1);
|
user.LockoutEndTime = DateTime.Now.AddMinutes(1);
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Account locked due to 5 consecutive failed attempts.", ipAddress);
|
||||||
}
|
}
|
||||||
_context.SaveChanges();
|
_context.SaveChanges();
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Login rejected: invalid password", ipAddress);
|
||||||
return Unauthorized("Invalid email or password.");
|
return Unauthorized("Invalid email or password.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +103,7 @@ namespace AceJobAgency.Controllers
|
|||||||
_context.SaveChanges();
|
_context.SaveChanges();
|
||||||
|
|
||||||
var token = GenerateJwtToken(user);
|
var token = GenerateJwtToken(user);
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Login successful", ipAddress);
|
||||||
return Ok(new { token });
|
return Ok(new { token });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +131,8 @@ namespace AceJobAgency.Controllers
|
|||||||
user.WhoAmI,
|
user.WhoAmI,
|
||||||
user.ResumeName,
|
user.ResumeName,
|
||||||
};
|
};
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Fetched user profile", ipAddress);
|
||||||
return Ok(response);
|
return Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +155,8 @@ namespace AceJobAgency.Controllers
|
|||||||
|
|
||||||
_context.Users.Update(user);
|
_context.Users.Update(user);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Updated user profile", ipAddress);
|
||||||
return Ok(user);
|
return Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +164,7 @@ namespace AceJobAgency.Controllers
|
|||||||
[HttpPut("change-password")]
|
[HttpPut("change-password")]
|
||||||
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
|
||||||
{
|
{
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
var user = _context.Users.FirstOrDefault(u => u.Id == userId && u.IsActive == 1);
|
var user = _context.Users.FirstOrDefault(u => u.Id == userId && u.IsActive == 1);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@@ -163,11 +174,13 @@ namespace AceJobAgency.Controllers
|
|||||||
|
|
||||||
if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, user.Password))
|
if (!BCrypt.Net.BCrypt.Verify(request.CurrentPassword, user.Password))
|
||||||
{
|
{
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Change password failed: Current password is incorrect", ipAddress);
|
||||||
return BadRequest("Current password is incorrect.");
|
return BadRequest("Current password is incorrect.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AccountManagement.IsPasswordComplex(request.NewPassword))
|
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.");
|
return BadRequest("Password must be at least 12 characters long and include uppercase, lowercase, number, and special character.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +188,7 @@ namespace AceJobAgency.Controllers
|
|||||||
user.UpdatedAt = DateTime.Now;
|
user.UpdatedAt = DateTime.Now;
|
||||||
_context.Users.Update(user);
|
_context.Users.Update(user);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Change password successful", ipAddress);
|
||||||
return Ok("Password updated successfully.");
|
return Ok("Password updated successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +278,9 @@ namespace AceJobAgency.Controllers
|
|||||||
_context.Users.Update(user);
|
_context.Users.Update(user);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Updated user profile", ipAddress);
|
||||||
|
|
||||||
return Ok(new { ResumeName = user.ResumeName });
|
return Ok(new { ResumeName = user.ResumeName });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,8 +295,11 @@ namespace AceJobAgency.Controllers
|
|||||||
return NotFound();
|
return NotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(user.ResumeName))
|
if (string.IsNullOrEmpty(user.ResumeName))
|
||||||
{
|
{
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Download resume failed: No resume found", ipAddress);
|
||||||
return NotFound("No resume found.");
|
return NotFound("No resume found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,6 +307,7 @@ namespace AceJobAgency.Controllers
|
|||||||
|
|
||||||
if (!System.IO.File.Exists(filePath))
|
if (!System.IO.File.Exists(filePath))
|
||||||
{
|
{
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Download resume failed: Resume file missing", ipAddress);
|
||||||
return NotFound("Resume file not found.");
|
return NotFound("Resume file not found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,6 +317,7 @@ namespace AceJobAgency.Controllers
|
|||||||
else if (extension == ".doc" || extension == ".docx") mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
else if (extension == ".doc" || extension == ".docx") mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
||||||
|
|
||||||
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
|
||||||
|
new ActivityLogController(_context).LogUserActivity(user.Id, "Download resume successful", ipAddress);
|
||||||
return new FileStreamResult(fileStream, mimeType) { FileDownloadName = user.ResumeName };
|
return new FileStreamResult(fileStream, mimeType) { FileDownloadName = user.ResumeName };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ namespace AceJobAgency.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
public DbSet<User> Users { get; set; }
|
public DbSet<User> Users { get; set; }
|
||||||
|
public DbSet<ActivityLog> ActivityLogs { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
AceJobAgency/Entities/ActivityLog.cs
Normal file
25
AceJobAgency/Entities/ActivityLog.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace AceJobAgency.Entities;
|
||||||
|
|
||||||
|
public class ActivityLog
|
||||||
|
{
|
||||||
|
[Key]
|
||||||
|
[MaxLength(36)]
|
||||||
|
public required string Id { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(36)]
|
||||||
|
public required string UserId { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[MaxLength(255)]
|
||||||
|
public required string Activity { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[StringLength(15)]
|
||||||
|
public required string IpAddress { get; set; }
|
||||||
|
|
||||||
|
[DataType(DataType.Date)]
|
||||||
|
public DateTime CreatedAt { get; init; } = DateTime.Now;
|
||||||
|
}
|
||||||
129
AceJobAgency/Migrations/20250210171437_AddedLogs.Designer.cs
generated
Normal file
129
AceJobAgency/Migrations/20250210171437_AddedLogs.Designer.cs
generated
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// <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("20250210171437_AddedLogs")]
|
||||||
|
partial class AddedLogs
|
||||||
|
{
|
||||||
|
/// <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.ActivityLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("varchar(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Activity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("varchar(15)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("varchar(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs");
|
||||||
|
});
|
||||||
|
|
||||||
|
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(2048)
|
||||||
|
.HasColumnType("varchar(2048)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
AceJobAgency/Migrations/20250210171437_AddedLogs.cs
Normal file
42
AceJobAgency/Migrations/20250210171437_AddedLogs.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace AceJobAgency.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedLogs : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ActivityLogs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "varchar(36)", maxLength: 36, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
UserId = table.Column<string>(type: "varchar(36)", maxLength: 36, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Activity = table.Column<string>(type: "varchar(255)", maxLength: 255, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
IpAddress = table.Column<string>(type: "varchar(15)", maxLength: 15, nullable: false)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ActivityLogs", x => x.Id);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ActivityLogs");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,35 @@ namespace AceJobAgency.Migrations
|
|||||||
|
|
||||||
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("AceJobAgency.Entities.ActivityLog", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("varchar(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Activity")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("varchar(255)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(15)
|
||||||
|
.HasColumnType("varchar(15)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(36)
|
||||||
|
.HasColumnType("varchar(36)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("ActivityLogs");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("AceJobAgency.Entities.User", b =>
|
modelBuilder.Entity("AceJobAgency.Entities.User", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Id")
|
b.Property<string>("Id")
|
||||||
|
|||||||
Reference in New Issue
Block a user