Merge pull request 'Dev' (#5) from dev into main

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2026-10-18 12:48:32 +05:30
14 changed files with 12 additions and 350 deletions
+5
View File
@@ -3,6 +3,11 @@
Base route: `api` Base route: `api`
All endpoints are `[AllowAnonymous]` at the HTTP layer, but individual functions may require a Bearer JWT (read via `HttpContext` claims) — noted per function below. All endpoints are `[AllowAnonymous]` at the HTTP layer, but individual functions may require a Bearer JWT (read via `HttpContext` claims) — noted per function below.
> **Roles were removed (2026-10).** AuthHex is authentication only: it has no roles, `/api/role` no longer exists, `registerUser`
> no longer needs (and ignores) `roleId`, user payloads carry no `roleId`/`role`, and the JWT has no `RoleId`/`RoleCode`
> claims. The ERP owns roles and permissions (users.RoleId in ERPCore). The role fields and the **/api/role** section
> below describe the old behaviour and are kept only for reference. Migration steps: `Docs/migrations/2026-10-remove-roles.sql`.
## Common envelope ## Common envelope
**Request** (`ApiRequest`) **Request** (`ApiRequest`)
-11
View File
@@ -8,7 +8,6 @@ namespace AuthHex
{ {
public AppDBContext(DbContextOptions<AppDBContext> options) : base(options) { } public AppDBContext(DbContextOptions<AppDBContext> options) : base(options) { }
public DbSet<Models.User> Users => Set<Models.User>(); public DbSet<Models.User> Users => Set<Models.User>();
public DbSet<Models.Role> Roles => Set<Models.Role>();
public DbSet<Models.UserType> UserType => Set<Models.UserType>(); public DbSet<Models.UserType> UserType => Set<Models.UserType>();
public DbSet<Models.UserSession> UserSessions => Set<Models.UserSession>(); public DbSet<Models.UserSession> UserSessions => Set<Models.UserSession>();
@@ -69,10 +68,6 @@ namespace AuthHex
.HasForeignKey(up => up.UserId) .HasForeignKey(up => up.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
// Configure Role primary key
modelBuilder.Entity<Models.Role>()
.HasKey(r => r.RoleId);
// Configure UserType primary key // Configure UserType primary key
modelBuilder.Entity<Models.UserType>() modelBuilder.Entity<Models.UserType>()
.HasKey(ut => ut.UserTypeId); .HasKey(ut => ut.UserTypeId);
@@ -81,12 +76,6 @@ namespace AuthHex
modelBuilder.Entity<Models.User>() modelBuilder.Entity<Models.User>()
.HasKey(u => u.Id); .HasKey(u => u.Id);
modelBuilder.Entity<Models.User>()
.HasOne(u => u.Role)
.WithMany()
.HasForeignKey(u => u.RoleId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Models.User>() modelBuilder.Entity<Models.User>()
.HasOne(u => u.UserType) .HasOne(u => u.UserType)
.WithMany() .WithMany()
+1 -10
View File
@@ -10,12 +10,11 @@ namespace AuthHex.Controllers
[ApiController] [ApiController]
[Route("api")] [Route("api")]
[EnableCors("AllowAll")] [EnableCors("AllowAll")]
public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager, RoleManager roleManager) : ControllerBase public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager) : ControllerBase
{ {
private readonly UserManager _userManager = userManager; private readonly UserManager _userManager = userManager;
private readonly RecoveryManager _recoveryManager = recoveryManager; private readonly RecoveryManager _recoveryManager = recoveryManager;
private readonly AltOptionManager _altOptionManager = altOptionManager; private readonly AltOptionManager _altOptionManager = altOptionManager;
private readonly RoleManager _roleManager = roleManager;
[HttpGet("status")] [HttpGet("status")]
[AllowAnonymous] [AllowAnonymous]
@@ -76,13 +75,5 @@ namespace AuthHex.Controllers
return StatusCode(response.StatusCode, response); return StatusCode(response.StatusCode, response);
} }
[HttpPost("role")]
[AllowAnonymous]
public async Task<IActionResult> RoleExecute([FromBody] ApiRequest request)
{
var response = await _roleManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
} }
} }
-13
View File
@@ -1,13 +0,0 @@
using AuthHex.Models;
namespace AuthHex.Interfaces
{
public interface IRoleManageRepository
{
Task<Role> AddRoleAsync(Role role, CancellationToken ct = default);
Task<Role?> GetRoleByIdAsync(Guid roleId, CancellationToken ct = default);
Task<List<Role>> ListRolesAsync(CancellationToken ct = default);
Task<bool> RoleInUseAsync(Guid roleId, CancellationToken ct = default);
Task DeleteRoleAsync(Role role, CancellationToken ct = default);
}
}
-15
View File
@@ -1,15 +0,0 @@
namespace AuthHex.Models
{
public class Role
{
public Guid RoleId { get; set; }
public string Code { get; set; } = string.Empty; // ADMIN, USER, STAFF
public string?Name { get; set; } = string.Empty;
public bool? IsSystemRole { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
+3 -4
View File
@@ -30,10 +30,9 @@ namespace AuthHex.Models
public bool IsMfaEnabled { get; set; } public bool IsMfaEnabled { get; set; }
public UserMfa? UserMfa { get; set; } public UserMfa? UserMfa { get; set; }
//User Role and Type // Roles are not an authentication concern: the ERP owns a person's role and permissions (users.RoleId in ERPCore).
public Guid RoleId { get; set; } // AuthHex only says who the person is. (The old Users.RoleId column is left in the table, nullable and unmapped,
public Role? Role { get; set; } // until it is dropped - see Docs/migrations/2026-10-remove-roles.sql.)
public Guid UserTypeId { get; set; } public Guid UserTypeId { get; set; }
public UserType? UserType { get; set; } public UserType? UserType { get; set; }
-3
View File
@@ -57,18 +57,15 @@ builder.Services.AddScoped<IUnitOfWork, EFUnitOfWork>();
builder.Services.AddScoped<IUserManageRepository, UserManageRepository>(); builder.Services.AddScoped<IUserManageRepository, UserManageRepository>();
builder.Services.AddScoped<IRecoveryManagerRepository, RecoveryManagerRepository>(); builder.Services.AddScoped<IRecoveryManagerRepository, RecoveryManagerRepository>();
builder.Services.AddScoped<IAltOptionManagerRepository, AltOptionManagerRepository>(); builder.Services.AddScoped<IAltOptionManagerRepository, AltOptionManagerRepository>();
builder.Services.AddScoped<IRoleManageRepository, RoleManageRepository>();
builder.Services.AddScoped<UserHelper>(); builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<UserManagerService>(); builder.Services.AddScoped<UserManagerService>();
builder.Services.AddScoped<RecoveryManagerService>(); builder.Services.AddScoped<RecoveryManagerService>();
builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>(); builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>();
builder.Services.AddScoped<AuthHex.Services.RoleManager.RoleManagerService>();
builder.Services.AddHttpClient<ThirdPartyService>(); builder.Services.AddHttpClient<ThirdPartyService>();
builder.Services.AddScoped<JwtTokenHelper>(); builder.Services.AddScoped<JwtTokenHelper>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.UserManager>(); builder.Services.AddScoped<AuthHex.Services.FunctionHandler.UserManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RecoveryManager>(); builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RecoveryManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.AltOptionManager>(); builder.Services.AddScoped<AuthHex.Services.FunctionHandler.AltOptionManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RoleManager>();
var configuredOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>(); var configuredOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
var allowedOrigins = configuredOrigins var allowedOrigins = configuredOrigins
-43
View File
@@ -1,43 +0,0 @@
using AuthHex.Interfaces;
using AuthHex.Models;
using Microsoft.EntityFrameworkCore;
namespace AuthHex.Repos
{
public class RoleManageRepository : IRoleManageRepository
{
private readonly AppDBContext _dbContext;
public RoleManageRepository(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public Task<Role> AddRoleAsync(Role role, CancellationToken ct = default)
{
_dbContext.Roles.Add(role);
return Task.FromResult(role);
}
public async Task<Role?> GetRoleByIdAsync(Guid roleId, CancellationToken ct = default)
{
return await _dbContext.Roles.FirstOrDefaultAsync(r => r.RoleId == roleId, ct);
}
public async Task<List<Role>> ListRolesAsync(CancellationToken ct = default)
{
return await _dbContext.Roles.OrderBy(r => r.Code).ToListAsync(ct);
}
public async Task<bool> RoleInUseAsync(Guid roleId, CancellationToken ct = default)
{
return await _dbContext.Users.AnyAsync(u => u.RoleId == roleId, ct);
}
public Task DeleteRoleAsync(Role role, CancellationToken ct = default)
{
_dbContext.Roles.Remove(role);
return Task.CompletedTask;
}
}
}
-1
View File
@@ -70,7 +70,6 @@ namespace AuthHex.Repos
public async Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default) public async Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default)
{ {
return await _dbContext.Users return await _dbContext.Users
.Include(u => u.Role)
.Include(u => u.UserType) .Include(u => u.UserType)
.FirstOrDefaultAsync(u => u.Id == userId, ct); .FirstOrDefaultAsync(u => u.Id == userId, ct);
} }
@@ -377,7 +377,6 @@ public class AltOptionManagerService
user.EmailVerified, user.EmailVerified,
user.MobileNumberVerified, user.MobileNumberVerified,
user.IsMfaEnabled, user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId UserTypeId = user.UserTypeId
} }
} }
-72
View File
@@ -1,72 +0,0 @@
using AuthHex.Models.IOs;
using AuthHex.Services.RoleManager;
namespace AuthHex.Services.FunctionHandler
{
public class RoleManager
{
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
public RoleManager(RoleManagerService roleManagerService)
{
_functionHandler = new()
{
{ "createRole", roleManagerService.CreateRole },
{ "getRole", roleManagerService.GetRole },
{ "listRoles", async (payload, context) => await roleManagerService.ListRoles(context) },
{ "updateRole", roleManagerService.UpdateRole },
{ "deleteRole", roleManagerService.DeleteRole }
};
}
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
{
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
{
return new ApiResponse
{
StatusCode = 404,
Message = $"Function '{request.FunctionName}' not found.",
Data = null
};
}
try
{
var results = await handler(request.Payload, httpContext);
return new ApiResponse
{
StatusCode = 200,
Message = "success",
Data = results
};
}
catch (KeyNotFoundException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (InvalidOperationException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (ArgumentException ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message,
Data = null
};
}
}
}
}
-151
View File
@@ -1,151 +0,0 @@
using System.Text.Json;
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Models;
using Core_Archi.Helpers;
using Microsoft.AspNetCore.Http;
namespace AuthHex.Services.RoleManager;
public class RoleManagerService
{
private readonly IRoleManageRepository _repository;
private readonly IUnitOfWork _uow;
public RoleManagerService(IRoleManageRepository repository, IUnitOfWork uow)
{
_repository = repository;
_uow = uow;
}
public async Task<object> CreateRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var code = data.ContainsKey("code") ? data["code"].GetString() : null;
var name = data.ContainsKey("name") ? data["name"].GetString() : null;
var isSystemRole = data.ContainsKey("isSystemRole")
&& data["isSystemRole"].ValueKind != JsonValueKind.Null
&& data["isSystemRole"].GetBoolean();
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("code is required");
var role = new Role
{
RoleId = Guid.NewGuid(),
Code = code,
Name = string.IsNullOrWhiteSpace(name) ? code : name,
IsSystemRole = isSystemRole,
CreatedAt = DateTime.UtcNow
};
await _repository.AddRoleAsync(role);
await _uow.CommitAsync();
return ToDto(role);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetRole(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
return ToDto(role);
}
public async Task<object> ListRoles(HttpContext httpContext)
{
var roles = await _repository.ListRolesAsync();
return roles.Select(ToDto).ToList();
}
public async Task<object> UpdateRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
if (data.ContainsKey("code"))
{
var code = data["code"].GetString();
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("code cannot be empty");
role.Code = code;
}
if (data.ContainsKey("name"))
role.Name = data["name"].GetString();
if (data.ContainsKey("isSystemRole") && data["isSystemRole"].ValueKind != JsonValueKind.Null)
role.IsSystemRole = data["isSystemRole"].GetBoolean();
await _uow.CommitAsync();
return ToDto(role);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
if (await _repository.RoleInUseAsync(roleId))
throw new InvalidOperationException("ROLE_IN_USE: role is assigned to one or more users");
await _repository.DeleteRoleAsync(role);
await _uow.CommitAsync();
return new { message = "Role deleted successfully" };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
private static object ToDto(Role role) => new
{
role.RoleId,
role.Code,
role.Name,
role.IsSystemRole,
role.CreatedAt
};
}
+1 -17
View File
@@ -65,8 +65,7 @@ public class UserManagerService
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device"; var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var RoleId)) // A "roleId" in the payload (old ERP builds still send one) is accepted and ignored: roles are the ERP's business.
throw new ArgumentException("Invalid roleId");
if (!data.ContainsKey("userTypeId") || !Guid.TryParse(data["userTypeId"].GetString(), out var UserTypeId)) if (!data.ContainsKey("userTypeId") || !Guid.TryParse(data["userTypeId"].GetString(), out var UserTypeId))
throw new ArgumentException("Invalid userTypeId"); throw new ArgumentException("Invalid userTypeId");
@@ -80,7 +79,6 @@ public class UserManagerService
Email = Email, Email = Email,
MobileNumber = MobileNumber, MobileNumber = MobileNumber,
Nic = Nic, Nic = Nic,
RoleId = RoleId,
UserTypeId = UserTypeId UserTypeId = UserTypeId
}; };
bool canSave = await CanRegisterUser(userToCheck); bool canSave = await CanRegisterUser(userToCheck);
@@ -110,7 +108,6 @@ public class UserManagerService
Nic = Nic, Nic = Nic,
Email = Email, Email = Email,
MobileNumber = MobileNumber, MobileNumber = MobileNumber,
RoleId = RoleId,
UserTypeId = UserTypeId, UserTypeId = UserTypeId,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
IsActive = true, IsActive = true,
@@ -194,7 +191,6 @@ public class UserManagerService
user.EmailVerified, user.EmailVerified,
user.MobileNumberVerified, user.MobileNumberVerified,
user.IsMfaEnabled, user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId UserTypeId = user.UserTypeId
} }
}; };
@@ -303,7 +299,6 @@ public class UserManagerService
user.EmailVerified, user.EmailVerified,
user.MobileNumberVerified, user.MobileNumberVerified,
user.IsMfaEnabled, user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId UserTypeId = user.UserTypeId
} }
}; };
@@ -412,7 +407,6 @@ public class UserManagerService
user.EmailVerified, user.EmailVerified,
user.MobileNumberVerified, user.MobileNumberVerified,
user.IsMfaEnabled, user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId UserTypeId = user.UserTypeId
} }
} }
@@ -505,7 +499,6 @@ public class UserManagerService
user.EmailVerified, user.EmailVerified,
user.MobileNumberVerified, user.MobileNumberVerified,
user.IsMfaEnabled, user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId UserTypeId = user.UserTypeId
} }
}; };
@@ -552,12 +545,6 @@ public class UserManagerService
user.IsActive, user.IsActive,
user.IsLocked, user.IsLocked,
user.CreatedAt, user.CreatedAt,
Role = new
{
user.Role?.RoleId,
user.Role?.Code,
user.Role?.Name
},
UserType = new UserType = new
{ {
user.UserType?.UserTypeId, user.UserType?.UserTypeId,
@@ -1215,9 +1202,6 @@ public class UserManagerService
if (user.UserTypeId == userToCheck.UserTypeId) if (user.UserTypeId == userToCheck.UserTypeId)
return false; return false;
if (user.RoleId == userToCheck.RoleId)
return false;
if (user.IsLocked == false) if (user.IsLocked == false)
return false; return false;
+2 -9
View File
@@ -22,13 +22,8 @@ public class JwtTokenHelper
public async Task<string> GenerateToken(User user) public async Task<string> GenerateToken(User user)
{ {
// Load user's role and usertype if not already loaded // Load the user's usertype if not already loaded. (No role: authorization belongs to the ERP, which reads the
if (user.Role == null) // person's role from its own tables - the token only says who the person is.)
{
await _dbContext.Entry(user)
.Reference(u => u.Role)
.LoadAsync();
}
if (user.UserType == null) if (user.UserType == null)
{ {
await _dbContext.Entry(user) await _dbContext.Entry(user)
@@ -41,8 +36,6 @@ public class JwtTokenHelper
new Claim("UserId", user.Id.ToString()), new Claim("UserId", user.Id.ToString()),
new Claim("UserTypeId", user.UserTypeId.ToString()), new Claim("UserTypeId", user.UserTypeId.ToString()),
new Claim("UserTypeCode", user.UserType?.Code ?? ""), new Claim("UserTypeCode", user.UserType?.Code ?? ""),
new Claim("RoleId", user.RoleId.ToString()),
new Claim("RoleCode", user.Role?.Code ?? ""),
new Claim("NIC", user.Nic ?? ""), new Claim("NIC", user.Nic ?? ""),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, new Claim(JwtRegisteredClaimNames.Iat,