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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user