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:
Dhananjaya99
2026-10-19 12:57:07 +05:30
parent fbd079d337
commit 15832d1ae3
19 changed files with 393 additions and 297 deletions
+28 -206
View File
@@ -28,9 +28,8 @@ public class UserManagerService
private readonly AppDBContext _dbContext;
private readonly ThirdPartyService _thirdPartyService;
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;
_recoveryRepository = recoveryRepository;
@@ -39,7 +38,6 @@ public class UserManagerService
_dbContext = dbContext;
_thirdPartyService = thirdPartyService;
_userHelper = userHelper;
_configuration = configuration;
}
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";
// 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");
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var RoleId))
throw new ArgumentException("Invalid roleId");
//chk user exist
if (data.ContainsKey("chkUser") == true) {
var userToCheck = new User
{
FullName = FullName,
Email = Email,
MobileNumber = MobileNumber,
Nic = Nic,
UserTypeId = UserTypeId
};
bool canSave = await CanRegisterUser(userToCheck);
if (!canSave)
throw new InvalidOperationException("User already exists");
}
var userToCheck = new User
{
Email = Email,
MobileNumber = MobileNumber,
Nic = Nic,
};
bool canSave = await CanRegisterUser(userToCheck);
if (!canSave)
throw new InvalidOperationException("User already exists");
// if send password empty generate random password
@@ -108,7 +98,7 @@ public class UserManagerService
Nic = Nic,
Email = Email,
MobileNumber = MobileNumber,
UserTypeId = UserTypeId,
RoleCode = RoleId.ToString(),
CreatedAt = DateTime.UtcNow,
IsActive = true,
IsLocked = false,
@@ -191,7 +181,7 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
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 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";
if (string.IsNullOrWhiteSpace(identifier))
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.MobileNumberVerified,
user.IsMfaEnabled,
UserTypeId = user.UserTypeId
RoleId = user.RoleCode
}
};
}
@@ -407,7 +396,7 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
UserTypeId = user.UserTypeId
RoleId = user.RoleCode
}
}
};
@@ -499,7 +488,7 @@ public class UserManagerService
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
UserTypeId = user.UserTypeId
RoleId = user.RoleCode
}
};
}
@@ -545,12 +534,7 @@ public class UserManagerService
user.IsActive,
user.IsLocked,
user.CreatedAt,
UserType = new
{
user.UserType?.UserTypeId,
user.UserType?.Code,
user.UserType?.Description
}
Role = user.RoleCode
};
}
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)
{
try
@@ -782,7 +755,8 @@ public class UserManagerService
if (data == null || !data.Any())
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);
@@ -851,6 +825,12 @@ public class UserManagerService
updated = true;
}
if (data.ContainsKey("roleCode"))
{
user.RoleCode = data["roleCode"].GetString();
updated = true;
}
if (data.ContainsKey("newPassword"))
{
var currentPassword = data.ContainsKey("currentPassword") ? data["currentPassword"].GetString() : null;
@@ -1199,7 +1179,7 @@ public class UserManagerService
foreach (var user in existingUsers)
{
if (user.UserTypeId == userToCheck.UserTypeId)
if (user.RoleCode == userToCheck.RoleCode)
return false;
if (user.IsLocked == false)
@@ -1245,162 +1225,4 @@ public class UserManagerService
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;
}
}
}