Add admin functions to UserManagerService and update appsettings for AdminApiKey
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -13,7 +13,10 @@
|
|||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"AppSettings": {
|
"AppSettings": {
|
||||||
"FrontendUrl": "http://localhost:3000"
|
"FrontendUrl": "https://erp.hexdive.com"
|
||||||
|
},
|
||||||
|
"AuthHex": {
|
||||||
|
"AdminApiKey": "fd85fb9db2e0533fe82a1652ad42936b5a313a9146dc536e46b1e736861290c9"
|
||||||
},
|
},
|
||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
|
|||||||
Reference in New Issue
Block a user