12 KiB
AuthHex API Documentation
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.
Common envelope
Request (ApiRequest)
{
"functionName": "string (required, ignored for dedicated routes below)",
"payload": { "...": "function-specific object" },
"reference": "string (required, can be empty)"
}
Response (ApiResponse)
{
"statusCode": 200,
"success": true,
"message": "success",
"data": { "...": "function-specific object" }
}
Errors: 404 unknown function, 400 (KeyNotFoundException/InvalidOperationException), 500 (ArgumentException).
Routes
| Route | Method | Dispatches to | FunctionName |
|---|---|---|---|
/api/status |
GET | — | — (health check) |
/api/user |
POST | UserManager | from body |
/api/recovery |
POST | RecoveryManager | from body |
/api/registerUser |
POST | UserManager | forced registerUser |
/api/loginUser |
POST | UserManager | forced loginUser |
/api/forgotPassword |
POST | RecoveryManager | forced forgotPassword |
/api/alt |
POST | AltOptionManager | from body |
/api/role |
POST | RoleManager | from body |
GET /api/status
Response:
{ "status": "API is running", "timestamp": "2026-07-03T00:00:00Z" }
/api/user — UserManager functions
registerUser
Payload:
{
"userId": "guid (required)",
"fullname": "string?",
"userName": "string?",
"nic": "string?",
"email": "string?",
"mobileNumber": "string?",
"deviceName": "string? (default 'Unknown Device')",
"roleId": "guid (required)",
"userTypeId": "guid (required)",
"chkUser": "bool? (if true, checks for existing conflicting user)",
"password": "string? (auto-generated if empty)"
}
Data:
{
"accessToken": "jwt",
"refreshToken": "string",
"expiresIn": 3600,
"user": { "id": "guid", "fullName": "", "userName": "", "email": "", "mobileNumber": "", "emailVerified": false, "mobileNumberVerified": false, "isMfaEnabled": false, "roleId": "guid", "userTypeId": "guid" }
}
loginUser
Payload:
{
"identifier": "string (required)",
"password": "string?",
"userTypeId": "guid?",
"deviceName": "string? (default 'Unknown Device')"
}
Data: same shape as registerUser data.
VerifyOtpForLogin
Payload:
{ "referenceNumber": "string (required)", "otpCode": "string (required)", "deviceName": "string?" }
Data:
{
"success": true,
"message": "OTP verified successfully.",
"data": {
"referenceNumber": "", "userId": "guid", "verified": true,
"accessToken": "jwt", "refreshToken": "", "expiresIn": 3600,
"user": { "...": "as above" }
}
}
refreshToken
Payload:
{ "refreshToken": "string (required)", "deviceName": "string?" }
Data: same shape as registerUser data (rotates session).
getUserDetails
Payload:
{ "userId": "guid (required)" }
Data:
{
"id": "guid", "fullName": "", "userName": "", "email": "", "nic": "",
"mobileNumber": "", "emailVerified": false, "mobileNumberVerified": false,
"isMfaEnabled": false, "isActive": true, "isLocked": false, "createdAt": "",
"role": { "roleId": "", "code": "", "name": "" },
"userType": { "userTypeId": "", "code": "", "description": "" }
}
getUserSessions
Requires auth (userId from claims). No payload needed. Data: array of
{ "sessionId": "", "deviceName": "", "browser": "", "os": "", "ipAddress": "", "createdAt": "", "expiresAt": "", "revokedAt": "", "isActive": true }
ChangeUserStatus
Requires auth. Payload:
{ "isActive": "bool (required)" }
Data: { "id", "fullName", "userName", "email", "mobileNumber", "isActive" }
LockUserAccount
Requires auth. Payload:
{ "isLocked": "bool (required)" }
Data: { "id", "fullName", "userName", "email", "mobileNumber", "isLocked", "deletedAt" }
(Also invalidates all user sessions.)
ChangeUserPassword
Requires auth. Payload:
{ "currentPassword": "string (required)", "newPassword": "string (required)" }
Data: { "message": "Password changed successfully" }
LogoutUser
Payload:
{ "userId": "guid (required)" }
Data: { "message": "User logged out successfully" }
UpdateUser
Requires auth. Payload (all optional, at least one required):
{
"fullName": "string?", "userName": "string?", "nic": "string?", "address": "string?",
"optional1": "string?", "optional2": "string?",
"email": "string|null", "mobileNumber": "string|null",
"currentPassword": "string? (required if newPassword set and existing password set)",
"newPassword": "string?"
}
Data:
{
"message": "User updated successfully",
"user": { "id", "fullName", "userName", "nic", "address", "optional1", "optional2", "email", "emailVerified", "mobileNumber", "mobileNumberVerified" }
}
2FA — initiateTwoFASetup
Requires auth. No payload. Data: third-party-service-defined setup result (QR/secret info).
2FA — completeTwoFASetup
Requires auth. Payload:
{ "secretKey": "string (required)", "verificationCode": "string (required)" }
Data:
{
"message": "2FA setup completed successfully",
"backupCodes": ["..."],
"user": { "id", "fullName", "userName", "email", "isMfaEnabled" }
}
2FA — verifyTwoFA
Requires auth. Payload:
{ "verificationCode": "string (required)" }
Data: raw third-party verification result object.
2FA — disableTwoFA
Requires auth. Payload:
{ "verificationCode": "string (required)" }
Data:
{ "message": "2FA has been disabled successfully", "user": { "id", "fullName", "userName", "email", "isMfaEnabled" } }
2FA — getTwoFAStatus
Requires auth. No payload. Data:
{ "isMfaEnabled": false, "isVerified": false, "lastUsedAt": null, "verifiedAt": null, "hasBackupCodes": false }
Admin user management (userId-targeted)
These act on an explicit userId in the payload (unlike ChangeUserStatus/LockUserAccount/UpdateUser,
which act on the token's own user). Used by ERP-core's UsersController.
listUsers
Payload: { "page": 1, "pageSize": 20, "q": "search?" }. Data: { "items": [ { id, fullName, userName, email, mobileNumber, nic, isActive, isLocked, createdAt, roleId, roleCode, roleName, userTypeId, userTypeCode } ], "total": N, "page": 1, "pageSize": 20 }.
adminUpdateUser
Payload: { "userId": "guid (required)", "fullName?", "userName?", "nic?", "email?", "mobileNumber?", "address?", "roleId?", "userTypeId?", "newPassword?" } — only keys present are applied. Data: the updated user summary.
adminSetStatus
Payload: { "userId": "guid (required)", "isActive": true|false }. Deactivating also invalidates the user's sessions. Data: { id, isActive }.
adminSetLock
Payload: { "userId": "guid (required)", "isLocked": true|false }. Locking also invalidates the user's sessions. Data: { id, isLocked }.
listUserTypes
Payload: {}. Data: [ { userTypeId, code, description } ]. (Seeded at startup: ERP_ADMIN, ERP_STAFF.)
/api/recovery — RecoveryManager functions
forgotPassword
Payload:
{ "identifier": "string (required)", "useResetLink": "bool? (default false)", "numberOfDigits": "int? (default 6)" }
Data (useResetLink = true):
{ "success": true, "message": "Password reset link sent to your email.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "ResetLink" } }
Data (useResetLink = false, OTP flow):
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP" } }
verifyOTP
Payload:
{ "referenceNumber": "string (required)", "otpCode": "string (required)" }
Data:
{ "success": true, "message": "OTP verified successfully. You can now reset your password.", "data": { "referenceNumber": "", "userId": "guid", "verified": true } }
resetPasswordWithToken
Payload:
{ "resetToken": "string (required)", "newPassword": "string (required, min 8 chars)", "confirmPassword": "string (required, must match)" }
Data:
{ "success": true, "message": "Password reset successful. Please login with your new password.", "data": { "userId": "guid", "email": "", "resetAt": "" } }
resetPassword
Payload:
{ "referenceNumber": "string (required)", "newPassword": "string (required, min 8 chars)", "confirmPassword": "string (required, must match)" }
(Requires recovery status = "Verified" via prior verifyOTP call.)
Data: same shape as resetPasswordWithToken.
/api/alt — AltOptionManager functions
IsAvailable
Payload:
{ "Identifier": "string?", "Recovery": "string? (any non-null value flags recovery-mode)" }
Data (available): { "isAvailable": true, "message": "Identifier is available" }
Data (taken, no Recovery): { "isAvailable": false, "message": "Identifier already in use" }
Data (taken, Recovery set): { "existingUsers": [...] }
sendOtp
Payload:
{ "identifier": "string (required, email or mobile)", "numberOfDigits": "int? (default 4)", "newUser": "bool? (default false)" }
Data (newUser = true):
{ "success": true, "message": "OTP sent successfully for new user.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR NEW USER" } }
Data (existing user):
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR LOGIN" } }
VerifyOTP (alt)
Payload:
{
"userId": "guid?", "referenceNumber": "string (required)", "otpCode": "string (required)",
"newUser": "bool? (default false)", "identifier": "string? (email/mobile/other — marks it verified on user)",
"deviceName": "string? (default 'Unknown Device')"
}
Data:
{
"success": true,
"message": "OTP verified successfully.",
"data": {
"referenceNumber": "", "userId": "guid", "verified": true,
"accessToken": "jwt", "refreshToken": "", "expiresIn": 3600,
"user": { "id", "fullName", "userName", "email", "mobileNumber", "emailVerified", "mobileNumberVerified", "isMfaEnabled", "roleId", "userTypeId" }
}
}
/api/role — RoleManager functions
AuthHex owns only the Role lookup table (identity source for the JWT RoleId/RoleCode claims).
Roles are authored in the main system (ERP-core) and cloned here via createRole using the same
RoleId so the two role stores stay in sync. This route is currently AllowAnonymous (server-to-server
call from ERP-core); tighten it with a service credential when dual-auth is introduced.
createRole
Clone a role created in ERP-core. Accepts the caller-supplied roleId so the AuthHex row matches.
Payload:
{ "roleId": "guid (required)", "code": "string (required, unique)", "name": "string?", "isSystemRole": "bool?" }
Data:
{ "roleId": "guid", "code": "ADMIN", "name": "Administrator", "isSystemRole": false, "createdAt": "2026-07-16T00:00:00Z" }
Errors: duplicate roleId or code → 409; missing/invalid roleId/code → 400.
listRoles
Payload: {} (none). Data: array of the role object shown above.
getRole
Payload: { "roleId": "guid (required)" }. Data: the role object above; unknown id → 404.
deleteRole
Payload: { "roleId": "guid (required)" }. Deletes the role. Blocked with 409 if any user is still assigned
(the User.RoleId FK is Restrict) so ERP-core — which calls this before removing its own copy — never orphans an
in-use role. Data: { "roleId": "guid", "deleted": true }; unknown id → 404.
Notes
- All timestamps are UTC.
deviceName,Browser,OS,IPAddressare captured per session from request headers for session tracking (getUserSessions).- Password login validation is active —
loginUserverifies the BCrypt hash viaPasswordHasher.Verifyand throwsInvalid credentialson mismatch. Login identifiers are email / mobile / NIC (not username). Register with an explicitpassword(a blank one is randomized → unusable). - Bootstrap seed (idempotent, at startup —
Infra/Seed/AuthHexSeeder.cs): user typesERP_ADMIN/ERP_STAFF, anADMINrole (fixed GUID11111111-…, mirrored by ERP-core), and an admin user (Bootstrap:AdminEmaildefaultadmin@erp.local/Bootstrap:AdminPassworddefaultAdmin@123). - JWT access tokens issued with
expiresIn: 3600(1 hour); refresh tokens/sessions expire after 30 days.