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)
{
var user = _context.Users.FirstOrDefault(u => u.Email == request.Email && u.IsActive == 1);
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
if (user == null)
{
return Unauthorized("Invalid email or password.");
@@ -77,6 +79,7 @@ namespace AceJobAgency.Controllers
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.");
}
@@ -87,8 +90,10 @@ namespace AceJobAgency.Controllers
{
user.IsLockedOut = true;
user.LockoutEndTime = DateTime.Now.AddMinutes(1);
new ActivityLogController(_context).LogUserActivity(user.Id, "Account locked due to 5 consecutive failed attempts.", ipAddress);
}
_context.SaveChanges();
new ActivityLogController(_context).LogUserActivity(user.Id, "Login rejected: invalid password", ipAddress);
return Unauthorized("Invalid email or password.");
}
@@ -98,6 +103,7 @@ namespace AceJobAgency.Controllers
_context.SaveChanges();
var token = GenerateJwtToken(user);
new ActivityLogController(_context).LogUserActivity(user.Id, "Login successful", ipAddress);
return Ok(new { token });
}
@@ -125,6 +131,8 @@ namespace AceJobAgency.Controllers
user.WhoAmI,
user.ResumeName,
};
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
new ActivityLogController(_context).LogUserActivity(user.Id, "Fetched user profile", ipAddress);
return Ok(response);
}
@@ -147,6 +155,8 @@ namespace AceJobAgency.Controllers
_context.Users.Update(user);
await _context.SaveChangesAsync();
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
new ActivityLogController(_context).LogUserActivity(user.Id, "Updated user profile", ipAddress);
return Ok(user);
}
@@ -154,6 +164,7 @@ namespace AceJobAgency.Controllers
[HttpPut("change-password")]
public async Task<IActionResult> ChangePassword([FromBody] ChangePasswordRequest request)
{
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var user = _context.Users.FirstOrDefault(u => u.Id == userId && u.IsActive == 1);
if (user == null)
@@ -163,11 +174,13 @@ namespace AceJobAgency.Controllers
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.");
}
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.");
}
@@ -175,6 +188,7 @@ namespace AceJobAgency.Controllers
user.UpdatedAt = DateTime.Now;
_context.Users.Update(user);
await _context.SaveChangesAsync();
new ActivityLogController(_context).LogUserActivity(user.Id, "Change password successful", ipAddress);
return Ok("Password updated successfully.");
}
@@ -263,6 +277,9 @@ namespace AceJobAgency.Controllers
_context.Users.Update(user);
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 });
}
@@ -277,9 +294,12 @@ namespace AceJobAgency.Controllers
{
return NotFound();
}
var ipAddress = HttpContext.Connection.RemoteIpAddress!.ToString();
if (string.IsNullOrEmpty(user.ResumeName))
{
new ActivityLogController(_context).LogUserActivity(user.Id, "Download resume failed: No resume found", ipAddress);
return NotFound("No resume found.");
}
@@ -287,6 +307,7 @@ namespace AceJobAgency.Controllers
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.");
}
@@ -296,6 +317,7 @@ namespace AceJobAgency.Controllers
else if (extension == ".doc" || extension == ".docx") mimeType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
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 };
}
}

View File

@@ -16,5 +16,6 @@ namespace AceJobAgency.Data
}
}
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);
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")