Dev #5

Merged
ashan_rusiru merged 2 commits from dev into main 2026-10-18 12:48:32 +05:30
14 changed files with 12 additions and 350 deletions
Showing only changes of commit fbd079d337 - Show all commits
+5
View File
@@ -3,6 +3,11 @@
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.
> **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
**Request** (`ApiRequest`)
-11
View File
@@ -8,7 +8,6 @@ namespace AuthHex
{
public AppDBContext(DbContextOptions<AppDBContext> options) : base(options) { }
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.UserSession> UserSessions => Set<Models.UserSession>();
@@ -69,10 +68,6 @@ namespace AuthHex
.HasForeignKey(up => up.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure Role primary key
modelBuilder.Entity<Models.Role>()
.HasKey(r => r.RoleId);
// Configure UserType primary key
modelBuilder.Entity<Models.UserType>()
.HasKey(ut => ut.UserTypeId);
@@ -81,12 +76,6 @@ namespace AuthHex
modelBuilder.Entity<Models.User>()
.HasKey(u => u.Id);
modelBuilder.Entity<Models.User>()
.HasOne(u => u.Role)
.WithMany()
.HasForeignKey(u => u.RoleId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Models.User>()
.HasOne(u => u.UserType)
.WithMany()
+1 -10
View File
@@ -10,12 +10,11 @@ namespace AuthHex.Controllers
[ApiController]
[Route("api")]
[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 RecoveryManager _recoveryManager = recoveryManager;
private readonly AltOptionManager _altOptionManager = altOptionManager;
private readonly RoleManager _roleManager = roleManager;
[HttpGet("status")]
[AllowAnonymous]
@@ -76,13 +75,5 @@ namespace AuthHex.Controllers
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 UserMfa? UserMfa { get; set; }
//User Role and Type
public Guid RoleId { get; set; }
public Role? Role { get; set; }
// Roles are not an authentication concern: the ERP owns a person's role and permissions (users.RoleId in ERPCore).
// AuthHex only says who the person is. (The old Users.RoleId column is left in the table, nullable and unmapped,
// until it is dropped - see Docs/migrations/2026-10-remove-roles.sql.)
public Guid UserTypeId { 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<IRecoveryManagerRepository, RecoveryManagerRepository>();
builder.Services.AddScoped<IAltOptionManagerRepository, AltOptionManagerRepository>();
builder.Services.AddScoped<IRoleManageRepository, RoleManageRepository>();
builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<UserManagerService>();
builder.Services.AddScoped<RecoveryManagerService>();
builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>();
builder.Services.AddScoped<AuthHex.Services.RoleManager.RoleManagerService>();
builder.Services.AddHttpClient<ThirdPartyService>();
builder.Services.AddScoped<JwtTokenHelper>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.UserManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RecoveryManager>();
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 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)
{
return await _dbContext.Users
.Include(u => u.Role)
.Include(u => u.UserType)
.FirstOrDefaultAsync(u => u.Id == userId, ct);
}
@@ -377,7 +377,6 @@ public class AltOptionManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleId,
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";
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var RoleId))
throw new ArgumentException("Invalid roleId");
// A "roleId" in the payload (old ERP builds still send one) is accepted and ignored: roles are the ERP's business.
if (!data.ContainsKey("userTypeId") || !Guid.TryParse(data["userTypeId"].GetString(), out var UserTypeId))
throw new ArgumentException("Invalid userTypeId");
@@ -80,7 +79,6 @@ public class UserManagerService
Email = Email,
MobileNumber = MobileNumber,
Nic = Nic,
RoleId = RoleId,
UserTypeId = UserTypeId
};
bool canSave = await CanRegisterUser(userToCheck);
@@ -110,7 +108,6 @@ public class UserManagerService
Nic = Nic,
Email = Email,
MobileNumber = MobileNumber,
RoleId = RoleId,
UserTypeId = UserTypeId,
CreatedAt = DateTime.UtcNow,
IsActive = true,
@@ -194,7 +191,6 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId
}
};
@@ -303,7 +299,6 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId
}
};
@@ -412,7 +407,6 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId
}
}
@@ -505,7 +499,6 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleId,
UserTypeId = user.UserTypeId
}
};
@@ -552,12 +545,6 @@ public class UserManagerService
user.IsActive,
user.IsLocked,
user.CreatedAt,
Role = new
{
user.Role?.RoleId,
user.Role?.Code,
user.Role?.Name
},
UserType = new
{
user.UserType?.UserTypeId,
@@ -1215,9 +1202,6 @@ public class UserManagerService
if (user.UserTypeId == userToCheck.UserTypeId)
return false;
if (user.RoleId == userToCheck.RoleId)
return false;
if (user.IsLocked == false)
return false;
+2 -9
View File
@@ -22,13 +22,8 @@ public class JwtTokenHelper
public async Task<string> GenerateToken(User user)
{
// Load user's role and usertype if not already loaded
if (user.Role == null)
{
await _dbContext.Entry(user)
.Reference(u => u.Role)
.LoadAsync();
}
// Load the user's usertype if not already loaded. (No role: authorization belongs to the ERP, which reads the
// person's role from its own tables - the token only says who the person is.)
if (user.UserType == null)
{
await _dbContext.Entry(user)
@@ -41,8 +36,6 @@ public class JwtTokenHelper
new Claim("UserId", user.Id.ToString()),
new Claim("UserTypeId", user.UserTypeId.ToString()),
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(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,