User activity logs

This commit is contained in:
2025-02-11 01:17:05 +08:00
parent 2076455655
commit 495fff3ac9
7 changed files with 275 additions and 0 deletions

View 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();
}
}

View File

@@ -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 };
} }
} }

View File

@@ -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; }
} }
} }

View 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;
}

View 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
}
}
}

View 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");
}
}
}

View File

@@ -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")