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)
|
||||
{
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ namespace AceJobAgency.Data
|
||||
}
|
||||
}
|
||||
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);
|
||||
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user