Decouple UserType, add Role entity and management
Removed all UserType logic and references, replacing them with a new Role model and management system. Introduced RoleManager service, repository, and API endpoint for role CRUD operations. Updated user flows and JWT claims to use RoleCode. Removed admin-only user CRUD endpoints. Updated DI, documentation, and error handling for consistency. Appsettings and DTOs now reflect the new role-based approach.
This commit is contained in:
@@ -3,11 +3,6 @@
|
|||||||
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`)
|
||||||
|
|||||||
+5
-11
@@ -8,8 +8,8 @@ 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.UserType> UserType => Set<Models.UserType>();
|
public DbSet<Models.Role> Roles => Set<Models.Role>();
|
||||||
public DbSet<Models.UserSession> UserSessions => Set<Models.UserSession>();
|
public DbSet<Models.UserSession> UserSessions => Set<Models.UserSession>();
|
||||||
|
|
||||||
public DbSet<Models.Token> Token => Set<Models.Token>();
|
public DbSet<Models.Token> Token => Set<Models.Token>();
|
||||||
|
|
||||||
@@ -68,19 +68,13 @@ namespace AuthHex
|
|||||||
.HasForeignKey(up => up.UserId)
|
.HasForeignKey(up => up.UserId)
|
||||||
.OnDelete(DeleteBehavior.Cascade);
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
// Configure UserType primary key
|
// Configure Role primary key
|
||||||
modelBuilder.Entity<Models.UserType>()
|
modelBuilder.Entity<Models.Role>()
|
||||||
.HasKey(ut => ut.UserTypeId);
|
.HasKey(r => r.RoleId);
|
||||||
|
|
||||||
// Configure User primary key and relationships
|
// Configure User primary key and relationships
|
||||||
modelBuilder.Entity<Models.User>()
|
modelBuilder.Entity<Models.User>()
|
||||||
.HasKey(u => u.Id);
|
.HasKey(u => u.Id);
|
||||||
|
|
||||||
modelBuilder.Entity<Models.User>()
|
|
||||||
.HasOne(u => u.UserType)
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey(u => u.UserTypeId)
|
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ namespace AuthHex.Controllers
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api")]
|
[Route("api")]
|
||||||
[EnableCors("AllowAll")]
|
[EnableCors("AllowAll")]
|
||||||
public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager) : ControllerBase
|
public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager, RoleManager roleManager) : 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]
|
||||||
@@ -75,5 +76,13 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ namespace AuthHex.Interfaces
|
|||||||
Task<User> AddUserAsync(User user, CancellationToken ct = default);
|
Task<User> AddUserAsync(User user, CancellationToken ct = default);
|
||||||
Task<Token> AddTokenAsync(Token token, CancellationToken ct = default);
|
Task<Token> AddTokenAsync(Token token, CancellationToken ct = default);
|
||||||
Task<List<User>> GetUserByIdentifiers(string? email, string? mobileNumber, string? nic, string? username, string? Optional1);
|
Task<List<User>> GetUserByIdentifiers(string? email, string? mobileNumber, string? nic, string? username, string? Optional1);
|
||||||
Task<User?> GetUserByIdentifierAndType(string identifier, Guid? userTypeId);
|
Task<User?> GetUserByIdentifierAndType(string identifier);
|
||||||
Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default);
|
Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default);
|
||||||
Task UpdateUserAsync(User user, CancellationToken ct = default);
|
Task UpdateUserAsync(User user, CancellationToken ct = default);
|
||||||
Task<List<UserSession>> GetUserSessionsAsync(Guid userId, CancellationToken ct = default);
|
Task<List<UserSession>> GetUserSessionsAsync(Guid userId, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-7
@@ -30,13 +30,8 @@ namespace AuthHex.Models
|
|||||||
public bool IsMfaEnabled { get; set; }
|
public bool IsMfaEnabled { get; set; }
|
||||||
public UserMfa? UserMfa { get; set; }
|
public UserMfa? UserMfa { get; set; }
|
||||||
|
|
||||||
// Roles are not an authentication concern: the ERP owns a person's role and permissions (users.RoleId in ERPCore).
|
//User Role and Type
|
||||||
// AuthHex only says who the person is. (The old Users.RoleId column is left in the table, nullable and unmapped,
|
public string? RoleCode { get; set; }
|
||||||
// until it is dropped - see Docs/migrations/2026-10-remove-roles.sql.)
|
|
||||||
public Guid UserTypeId { get; set; }
|
|
||||||
public UserType? UserType { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
public ICollection<UserSession>? Sessions { get; set; }
|
public ICollection<UserSession>? Sessions { get; set; }
|
||||||
public ICollection<UserProvider>? Providers { get; set; }
|
public ICollection<UserProvider>? Providers { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
namespace AuthHex.Models
|
|
||||||
{
|
|
||||||
public class UserType
|
|
||||||
{
|
|
||||||
public Guid UserTypeId { get; set; }
|
|
||||||
|
|
||||||
public string Code { get; set; } = string.Empty; // INDIVIDUAL, MERCHANT, AGENT
|
|
||||||
public string? Description { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,15 +57,18 @@ 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
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,9 +43,8 @@ namespace AuthHex.Repos
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task<User?> GetUserByIdentifierAndType(string identifier, Guid? userTypeId)
|
public async Task<User?> GetUserByIdentifierAndType(string identifier)
|
||||||
{
|
{
|
||||||
|
|
||||||
User? user = await _dbContext.Users
|
User? user = await _dbContext.Users
|
||||||
.FirstOrDefaultAsync(u =>
|
.FirstOrDefaultAsync(u =>
|
||||||
u.Email == identifier ||
|
u.Email == identifier ||
|
||||||
@@ -70,7 +69,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.UserType)
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == userId, ct);
|
.FirstOrDefaultAsync(u => u.Id == userId, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ public class AltOptionManagerService
|
|||||||
|
|
||||||
//exisiting one
|
//exisiting one
|
||||||
|
|
||||||
var user = await _userRepository.GetUserByIdentifierAndType(identifier, null);
|
var user = await _userRepository.GetUserByIdentifierAndType(identifier);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new KeyNotFoundException("No user found with the provided identifier.");
|
throw new KeyNotFoundException("No user found with the provided identifier.");
|
||||||
@@ -293,7 +293,7 @@ public class AltOptionManagerService
|
|||||||
User? user = null;
|
User? user = null;
|
||||||
if (!string.IsNullOrWhiteSpace(identifier))
|
if (!string.IsNullOrWhiteSpace(identifier))
|
||||||
{
|
{
|
||||||
user = await _userRepository.GetUserByIdentifierAndType(identifier, null);
|
user = await _userRepository.GetUserByIdentifierAndType(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user is null && userId.HasValue)
|
if (user is null && userId.HasValue)
|
||||||
@@ -377,7 +377,7 @@ public class AltOptionManagerService
|
|||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
user.MobileNumberVerified,
|
user.MobileNumberVerified,
|
||||||
user.IsMfaEnabled,
|
user.IsMfaEnabled,
|
||||||
UserTypeId = user.UserTypeId
|
RoleId = user.RoleCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,8 +18,7 @@ namespace AuthHex.Services.FunctionHandler
|
|||||||
{ "refreshToken", UserManagerService.RefreshToken },
|
{ "refreshToken", UserManagerService.RefreshToken },
|
||||||
{ "getUserDetails", UserManagerService.GetUserDetails },
|
{ "getUserDetails", UserManagerService.GetUserDetails },
|
||||||
{ "getUserSessions", async (payload, context) => await UserManagerService.GetUserSessions(context) },
|
{ "getUserSessions", async (payload, context) => await UserManagerService.GetUserSessions(context) },
|
||||||
{ "listUserTypes", async (payload, context) => await UserManagerService.ListUserTypes(context) },
|
{ "ChangeUserStatus", UserManagerService.ChangeUserStatus},
|
||||||
{ "ChangeUserStatus", UserManagerService.ChangeUserStatus},
|
|
||||||
{"LockUserAccount" , UserManagerService.LockUserAccount },
|
{"LockUserAccount" , UserManagerService.LockUserAccount },
|
||||||
{"ChangeUserPassword" , UserManagerService.ChangeUserPassword },
|
{"ChangeUserPassword" , UserManagerService.ChangeUserPassword },
|
||||||
{"VerifyPassword" , UserManagerService.VerifyPassword },
|
{"VerifyPassword" , UserManagerService.VerifyPassword },
|
||||||
@@ -27,11 +26,6 @@ namespace AuthHex.Services.FunctionHandler
|
|||||||
{"UpdateUser", UserManagerService.UpdateUser},
|
{"UpdateUser", UserManagerService.UpdateUser},
|
||||||
{"VerifyOtpForLogin",UserManagerService.VerifyOtpForLogin},
|
{"VerifyOtpForLogin",UserManagerService.VerifyOtpForLogin},
|
||||||
|
|
||||||
// Admin-only — require X-Admin-Api-Key, act on an arbitrary target user.
|
|
||||||
{"AdminUpdateUser", UserManagerService.AdminUpdateUser},
|
|
||||||
{"AdminSetUserStatus", UserManagerService.AdminSetUserStatus},
|
|
||||||
{"deleteUser", UserManagerService.DeleteUser},
|
|
||||||
|
|
||||||
// 2FA Management Functions
|
// 2FA Management Functions
|
||||||
{ "initiateTwoFASetup", async (payload, context) => await UserManagerService.InitiateTwoFASetup(context) },
|
{ "initiateTwoFASetup", async (payload, context) => await UserManagerService.InitiateTwoFASetup(context) },
|
||||||
{ "completeTwoFASetup", UserManagerService.CompleteTwoFASetup },
|
{ "completeTwoFASetup", UserManagerService.CompleteTwoFASetup },
|
||||||
@@ -62,15 +56,6 @@ namespace AuthHex.Services.FunctionHandler
|
|||||||
Data = results
|
Data = results
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (UnauthorizedAccessException ex)
|
|
||||||
{
|
|
||||||
return new ApiResponse
|
|
||||||
{
|
|
||||||
StatusCode = 401,
|
|
||||||
Message = ex.Message,
|
|
||||||
Data = null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch (KeyNotFoundException ex)
|
catch (KeyNotFoundException ex)
|
||||||
{
|
{
|
||||||
return new ApiResponse
|
return new ApiResponse
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public class RecoveryManagerService
|
|||||||
if (string.IsNullOrEmpty(identifier))
|
if (string.IsNullOrEmpty(identifier))
|
||||||
throw new ArgumentException("Identifier is required.");
|
throw new ArgumentException("Identifier is required.");
|
||||||
|
|
||||||
var user = await _userRepository.GetUserByIdentifierAndType(identifier, null);
|
var user = await _userRepository.GetUserByIdentifierAndType(identifier);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
throw new KeyNotFoundException("No user found with the provided identifier.");
|
throw new KeyNotFoundException("No user found with the provided identifier.");
|
||||||
|
|
||||||
@@ -119,9 +119,14 @@ public class RecoveryManagerService
|
|||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
ReferenceNumber = recovery.RecoveryReferenceNum,
|
Success = true,
|
||||||
ExpiresAt = recovery.ExpirationTime,
|
Message = "Password reset link sent to your email.",
|
||||||
RecoveryType = "ResetLink"
|
Data = new
|
||||||
|
{
|
||||||
|
ReferenceNumber = recovery.RecoveryReferenceNum,
|
||||||
|
ExpiresAt = recovery.ExpirationTime,
|
||||||
|
RecoveryType = "ResetLink"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -171,9 +176,14 @@ public class RecoveryManagerService
|
|||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
ReferenceNumber = recovery.RecoveryReferenceNum,
|
Success = true,
|
||||||
ExpiresAt = recovery.ExpirationTime,
|
Message = "OTP sent successfully.",
|
||||||
RecoveryType = "OTP"
|
Data = new
|
||||||
|
{
|
||||||
|
ReferenceNumber = recovery.RecoveryReferenceNum,
|
||||||
|
ExpiresAt = recovery.ExpirationTime,
|
||||||
|
RecoveryType = "OTP"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -227,9 +237,14 @@ public class RecoveryManagerService
|
|||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
ReferenceNumber = recovery.RecoveryReferenceNum,
|
Success = true,
|
||||||
UserId = recovery.UserId,
|
Message = "OTP verified successfully. You can now reset your password.",
|
||||||
Verified = true
|
Data = new
|
||||||
|
{
|
||||||
|
ReferenceNumber = recovery.RecoveryReferenceNum,
|
||||||
|
UserId = recovery.UserId,
|
||||||
|
Verified = true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -328,9 +343,14 @@ public class RecoveryManagerService
|
|||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
Success = true,
|
||||||
Email = user.Email,
|
Message = "Password reset successful. Please login with your new password.",
|
||||||
ResetAt = DateTime.UtcNow
|
Data = new
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Email = user.Email,
|
||||||
|
ResetAt = DateTime.UtcNow
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -421,9 +441,14 @@ public class RecoveryManagerService
|
|||||||
|
|
||||||
return new
|
return new
|
||||||
{
|
{
|
||||||
UserId = user.Id,
|
Success = true,
|
||||||
Email = user.Email,
|
Message = "Password reset successful. Please login with your new password.",
|
||||||
ResetAt = DateTime.UtcNow
|
Data = new
|
||||||
|
{
|
||||||
|
UserId = user.Id,
|
||||||
|
Email = user.Email,
|
||||||
|
ResetAt = DateTime.UtcNow
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -28,9 +28,8 @@ public class UserManagerService
|
|||||||
private readonly AppDBContext _dbContext;
|
private readonly AppDBContext _dbContext;
|
||||||
private readonly ThirdPartyService _thirdPartyService;
|
private readonly ThirdPartyService _thirdPartyService;
|
||||||
private readonly UserHelper _userHelper;
|
private readonly UserHelper _userHelper;
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
|
|
||||||
public UserManagerService(IUserManageRepository repository, IRecoveryManagerRepository recoveryRepository, IUnitOfWork uow, JwtTokenHelper jwt, AppDBContext dbContext, ThirdPartyService thirdPartyService, UserHelper userHelper, IConfiguration configuration)
|
public UserManagerService(IUserManageRepository repository, IRecoveryManagerRepository recoveryRepository, IUnitOfWork uow, JwtTokenHelper jwt, AppDBContext dbContext, ThirdPartyService thirdPartyService, UserHelper userHelper)
|
||||||
{
|
{
|
||||||
_repository = repository;
|
_repository = repository;
|
||||||
_recoveryRepository = recoveryRepository;
|
_recoveryRepository = recoveryRepository;
|
||||||
@@ -39,7 +38,6 @@ public class UserManagerService
|
|||||||
_dbContext = dbContext;
|
_dbContext = dbContext;
|
||||||
_thirdPartyService = thirdPartyService;
|
_thirdPartyService = thirdPartyService;
|
||||||
_userHelper = userHelper;
|
_userHelper = userHelper;
|
||||||
_configuration = configuration;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> RegisterUser(object payload, HttpContext httpContext)
|
public async Task<object> RegisterUser(object payload, HttpContext httpContext)
|
||||||
@@ -65,27 +63,19 @@ public class UserManagerService
|
|||||||
|
|
||||||
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
|
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
|
||||||
|
|
||||||
// A "roleId" in the payload (old ERP builds still send one) is accepted and ignored: roles are the ERP's business.
|
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var RoleId))
|
||||||
|
throw new ArgumentException("Invalid roleId");
|
||||||
if (!data.ContainsKey("userTypeId") || !Guid.TryParse(data["userTypeId"].GetString(), out var UserTypeId))
|
|
||||||
throw new ArgumentException("Invalid userTypeId");
|
|
||||||
|
|
||||||
|
|
||||||
//chk user exist
|
//chk user exist
|
||||||
if (data.ContainsKey("chkUser") == true) {
|
var userToCheck = new User
|
||||||
var userToCheck = new User
|
{
|
||||||
{
|
Email = Email,
|
||||||
FullName = FullName,
|
MobileNumber = MobileNumber,
|
||||||
Email = Email,
|
Nic = Nic,
|
||||||
MobileNumber = MobileNumber,
|
};
|
||||||
Nic = Nic,
|
bool canSave = await CanRegisterUser(userToCheck);
|
||||||
UserTypeId = UserTypeId
|
if (!canSave)
|
||||||
};
|
throw new InvalidOperationException("User already exists");
|
||||||
bool canSave = await CanRegisterUser(userToCheck);
|
|
||||||
|
|
||||||
if (!canSave)
|
|
||||||
throw new InvalidOperationException("User already exists");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// if send password empty generate random password
|
// if send password empty generate random password
|
||||||
@@ -108,7 +98,7 @@ public class UserManagerService
|
|||||||
Nic = Nic,
|
Nic = Nic,
|
||||||
Email = Email,
|
Email = Email,
|
||||||
MobileNumber = MobileNumber,
|
MobileNumber = MobileNumber,
|
||||||
UserTypeId = UserTypeId,
|
RoleCode = RoleId.ToString(),
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
IsLocked = false,
|
IsLocked = false,
|
||||||
@@ -191,7 +181,7 @@ public class UserManagerService
|
|||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
user.MobileNumberVerified,
|
user.MobileNumberVerified,
|
||||||
user.IsMfaEnabled,
|
user.IsMfaEnabled,
|
||||||
UserTypeId = user.UserTypeId
|
RoleId = user.RoleCode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -219,13 +209,12 @@ public class UserManagerService
|
|||||||
|
|
||||||
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
|
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
|
||||||
var password = data.ContainsKey("password") ? data["password"].GetString() : null;
|
var password = data.ContainsKey("password") ? data["password"].GetString() : null;
|
||||||
var userTypeId = data.ContainsKey("userTypeId") && Guid.TryParse(data["userTypeId"].GetString(), out var utid) ? utid : (Guid?)null;
|
|
||||||
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
|
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(identifier))
|
if (string.IsNullOrWhiteSpace(identifier))
|
||||||
throw new ArgumentException("Identifier and password are required");
|
throw new ArgumentException("Identifier and password are required");
|
||||||
|
|
||||||
var user = await _repository.GetUserByIdentifierAndType(identifier, userTypeId);
|
var user = await _repository.GetUserByIdentifierAndType(identifier);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -299,7 +288,7 @@ public class UserManagerService
|
|||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
user.MobileNumberVerified,
|
user.MobileNumberVerified,
|
||||||
user.IsMfaEnabled,
|
user.IsMfaEnabled,
|
||||||
UserTypeId = user.UserTypeId
|
RoleId = user.RoleCode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -407,7 +396,7 @@ public class UserManagerService
|
|||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
user.MobileNumberVerified,
|
user.MobileNumberVerified,
|
||||||
user.IsMfaEnabled,
|
user.IsMfaEnabled,
|
||||||
UserTypeId = user.UserTypeId
|
RoleId = user.RoleCode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -499,7 +488,7 @@ public class UserManagerService
|
|||||||
user.EmailVerified,
|
user.EmailVerified,
|
||||||
user.MobileNumberVerified,
|
user.MobileNumberVerified,
|
||||||
user.IsMfaEnabled,
|
user.IsMfaEnabled,
|
||||||
UserTypeId = user.UserTypeId
|
RoleId = user.RoleCode
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -545,12 +534,7 @@ public class UserManagerService
|
|||||||
user.IsActive,
|
user.IsActive,
|
||||||
user.IsLocked,
|
user.IsLocked,
|
||||||
user.CreatedAt,
|
user.CreatedAt,
|
||||||
UserType = new
|
Role = user.RoleCode
|
||||||
{
|
|
||||||
user.UserType?.UserTypeId,
|
|
||||||
user.UserType?.Code,
|
|
||||||
user.UserType?.Description
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -559,17 +543,6 @@ public class UserManagerService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object> ListUserTypes(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
var userTypes = await _dbContext.UserType.OrderBy(t => t.Code).ToListAsync();
|
|
||||||
return userTypes.Select(t => new
|
|
||||||
{
|
|
||||||
t.UserTypeId,
|
|
||||||
t.Code,
|
|
||||||
t.Description
|
|
||||||
}).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> GetUserSessions(HttpContext httpContext)
|
public async Task<object> GetUserSessions(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -782,7 +755,8 @@ public class UserManagerService
|
|||||||
if (data == null || !data.Any())
|
if (data == null || !data.Any())
|
||||||
throw new ArgumentException("Invalid payload data");
|
throw new ArgumentException("Invalid payload data");
|
||||||
|
|
||||||
var userId = _userHelper.GetUserIdFromClaims(httpContext);
|
if (!data.ContainsKey("userId") || !Guid.TryParse(data["userId"].GetString(), out var userId))
|
||||||
|
throw new ArgumentException("Invalid userId");
|
||||||
|
|
||||||
var user = await _repository.GetUserByIdAsync(userId);
|
var user = await _repository.GetUserByIdAsync(userId);
|
||||||
|
|
||||||
@@ -851,6 +825,12 @@ public class UserManagerService
|
|||||||
updated = true;
|
updated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.ContainsKey("roleCode"))
|
||||||
|
{
|
||||||
|
user.RoleCode = data["roleCode"].GetString();
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.ContainsKey("newPassword"))
|
if (data.ContainsKey("newPassword"))
|
||||||
{
|
{
|
||||||
var currentPassword = data.ContainsKey("currentPassword") ? data["currentPassword"].GetString() : null;
|
var currentPassword = data.ContainsKey("currentPassword") ? data["currentPassword"].GetString() : null;
|
||||||
@@ -1199,7 +1179,7 @@ public class UserManagerService
|
|||||||
|
|
||||||
foreach (var user in existingUsers)
|
foreach (var user in existingUsers)
|
||||||
{
|
{
|
||||||
if (user.UserTypeId == userToCheck.UserTypeId)
|
if (user.RoleCode == userToCheck.RoleCode)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (user.IsLocked == false)
|
if (user.IsLocked == false)
|
||||||
@@ -1245,162 +1225,4 @@ public class UserManagerService
|
|||||||
return System.Text.RegularExpressions.Regex.IsMatch(cleaned, @"^\d{7,15}$");
|
return System.Text.RegularExpressions.Regex.IsMatch(cleaned, @"^\d{7,15}$");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Admin-only functions (require X-Admin-Api-Key) ----------------------------
|
|
||||||
// Everything above acts on the caller's own account, resolved from the bearer token's
|
|
||||||
// claims. These three act on an arbitrary target user (by id, or by identifier for
|
|
||||||
// delete) on an admin's behalf — ERPCore calls them with a shared secret instead of a
|
|
||||||
// user bearer token, since the admin and the target are different people.
|
|
||||||
|
|
||||||
private bool IsAuthorizedAdmin(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
var expected = _configuration["AuthHex:AdminApiKey"];
|
|
||||||
var provided = httpContext.Request.Headers["X-Admin-Api-Key"].FirstOrDefault();
|
|
||||||
return !string.IsNullOrEmpty(expected) && provided == expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> AdminUpdateUser(object payload, HttpContext httpContext)
|
|
||||||
{
|
|
||||||
if (!IsAuthorizedAdmin(httpContext))
|
|
||||||
throw new UnauthorizedAccessException("A valid X-Admin-Api-Key header is required for this operation.");
|
|
||||||
|
|
||||||
var data = DeserializePayload.Deserialize(payload);
|
|
||||||
if (!data.ContainsKey("userId"))
|
|
||||||
throw new ArgumentException("userId is required");
|
|
||||||
|
|
||||||
var userId = Guid.Parse(data["userId"].GetString() ?? throw new ArgumentException("userId is required"));
|
|
||||||
var user = await _repository.GetUserByIdAsync(userId)
|
|
||||||
?? throw new ArgumentException("User not found");
|
|
||||||
|
|
||||||
await _uow.BeginAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (data.ContainsKey("fullName")) user.FullName = data["fullName"].GetString();
|
|
||||||
if (data.ContainsKey("userName")) user.UserName = data["userName"].GetString();
|
|
||||||
if (data.ContainsKey("email"))
|
|
||||||
{
|
|
||||||
user.Email = data["email"].GetString();
|
|
||||||
user.EmailVerified = false;
|
|
||||||
}
|
|
||||||
if (data.ContainsKey("mobileNumber"))
|
|
||||||
{
|
|
||||||
user.MobileNumber = data["mobileNumber"].GetString();
|
|
||||||
user.MobileNumberVerified = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _uow.CommitAsync();
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
user.Id,
|
|
||||||
user.FullName,
|
|
||||||
user.UserName,
|
|
||||||
user.Email,
|
|
||||||
user.MobileNumber,
|
|
||||||
user.IsActive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await _uow.RollbackAsync();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<object> AdminSetUserStatus(object payload, HttpContext httpContext)
|
|
||||||
{
|
|
||||||
if (!IsAuthorizedAdmin(httpContext))
|
|
||||||
throw new UnauthorizedAccessException("A valid X-Admin-Api-Key header is required for this operation.");
|
|
||||||
|
|
||||||
var data = DeserializePayload.Deserialize(payload);
|
|
||||||
if (!data.ContainsKey("userId"))
|
|
||||||
throw new ArgumentException("userId is required");
|
|
||||||
|
|
||||||
var userId = Guid.Parse(data["userId"].GetString() ?? throw new ArgumentException("userId is required"));
|
|
||||||
var isActive = data.ContainsKey("isActive") ? data["isActive"].GetBoolean() : (bool?)null;
|
|
||||||
if (isActive == null)
|
|
||||||
throw new ArgumentException("isActive is required");
|
|
||||||
|
|
||||||
var user = await _repository.GetUserByIdAsync(userId)
|
|
||||||
?? throw new ArgumentException("User not found");
|
|
||||||
|
|
||||||
await _uow.BeginAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
user.IsActive = isActive.Value;
|
|
||||||
|
|
||||||
await _uow.CommitAsync();
|
|
||||||
|
|
||||||
// Deactivating must actually end any live sessions, not just flip a flag that
|
|
||||||
// existing refresh tokens ignore.
|
|
||||||
if (!isActive.Value)
|
|
||||||
await _repository.InvalidateUserSessionsAsync(user.Id);
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
|
||||||
user.Id,
|
|
||||||
user.FullName,
|
|
||||||
user.UserName,
|
|
||||||
user.Email,
|
|
||||||
user.IsActive
|
|
||||||
};
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await _uow.RollbackAsync();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hard-deletes one or more users matched by any of the given identifiers. Sessions,
|
|
||||||
/// tokens, MFA and provider links cascade at the DB level (AppDBContext.OnModelCreating);
|
|
||||||
/// Recovery rows have a plain Restrict FK and must be removed explicitly first.
|
|
||||||
/// </summary>
|
|
||||||
public async Task<object> DeleteUser(object payload, HttpContext httpContext)
|
|
||||||
{
|
|
||||||
if (!IsAuthorizedAdmin(httpContext))
|
|
||||||
throw new UnauthorizedAccessException("A valid X-Admin-Api-Key header is required for this operation.");
|
|
||||||
|
|
||||||
var data = DeserializePayload.Deserialize(payload);
|
|
||||||
string? email = data.ContainsKey("email") ? data["email"].GetString() : null;
|
|
||||||
string? mobileNumber = data.ContainsKey("mobileNumber") ? data["mobileNumber"].GetString() : null;
|
|
||||||
string? nic = data.ContainsKey("nic") ? data["nic"].GetString() : null;
|
|
||||||
string? userName = data.ContainsKey("userName") ? data["userName"].GetString() : null;
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(email) && string.IsNullOrWhiteSpace(mobileNumber)
|
|
||||||
&& string.IsNullOrWhiteSpace(nic) && string.IsNullOrWhiteSpace(userName))
|
|
||||||
throw new ArgumentException("At least one of email, mobileNumber, nic or userName is required.");
|
|
||||||
|
|
||||||
var matches = await _dbContext.Users
|
|
||||||
.Where(u =>
|
|
||||||
(email != null && u.Email == email) ||
|
|
||||||
(mobileNumber != null && u.MobileNumber == mobileNumber) ||
|
|
||||||
(nic != null && u.Nic == nic) ||
|
|
||||||
(userName != null && u.UserName == userName))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
if (matches.Count == 0)
|
|
||||||
throw new ArgumentException("No matching user was found.");
|
|
||||||
|
|
||||||
var ids = matches.Select(u => u.Id).ToList();
|
|
||||||
var deleted = matches.Select(u => new { u.Id, u.FullName, u.UserName, u.Email }).ToList();
|
|
||||||
|
|
||||||
await _uow.BeginAsync();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_dbContext.Recovery.RemoveRange(_dbContext.Recovery.Where(r => r.UserId != null && ids.Contains(r.UserId.Value)));
|
|
||||||
_dbContext.AuthEventLogs.RemoveRange(_dbContext.AuthEventLogs.Where(l => l.UserId != null && ids.Contains(l.UserId.Value)));
|
|
||||||
_dbContext.Users.RemoveRange(matches);
|
|
||||||
|
|
||||||
await _uow.CommitAsync();
|
|
||||||
|
|
||||||
return new { deletedCount = deleted.Count, deleted };
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
await _uow.RollbackAsync();
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,20 +22,12 @@ public class JwtTokenHelper
|
|||||||
|
|
||||||
public async Task<string> GenerateToken(User user)
|
public async Task<string> GenerateToken(User user)
|
||||||
{
|
{
|
||||||
// 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)
|
|
||||||
.Reference(u => u.UserType)
|
|
||||||
.LoadAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
new Claim("UserId", user.Id.ToString()),
|
new Claim("UserId", user.Id.ToString()),
|
||||||
new Claim("UserTypeId", user.UserTypeId.ToString()),
|
new Claim("RoleId", user.RoleCode ?? ""),
|
||||||
new Claim("UserTypeCode", user.UserType?.Code ?? ""),
|
new Claim("RoleCode", user.RoleCode ?? ""),
|
||||||
|
|
||||||
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,
|
||||||
|
|||||||
+1
-4
@@ -13,10 +13,7 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"AppSettings": {
|
"AppSettings": {
|
||||||
"FrontendUrl": "https://erp.hexdive.com"
|
"FrontendUrl": "http://localhost:3000"
|
||||||
},
|
|
||||||
"AuthHex": {
|
|
||||||
"AdminApiKey": "fd85fb9db2e0533fe82a1652ad42936b5a313a9146dc536e46b1e736861290c9"
|
|
||||||
},
|
},
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
|
|||||||
Reference in New Issue
Block a user