Add admin functions to UserManagerService and update appsettings for AdminApiKey

This commit is contained in:
Dhananjaya99
2026-10-15 10:44:51 +05:30
parent f1cf6d0fad
commit 0dd86dc98d
4 changed files with 194 additions and 42 deletions
+14
View File
@@ -26,6 +26,11 @@ namespace AuthHex.Services.FunctionHandler
{"LogoutUser" , UserManagerService.LogoutUser}, {"LogoutUser" , UserManagerService.LogoutUser},
{"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) },
@@ -57,6 +62,15 @@ 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
@@ -119,14 +119,9 @@ public class RecoveryManagerService
return new return new
{ {
Success = true, ReferenceNumber = recovery.RecoveryReferenceNum,
Message = "Password reset link sent to your email.", ExpiresAt = recovery.ExpirationTime,
Data = new RecoveryType = "ResetLink"
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = "ResetLink"
}
}; };
} }
else else
@@ -176,14 +171,9 @@ public class RecoveryManagerService
return new return new
{ {
Success = true, ReferenceNumber = recovery.RecoveryReferenceNum,
Message = "OTP sent successfully.", ExpiresAt = recovery.ExpirationTime,
Data = new RecoveryType = "OTP"
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = "OTP"
}
}; };
} }
} }
@@ -237,14 +227,9 @@ public class RecoveryManagerService
return new return new
{ {
Success = true, ReferenceNumber = recovery.RecoveryReferenceNum,
Message = "OTP verified successfully. You can now reset your password.", UserId = recovery.UserId,
Data = new Verified = true
{
ReferenceNumber = recovery.RecoveryReferenceNum,
UserId = recovery.UserId,
Verified = true
}
}; };
} }
catch catch
@@ -343,14 +328,9 @@ public class RecoveryManagerService
return new return new
{ {
Success = true, UserId = user.Id,
Message = "Password reset successful. Please login with your new password.", Email = user.Email,
Data = new ResetAt = DateTime.UtcNow
{
UserId = user.Id,
Email = user.Email,
ResetAt = DateTime.UtcNow
}
}; };
} }
catch catch
@@ -441,14 +421,9 @@ public class RecoveryManagerService
return new return new
{ {
Success = true, UserId = user.Id,
Message = "Password reset successful. Please login with your new password.", Email = user.Email,
Data = new ResetAt = DateTime.UtcNow
{
UserId = user.Id,
Email = user.Email,
ResetAt = DateTime.UtcNow
}
}; };
} }
catch catch
+161 -1
View File
@@ -28,8 +28,9 @@ 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) public UserManagerService(IUserManageRepository repository, IRecoveryManagerRepository recoveryRepository, IUnitOfWork uow, JwtTokenHelper jwt, AppDBContext dbContext, ThirdPartyService thirdPartyService, UserHelper userHelper, IConfiguration configuration)
{ {
_repository = repository; _repository = repository;
_recoveryRepository = recoveryRepository; _recoveryRepository = recoveryRepository;
@@ -38,6 +39,7 @@ 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)
@@ -1259,4 +1261,162 @@ 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;
}
}
} }
+4 -1
View File
@@ -13,7 +13,10 @@
}, },
"AllowedHosts": "*", "AllowedHosts": "*",
"AppSettings": { "AppSettings": {
"FrontendUrl": "http://localhost:3000" "FrontendUrl": "https://erp.hexdive.com"
},
"AuthHex": {
"AdminApiKey": "fd85fb9db2e0533fe82a1652ad42936b5a313a9146dc536e46b1e736861290c9"
}, },
"Kestrel": { "Kestrel": {
"Endpoints": { "Endpoints": {