This commit is contained in:
Kalana B. Thilakarathna
2026-10-19 02:14:25 +05:30
commit f081bca82e
57 changed files with 7274 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
name: CI/CD - erp auth
on:
pull_request:
branches:
- production
types:
- closed
jobs:
build-and-deploy:
name: Build & Deploy
runs-on: linux
if: github.event.pull_request.merged == true
steps:
- name: Checkout code
run: |
git clone \
--depth 1 \
--branch main \
https://oauth2:${{ secrets.ERPAUTH }}@gitea.hexdive.com/New_REP_SYSTEM/Distribution_auth.git \
.
- name: Restore
run: dotnet restore AuthHex.sln
- name: Publish
run: |
dotnet publish AuthHex.csproj \
-c Release \
-r linux-x64 \
--self-contained true \
-o /tmp/authhex-deploy
- name: Stop service
run: systemctl stop erpAuth.service || true
- name: Deploy
run: |
cp -r /tmp/authhex-deploy/* /var/www/internalServices/disAuth/publish/
- name: Fix ownership and permissions
run: |
chown -R hexafitauth:hexafitauth /var/www/internalServices/disAuth/publish/
find /var/www/internalServices/disAuth/publish/ -type d -exec chmod 755 {} +
find /var/www/internalServices/disAuth/publish/ -type f -exec chmod 644 {} +
chmod +x /var/www/internalServices/disAuth/publish/AuthHex
- name: Start service
run: systemctl start disAuth.service
- name: Verify
run: |
sleep 3
systemctl is-active disAuth.service \
&& echo "Service running" \
|| (journalctl -u disAuth.service -n 30 --no-pager && exit 1)
+22
View File
@@ -0,0 +1,22 @@
# Build artifacts
bin/
obj/
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
# Logs
*.log
# Entity Framework migrations
Migrations/
# Security - RSA Keys and sensitive config
*.key
*.pem
GenerateKeys.ps1
+385
View File
@@ -0,0 +1,385 @@
# 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`)
```json
{
"functionName": "string (required, ignored for dedicated routes below)",
"payload": { "...": "function-specific object" },
"reference": "string (required, can be empty)"
}
```
**Response** (`ApiResponse`)
```json
{
"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:
```json
{ "status": "API is running", "timestamp": "2026-07-03T00:00:00Z" }
```
---
## /api/user — UserManager functions
### registerUser
Payload:
```json
{
"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)",
"sendCredentialsEmail": "bool? (default true; emails `email` the username + password when set)"
}
```
The generated/supplied password is now persisted (hashed) against the user record (previously a bug left it unset). When `email` is present and `sendCredentialsEmail` is not `false`, an email with the username and password is sent best-effort after the user is committed (failures do not roll back registration).
Data:
```json
{
"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:
```json
{
"identifier": "string (required)",
"password": "string?",
"userTypeId": "guid?",
"deviceName": "string? (default 'Unknown Device')"
}
```
Data: same shape as `registerUser` data.
### VerifyOtpForLogin
Payload:
```json
{ "referenceNumber": "string (required)", "otpCode": "string (required)", "deviceName": "string?" }
```
Data:
```json
{
"success": true,
"message": "OTP verified successfully.",
"data": {
"referenceNumber": "", "userId": "guid", "verified": true,
"accessToken": "jwt", "refreshToken": "", "expiresIn": 3600,
"user": { "...": "as above" }
}
}
```
### refreshToken
Payload:
```json
{ "refreshToken": "string (required)", "deviceName": "string?" }
```
Data: same shape as `registerUser` data (rotates session).
### getUserDetails
Payload:
```json
{ "userId": "guid (required)" }
```
Data:
```json
{
"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": "" }
}
```
### listUserTypes
No payload.
Data: array of
```json
{ "userTypeId": "guid", "code": "", "description": "" }
```
### getUserSessions
_Requires auth (userId from claims). No payload needed._
Data: array of
```json
{ "sessionId": "", "deviceName": "", "browser": "", "os": "", "ipAddress": "", "createdAt": "", "expiresAt": "", "revokedAt": "", "isActive": true }
```
### ChangeUserStatus
_Requires auth._ Payload:
```json
{ "isActive": "bool (required)" }
```
Data: `{ "id", "fullName", "userName", "email", "mobileNumber", "isActive" }`
### LockUserAccount
_Requires auth._ Payload:
```json
{ "isLocked": "bool (required)" }
```
Data: `{ "id", "fullName", "userName", "email", "mobileNumber", "isLocked", "deletedAt" }`
(Also invalidates all user sessions.)
### ChangeUserPassword
_Requires auth._ Payload:
```json
{ "currentPassword": "string (required)", "newPassword": "string (required)" }
```
Data: `{ "message": "Password changed successfully" }`
### LogoutUser
Payload:
```json
{ "userId": "guid (required)" }
```
Data: `{ "message": "User logged out successfully" }`
### UpdateUser
_Requires auth._ Payload (all optional, at least one required):
```json
{
"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:
```json
{
"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:
```json
{ "secretKey": "string (required)", "verificationCode": "string (required)" }
```
Data:
```json
{
"message": "2FA setup completed successfully",
"backupCodes": ["..."],
"user": { "id", "fullName", "userName", "email", "isMfaEnabled" }
}
```
### 2FA — verifyTwoFA
_Requires auth._ Payload:
```json
{ "verificationCode": "string (required)" }
```
Data: raw third-party verification result object.
### 2FA — disableTwoFA
_Requires auth._ Payload:
```json
{ "verificationCode": "string (required)" }
```
Data:
```json
{ "message": "2FA has been disabled successfully", "user": { "id", "fullName", "userName", "email", "isMfaEnabled" } }
```
### 2FA — getTwoFAStatus
_Requires auth. No payload._
Data:
```json
{ "isMfaEnabled": false, "isVerified": false, "lastUsedAt": null, "verifiedAt": null, "hasBackupCodes": false }
```
---
## /api/recovery — RecoveryManager functions
### forgotPassword
Payload:
```json
{ "identifier": "string (required)", "useResetLink": "bool? (default false)", "numberOfDigits": "int? (default 6)" }
```
Data (useResetLink = true):
```json
{ "success": true, "message": "Password reset link sent to your email.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "ResetLink" } }
```
Data (useResetLink = false, OTP flow):
```json
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP" } }
```
### verifyOTP
Payload:
```json
{ "referenceNumber": "string (required)", "otpCode": "string (required)" }
```
Data:
```json
{ "success": true, "message": "OTP verified successfully. You can now reset your password.", "data": { "referenceNumber": "", "userId": "guid", "verified": true } }
```
### resetPasswordWithToken
Payload:
```json
{ "resetToken": "string (required)", "newPassword": "string (required, min 8 chars)", "confirmPassword": "string (required, must match)" }
```
Data:
```json
{ "success": true, "message": "Password reset successful. Please login with your new password.", "data": { "userId": "guid", "email": "", "resetAt": "" } }
```
### resetPassword
Payload:
```json
{ "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:
```json
{ "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:
```json
{ "identifier": "string (required, email or mobile)", "numberOfDigits": "int? (default 4)", "newUser": "bool? (default false)" }
```
Data (newUser = true):
```json
{ "success": true, "message": "OTP sent successfully for new user.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR NEW USER" } }
```
Data (existing user):
```json
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR LOGIN" } }
```
### VerifyOTP (alt)
Payload:
```json
{
"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:
```json
{
"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
> **Null-tolerant fields (fixed 2026-07-18):** `isSystemRole` is read via `data["isSystemRole"].ValueKind != JsonValueKind.Null`
> before calling `GetBoolean()` in both `createRole` and `updateRole` — an explicit JSON `null` (as opposed to the key being
> absent) previously threw an unhandled `InvalidOperationException`, surfaced to ERPCore callers as a generic `500`/`AUTH_UPSTREAM_ERROR`.
### createRole
Payload:
```json
{ "code": "string (required)", "name": "string? (defaults to code)", "isSystemRole": "bool? (default false)" }
```
Data:
```json
{ "roleId": "guid", "code": "", "name": "", "isSystemRole": false, "createdAt": "" }
```
### getRole
Payload:
```json
{ "roleId": "guid (required)" }
```
Data: same shape as `createRole`.
### listRoles
No payload.
Data: array of `createRole`-shaped objects.
### updateRole
Payload (all fields optional except `roleId`):
```json
{ "roleId": "guid (required)", "code": "string?", "name": "string?", "isSystemRole": "bool?" }
```
Data: same shape as `createRole`.
### deleteRole
Payload:
```json
{ "roleId": "guid (required)" }
```
Blocked with a `400` (`"ROLE_IN_USE: role is assigned to one or more users"`) if any `User.RoleId` references it (FK is `Restrict`).
Data: `{ "message": "Role deleted successfully" }`
---
## Notes
- All timestamps are UTC.
- `deviceName`, `Browser`, `OS`, `IPAddress` are captured per session from request headers for session tracking (`getUserSessions`).
- Password login validation (`PasswordHasher.Verify`) is performed in `loginUser` — invalid/missing password hashes are rejected.
- JWT access tokens issued with `expiresIn: 3600` (1 hour); refresh tokens/sessions expire after 30 days.
+99
View File
@@ -0,0 +1,99 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace AuthHex
{
public class AppDBContext : DbContext
{
public AppDBContext(DbContextOptions<AppDBContext> options) : base(options) { }
public DbSet<Models.User> Users => Set<Models.User>();
public DbSet<Models.Role> Roles => Set<Models.Role>();
public DbSet<Models.UserSession> UserSessions => Set<Models.UserSession>();
public DbSet<Models.Token> Token => Set<Models.Token>();
public DbSet<Models.AuthEventLog> AuthEventLogs => Set<Models.AuthEventLog>();
public DbSet<Models.UserMfa> UserMfas => Set<Models.UserMfa>();
public DbSet<Models.UserProvider> UserProviders => Set<Models.UserProvider>();
public DbSet<Models.Recovery> Recovery => Set<Models.Recovery>();
public DbSet<Models.SysConfig> SysConfigs => Set<Models.SysConfig>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure UserSession primary key
modelBuilder.Entity<Models.UserSession>()
.HasKey(us => us.SessionId);
// Configure relationship between UserSession and User
modelBuilder.Entity<Models.UserSession>()
.HasOne(us => us.User)
.WithMany(u => u.Sessions)
.HasForeignKey(us => us.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure AuthEventLog primary key
modelBuilder.Entity<Models.AuthEventLog>()
.HasKey(ae => ae.Id);
// Configure Token primary key and relationships
modelBuilder.Entity<Models.Token>()
.HasKey(t => t.TokenId);
modelBuilder.Entity<Models.Token>()
.HasOne(t => t.User)
.WithMany()
.HasForeignKey(t => t.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure UserMfa primary key and relationships
modelBuilder.Entity<Models.UserMfa>()
.HasKey(um => um.UserMfaId);
modelBuilder.Entity<Models.UserMfa>()
.HasOne(um => um.User)
.WithOne(u => u.UserMfa)
.HasForeignKey<Models.UserMfa>(um => um.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure UserProvider primary key and relationships
modelBuilder.Entity<Models.UserProvider>()
.HasKey(up => up.ProviderId);
modelBuilder.Entity<Models.UserProvider>()
.HasOne(up => up.User)
.WithMany(u => u.Providers)
.HasForeignKey(up => up.UserId)
.OnDelete(DeleteBehavior.Cascade);
// Configure Role primary key
modelBuilder.Entity<Models.Role>()
.HasKey(r => r.RoleId);
// Configure User primary key and relationships
modelBuilder.Entity<Models.User>()
.HasKey(u => u.Id);
}
}
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<AppDBContext>
{
public AppDBContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false)
.Build();
var optionsBuilder = new DbContextOptionsBuilder<AppDBContext>();
var connectionString = config.GetConnectionString("DefaultConnection");
// Pomelo MySQL provider
optionsBuilder.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 21)));
return new AppDBContext(optionsBuilder.Options);
}
}
}
+26
View File
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.11" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="DTOs\" />
<Folder Include="Enums\" />
</ItemGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.1.11312.151 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AuthHex", "AuthHex.csproj", "{2D195EDD-1ECF-968A-CF6C-99F5FCE8CA56}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2D195EDD-1ECF-968A-CF6C-99F5FCE8CA56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D195EDD-1ECF-968A-CF6C-99F5FCE8CA56}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D195EDD-1ECF-968A-CF6C-99F5FCE8CA56}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D195EDD-1ECF-968A-CF6C-99F5FCE8CA56}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8133C788-893B-4632-842E-7FA75FFA716B}
EndGlobalSection
EndGlobal
+743
View File
@@ -0,0 +1,743 @@
{
"info": {
"name": "AuthHex Client API Collection",
"description": "Web client authentication flow - Email OTP registration and password login",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_postman_id": "auth-hex-client-api-collection",
"version": "1.0.0"
},
"variable": [
{
"key": "baseUrl",
"value": "https://localhost:7001/api",
"description": "Base URL for AuthHex API"
},
{
"key": "accessToken",
"value": "",
"description": "JWT Access Token - set automatically after login"
},
{
"key": "refreshToken",
"value": "",
"description": "JWT Refresh Token - set automatically after login"
},
{
"key": "email",
"value": "user@example.com",
"description": "Email for testing"
},
{
"key": "referenceNumber",
"value": "",
"description": "OTP Reference Number - set automatically"
}
],
"item": [
{
"name": "Health Check",
"item": [
{
"name": "API Status",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/status",
"host": ["{{baseUrl}}"],
"path": ["status"]
},
"description": "Check if API is running"
},
"response": []
}
]
},
{
"name": "Client Registration Flow",
"item": [
{
"name": "Step 1: Check Email Availability",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"isAvailable\",\n \"payload\": {\n \"Identifier\": \"{{email}}\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Check if email is available for registration"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.IsAvailable) {",
" pm.environment.set('emailAvailable', true);",
" console.log('Email is available');",
"} else {",
" console.log('Email already exists');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 2: Send Email OTP (New User)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"sendOtp\",\n \"payload\": {\n \"identifier\": \"{{email}}\",\n \"numberOfDigits\": 6,\n \"newUser\": true\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Send OTP to email for new user registration"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.Data && jsonData.data.Data.ReferenceNumber) {",
" pm.environment.set('referenceNumber', jsonData.data.Data.ReferenceNumber);",
" console.log('Email OTP Reference: ' + jsonData.data.Data.ReferenceNumber);",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 3: Verify Email OTP (New User)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"VerifyOTP\",\n \"payload\": {\n \"referenceNumber\": \"{{referenceNumber}}\",\n \"otpCode\": \"123456\",\n \"newUser\": true,\n \"identifier\": \"{{email}}\",\n \"deviceName\": \"Chrome Desktop\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Verify email OTP for new user registration. Returns verification token."
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.verificationToken) {",
" pm.environment.set('verificationToken', jsonData.data.verificationToken);",
" console.log('Email OTP Verified - Registration can proceed');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 4: Complete Registration",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"registerUser\",\n \"payload\": {\n \"fullName\": \"Alice Johnson\",\n \"userName\": \"alicejohnson\",\n \"email\": \"{{email}}\",\n \"nic\": \"199612345679\",\n \"password\": \"SecureClientPass123!\",\n \"roleId\": \"00000000-0000-0000-0000-000000000001\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"chkUser\": false\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Complete user registration with verified email"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.statusCode === 200) {",
" console.log('Client user registered successfully');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Alternative: Direct Registration",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fullName\": \"Bob Williams\",\n \"userName\": \"bobwilliams\",\n \"email\": \"bob.williams@example.com\",\n \"nic\": \"199712345678\",\n \"password\": \"SecureClientPass123!\",\n \"roleId\": \"00000000-0000-0000-0000-000000000001\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"chkUser\": true\n}"
},
"url": {
"raw": "{{baseUrl}}/registerUser",
"host": ["{{baseUrl}}"],
"path": ["registerUser"]
},
"description": "Direct registration endpoint - bypasses OTP verification"
},
"response": []
}
]
},
{
"name": "Client Login Flow",
"item": [
{
"name": "Login with Email/Username & Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"identifier\": \"{{email}}\",\n \"password\": \"SecureClientPass123!\",\n \"deviceName\": \"Chrome Desktop\"\n}"
},
"url": {
"raw": "{{baseUrl}}/loginUser",
"host": ["{{baseUrl}}"],
"path": ["loginUser"]
},
"description": "Login with email/username and password - returns JWT tokens"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.AccessToken) {",
" pm.environment.set('accessToken', jsonData.data.AccessToken);",
" pm.environment.set('refreshToken', jsonData.data.RefreshToken);",
" console.log('Login successful - Tokens saved');",
"} else if (jsonData.data && jsonData.data.RequiresTwoFA) {",
" console.log('2FA Required - Use Verify 2FA endpoint');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Alternative: Login via /user endpoint",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"loginUser\",\n \"payload\": {\n \"identifier\": \"{{email}}\",\n \"password\": \"SecureClientPass123!\",\n \"deviceName\": \"Chrome Desktop\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Alternative login using the generic /user endpoint"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.AccessToken) {",
" pm.environment.set('accessToken', jsonData.data.AccessToken);",
" pm.environment.set('refreshToken', jsonData.data.RefreshToken);",
" console.log('Login successful - Tokens saved');",
"}"
],
"type": "text/javascript"
}
}
]
}
]
},
{
"name": "Authenticated Client Operations",
"item": [
{
"name": "Get User Details",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserDetails\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get authenticated user details"
},
"response": []
},
{
"name": "Get User Sessions",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserSessions\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get all active sessions for the user"
},
"response": []
},
{
"name": "Change Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ChangeUserPassword\",\n \"payload\": {\n \"currentPassword\": \"SecureClientPass123!\",\n \"newPassword\": \"NewSecureClientPass456!\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Change user password"
},
"response": []
},
{
"name": "Change User Status",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ChangeUserStatus\",\n \"payload\": {\n \"isActive\": true\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Enable or disable user account"
},
"response": []
},
{
"name": "Logout",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"LogoutUser\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Logout user and invalidate current session"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.environment.unset('accessToken');",
"pm.environment.unset('refreshToken');",
"console.log('Logged out - Tokens cleared');"
],
"type": "text/javascript"
}
}
]
}
]
},
{
"name": "Password Recovery",
"item": [
{
"name": "Step 1: Request Password Reset (Forgot Password)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"identifier\": \"{{email}}\"\n}"
},
"url": {
"raw": "{{baseUrl}}/forgotPassword",
"host": ["{{baseUrl}}"],
"path": ["forgotPassword"]
},
"description": "Request password reset - sends OTP to registered email/mobile"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.ReferenceNumber) {",
" pm.environment.set('referenceNumber', jsonData.data.ReferenceNumber);",
" console.log('Reset OTP Reference: ' + jsonData.data.ReferenceNumber);",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 2: Verify Reset OTP",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"VerifyForgotPasswordOTP\",\n \"payload\": {\n \"referenceNumber\": \"{{referenceNumber}}\",\n \"otpCode\": \"123456\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/recovery",
"host": ["{{baseUrl}}"],
"path": ["recovery"]
},
"description": "Verify OTP for password reset"
},
"response": []
},
{
"name": "Step 3: Reset Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ResetPassword\",\n \"payload\": {\n \"referenceNumber\": \"{{referenceNumber}}\",\n \"newPassword\": \"MyNewPassword123!\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/recovery",
"host": ["{{baseUrl}}"],
"path": ["recovery"]
},
"description": "Set new password using verified OTP reference"
},
"response": []
}
]
},
{
"name": "Two-Factor Authentication (Client)",
"item": [
{
"name": "Initiate 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"initiateTwoFASetup\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get QR code and secret for Google Authenticator setup"
},
"response": []
},
{
"name": "Complete 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"completeTwoFASetup\",\n \"payload\": {\n \"verificationCode\": \"123456\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Verify and complete 2FA setup"
},
"response": []
},
{
"name": "Verify 2FA Code (During Login)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"verifyTwoFA\",\n \"payload\": {\n \"code\": \"123456\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Verify 2FA code during login for accounts with 2FA enabled"
},
"response": []
},
{
"name": "Get 2FA Status",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getTwoFAStatus\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Check if 2FA is enabled for user"
},
"response": []
},
{
"name": "Disable 2FA",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"disableTwoFA\",\n \"payload\": {\n \"password\": \"SecureClientPass123!\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Disable 2FA for user account (requires password confirmation)"
},
"response": []
}
]
},
{
"name": "Utility Functions",
"item": [
{
"name": "Check Identifier Availability",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"isAvailable\",\n \"payload\": {\n \"Identifier\": \"testuser@example.com\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Check if email, username, NIC, or mobile number is already registered"
},
"response": []
}
]
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{accessToken}}",
"type": "string"
}
]
}
}
+596
View File
@@ -0,0 +1,596 @@
{
"info": {
"name": "AuthHex Mobile API Collection",
"description": "Mobile authentication flow - OTP-based registration and login",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_postman_id": "auth-hex-mobile-api-collection",
"version": "1.0.0"
},
"variable": [
{
"key": "baseUrl",
"value": "https://localhost:7001/api",
"description": "Base URL for AuthHex API"
},
{
"key": "accessToken",
"value": "",
"description": "JWT Access Token - set automatically after login"
},
{
"key": "refreshToken",
"value": "",
"description": "JWT Refresh Token - set automatically after login"
},
{
"key": "mobileNumber",
"value": "+94771234567",
"description": "Mobile number for testing"
},
{
"key": "referenceNumber",
"value": "",
"description": "OTP Reference Number - set automatically"
}
],
"item": [
{
"name": "Health Check",
"item": [
{
"name": "API Status",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/status",
"host": ["{{baseUrl}}"],
"path": ["status"]
},
"description": "Check if API is running"
},
"response": []
}
]
},
{
"name": "Mobile Registration Flow",
"item": [
{
"name": "Step 1: Check Mobile Availability",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"isAvailable\",\n \"payload\": {\n \"Identifier\": \"{{mobileNumber}}\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Check if mobile number is available for registration"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.IsAvailable) {",
" pm.environment.set('mobileAvailable', true);",
" console.log('Mobile number is available');",
"} else {",
" console.log('Mobile number already exists');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 2: Send Mobile OTP (New User)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"sendOtp\",\n \"payload\": {\n \"identifier\": \"{{mobileNumber}}\",\n \"numberOfDigits\": 6,\n \"newUser\": true\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Send OTP to mobile number for new user registration"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.Data && jsonData.data.Data.ReferenceNumber) {",
" pm.environment.set('referenceNumber', jsonData.data.Data.ReferenceNumber);",
" console.log('Reference Number: ' + jsonData.data.Data.ReferenceNumber);",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 3: Verify OTP (New User)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"VerifyOTP\",\n \"payload\": {\n \"referenceNumber\": \"{{referenceNumber}}\",\n \"otpCode\": \"123456\",\n \"newUser\": true,\n \"identifier\": \"{{mobileNumber}}\",\n \"deviceName\": \"iPhone 14 Pro\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Verify OTP for new user registration. Returns verification token."
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.verificationToken) {",
" pm.environment.set('verificationToken', jsonData.data.verificationToken);",
" console.log('OTP Verified - Registration can proceed');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 4: Complete Registration",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"registerUser\",\n \"payload\": {\n \"fullName\": \"John Doe\",\n \"userName\": \"johndoe_mobile\",\n \"mobileNumber\": \"{{mobileNumber}}\",\n \"nic\": \"199512345678\",\n \"password\": \"SecurePass123!\",\n \"roleId\": \"00000000-0000-0000-0000-000000000001\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"chkUser\": false\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Complete user registration with verified mobile number"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.statusCode === 200) {",
" console.log('User registered successfully');",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Alternative: Direct Registration",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"fullName\": \"Jane Smith\",\n \"userName\": \"janesmith_mobile\",\n \"mobileNumber\": \"+94779876543\",\n \"nic\": \"199612345678\",\n \"password\": \"SecurePass123!\",\n \"roleId\": \"00000000-0000-0000-0000-000000000001\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"chkUser\": true\n}"
},
"url": {
"raw": "{{baseUrl}}/registerUser",
"host": ["{{baseUrl}}"],
"path": ["registerUser"]
},
"description": "Direct registration endpoint - bypasses OTP verification"
},
"response": []
}
]
},
{
"name": "Mobile Login Flow",
"item": [
{
"name": "Step 1: Send Login OTP to Mobile",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"sendOtp\",\n \"payload\": {\n \"identifier\": \"{{mobileNumber}}\",\n \"numberOfDigits\": 6,\n \"newUser\": false\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Send OTP to mobile number for existing user login"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.Data && jsonData.data.Data.ReferenceNumber) {",
" pm.environment.set('referenceNumber', jsonData.data.Data.ReferenceNumber);",
" console.log('Login OTP Reference: ' + jsonData.data.Data.ReferenceNumber);",
"}"
],
"type": "text/javascript"
}
}
]
},
{
"name": "Step 2: Verify OTP & Login",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"VerifyOTP\",\n \"payload\": {\n \"referenceNumber\": \"{{referenceNumber}}\",\n \"otpCode\": \"123456\",\n \"newUser\": false,\n \"identifier\": \"{{mobileNumber}}\",\n \"deviceName\": \"iPhone 14 Pro\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/alt",
"host": ["{{baseUrl}}"],
"path": ["alt"]
},
"description": "Verify OTP and complete login - returns JWT tokens"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"var jsonData = pm.response.json();",
"if (jsonData.data && jsonData.data.AccessToken) {",
" pm.environment.set('accessToken', jsonData.data.AccessToken);",
" pm.environment.set('refreshToken', jsonData.data.RefreshToken);",
" console.log('Login successful - Tokens saved');",
"}"
],
"type": "text/javascript"
}
}
]
}
]
},
{
"name": "Authenticated Mobile Operations",
"item": [
{
"name": "Get User Details",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserDetails\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get authenticated user details"
},
"response": []
},
{
"name": "Get User Sessions",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserSessions\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get all active sessions for the user"
},
"response": []
},
{
"name": "Change Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ChangeUserPassword\",\n \"payload\": {\n \"currentPassword\": \"SecurePass123!\",\n \"newPassword\": \"NewSecurePass456!\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Change user password"
},
"response": []
},
{
"name": "Logout",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"LogoutUser\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Logout user and invalidate current session"
},
"response": [],
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.environment.unset('accessToken');",
"pm.environment.unset('refreshToken');",
"console.log('Logged out - Tokens cleared');"
],
"type": "text/javascript"
}
}
]
}
]
},
{
"name": "Two-Factor Authentication (Mobile)",
"item": [
{
"name": "Initiate 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"initiateTwoFASetup\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get QR code and secret for Google Authenticator setup"
},
"response": []
},
{
"name": "Complete 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"completeTwoFASetup\",\n \"payload\": {\n \"verificationCode\": \"123456\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Verify and complete 2FA setup"
},
"response": []
},
{
"name": "Verify 2FA Code",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"verifyTwoFA\",\n \"payload\": {\n \"code\": \"123456\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Verify 2FA code during login"
},
"response": []
},
{
"name": "Get 2FA Status",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getTwoFAStatus\",\n \"payload\": {}\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Check if 2FA is enabled for user"
},
"response": []
},
{
"name": "Disable 2FA",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"disableTwoFA\",\n \"payload\": {\n \"password\": \"SecurePass123!\"\n }\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Disable 2FA for user account"
},
"response": []
}
]
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{accessToken}}",
"type": "string"
}
]
}
}
+600
View File
@@ -0,0 +1,600 @@
{
"info": {
"name": "AuthHex API Collection",
"description": "Complete API collection for AuthHex authentication system",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
"_postman_id": "auth-hex-api-collection",
"version": "1.0.0"
},
"variable": [
{
"key": "baseUrl",
"value": "https://localhost:7001/api",
"description": "Base URL for AuthHex API"
},
{
"key": "accessToken",
"value": "",
"description": "JWT Access Token - will be set automatically after login"
},
{
"key": "refreshToken",
"value": "",
"description": "JWT Refresh Token - will be set automatically after login"
}
],
"item": [
{
"name": "Health & Status",
"item": [
{
"name": "API Status Check",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{baseUrl}}/status",
"host": ["{{baseUrl}}"],
"path": ["status"]
},
"description": "Check if API is running and get current timestamp"
},
"response": []
}
]
},
{
"name": "Authentication",
"item": [
{
"name": "Register User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"registerUser\",\n \"payload\": {\n \"fullName\": \"John Doe\",\n \"userName\": \"johndoe\",\n \"email\": \"john.doe@example.com\",\n \"mobileNumber\": \"+1234567890\",\n \"nic\": \"123456789V\",\n \"password\": \"Password123!\",\n \"roleId\": \"00000000-0000-0000-0000-000000000001\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"chkUser\": true\n },\n \"reference\": \"REG001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/registerUser",
"host": ["{{baseUrl}}"],
"path": ["registerUser"]
},
"description": "Register a new user account"
},
"response": []
},
{
"name": "Login User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"loginUser\",\n \"payload\": {\n \"identifier\": \"john.doe@example.com\",\n \"password\": \"Password123!\",\n \"userTypeId\": \"00000000-0000-0000-0000-000000000001\",\n \"deviceName\": \"Postman Client\"\n },\n \"reference\": \"LOG001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/loginUser",
"host": ["{{baseUrl}}"],
"path": ["loginUser"]
},
"description": "Authenticate user and get access token"
},
"event": [
{
"listen": "test",
"script": {
"exec": [
"if (pm.response.code === 200) {",
" const response = pm.response.json();",
" if (response.data && response.data.AccessToken) {",
" pm.collectionVariables.set('accessToken', response.data.AccessToken);",
" pm.collectionVariables.set('refreshToken', response.data.RefreshToken);",
" console.log('Access token saved to collection variables');",
" }",
"}"
]
}
}
],
"response": []
}
]
},
{
"name": "User Management",
"item": [
{
"name": "Get User Details",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserDetails\",\n \"payload\": {},\n \"reference\": \"GETUSR001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get current user profile details"
},
"response": []
},
{
"name": "Get User Sessions",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getUserSessions\",\n \"payload\": {},\n \"reference\": \"GETSESS001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Get all active sessions for current user"
},
"response": []
},
{
"name": "Change User Status",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ChangeUserStatus\",\n \"payload\": {\n \"userId\": \"00000000-0000-0000-0000-000000000001\",\n \"isActive\": true\n },\n \"reference\": \"CHGSTAT001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Activate or deactivate a user account"
},
"response": []
},
{
"name": "Lock User Account",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"LockUserAccount\",\n \"payload\": {\n \"userId\": \"00000000-0000-0000-0000-000000000001\",\n \"isLocked\": true\n },\n \"reference\": \"LOCKUSR001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Lock or unlock a user account"
},
"response": []
},
{
"name": "Change User Password",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"ChangeUserPassword\",\n \"payload\": {\n \"currentPassword\": \"OldPassword123!\",\n \"newPassword\": \"NewPassword123!\"\n },\n \"reference\": \"CHGPASS001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Change current user password"
},
"response": []
},
{
"name": "Logout User",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"LogoutUser\",\n \"payload\": {},\n \"reference\": \"LOGOUT001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Logout current user and invalidate session"
},
"response": []
}
]
},
{
"name": "Two-Factor Authentication (2FA)",
"item": [
{
"name": "Get 2FA Status",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"getTwoFAStatus\",\n \"payload\": {},\n \"reference\": \"2FASTAT001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Check if 2FA is enabled for current user"
},
"response": []
},
{
"name": "Initiate 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"initiateTwoFASetup\",\n \"payload\": {},\n \"reference\": \"2FAINIT001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Generate QR code and secret for 2FA setup"
},
"response": []
},
{
"name": "Complete 2FA Setup",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"completeTwoFASetup\",\n \"payload\": {\n \"verificationCode\": \"123456\"\n },\n \"reference\": \"2FACOMP001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Complete 2FA setup by verifying the code"
},
"response": []
},
{
"name": "Verify 2FA Code",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"verifyTwoFA\",\n \"payload\": {\n \"verificationCode\": \"123456\"\n },\n \"reference\": \"2FAVRF001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Verify 2FA code during login or operations"
},
"response": []
},
{
"name": "Disable 2FA",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Authorization",
"value": "Bearer {{accessToken}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"disableTwoFA\",\n \"payload\": {\n \"verificationCode\": \"123456\"\n },\n \"reference\": \"2FADIS001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/user",
"host": ["{{baseUrl}}"],
"path": ["user"]
},
"description": "Disable 2FA for current user"
},
"response": []
}
]
},
{
"name": "Password Recovery",
"item": [
{
"name": "Forgot Password (OTP)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"forgotPassword\",\n \"payload\": {\n \"identifier\": \"john.doe@example.com\",\n \"useResetLink\": false,\n \"numberOfDigits\": 6\n },\n \"reference\": \"FORGPWD001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/forgotPassword",
"host": ["{{baseUrl}}"],
"path": ["forgotPassword"]
},
"description": "Send OTP for password recovery via SMS/Email"
},
"response": []
},
{
"name": "Forgot Password (Reset Link)",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"forgotPassword\",\n \"payload\": {\n \"identifier\": \"john.doe@example.com\",\n \"useResetLink\": true\n },\n \"reference\": \"FORGPWD002\"\n}"
},
"url": {
"raw": "{{baseUrl}}/forgotPassword",
"host": ["{{baseUrl}}"],
"path": ["forgotPassword"]
},
"description": "Send password reset link via Email"
},
"response": []
},
{
"name": "Verify OTP",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"verifyOTP\",\n \"payload\": {\n \"referenceNumber\": \"REF123456789\",\n \"otpCode\": \"123456\"\n },\n \"reference\": \"VEROTP001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/recovery",
"host": ["{{baseUrl}}"],
"path": ["recovery"]
},
"description": "Verify OTP code for password recovery"
},
"response": []
},
{
"name": "Reset Password with Token",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"resetPasswordWithToken\",\n \"payload\": {\n \"resetToken\": \"your-reset-token-here\",\n \"newPassword\": \"NewPassword123!\"\n },\n \"reference\": \"RESETPWD001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/recovery",
"host": ["{{baseUrl}}"],
"path": ["recovery"]
},
"description": "Reset password using reset token from email link"
},
"response": []
},
{
"name": "Reset Password with OTP",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"resetPassword\",\n \"payload\": {\n \"referenceNumber\": \"REF123456789\",\n \"newPassword\": \"NewPassword123!\"\n },\n \"reference\": \"RESETPWD002\"\n}"
},
"url": {
"raw": "{{baseUrl}}/recovery",
"host": ["{{baseUrl}}"],
"path": ["recovery"]
},
"description": "Reset password after OTP verification"
},
"response": []
}
]
},
{
"name": "Utility Functions",
"item": [
{
"name": "Check Availability",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"functionName\": \"isAvailable\",\n \"payload\": {\n \"resetToken\": \"john.doe@example.com\"\n },\n \"reference\": \"CHKAVAIL001\"\n}"
},
"url": {
"raw": "{{baseUrl}}/isAvailable",
"host": ["{{baseUrl}}"],
"path": ["isAvailable"]
},
"description": "Check if email/username/mobile is available for registration"
},
"response": []
}
]
}
],
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{accessToken}}",
"type": "string"
}
]
},
"event": [
{
"listen": "prerequest",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
},
{
"listen": "test",
"script": {
"type": "text/javascript",
"exec": [
""
]
}
}
]
}
+88
View File
@@ -0,0 +1,88 @@
using AuthHex.Models.IOs;
using AuthHex.Services.AltOptionManager;
using AuthHex.Services.FunctionHandler;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
namespace AuthHex.Controllers
{
[ApiController]
[Route("api")]
[EnableCors("AllowAll")]
public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager, RoleManager roleManager) : ControllerBase
{
private readonly UserManager _userManager = userManager;
private readonly RecoveryManager _recoveryManager = recoveryManager;
private readonly AltOptionManager _altOptionManager = altOptionManager;
private readonly RoleManager _roleManager = roleManager;
[HttpGet("status")]
[AllowAnonymous]
public IActionResult GetStatus()
{
return Ok(new { status = "API is running", timestamp = System.DateTime.UtcNow });
}
[HttpPost("user")]
[AllowAnonymous]
public async Task<IActionResult> UserExecute([FromBody] ApiRequest request)
{
var response = await _userManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("recovery")]
[AllowAnonymous]
public async Task<IActionResult> RecoveryExecute([FromBody] ApiRequest request)
{
var response = await _recoveryManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("registerUser")]
[AllowAnonymous]
public async Task<IActionResult> RegisterUser([FromBody] ApiRequest request)
{
request.FunctionName = "registerUser";
var response = await _userManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("loginUser")]
[AllowAnonymous]
public async Task<IActionResult> LoginUser([FromBody] ApiRequest request)
{
request.FunctionName = "loginUser";
var response = await _userManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("forgotPassword")]
[AllowAnonymous]
public async Task<IActionResult> ForgotPassword([FromBody] ApiRequest request)
{
request.FunctionName = "forgotPassword";
var response = await _recoveryManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("alt")]
[AllowAnonymous]
public async Task<IActionResult> IsAvailable([FromBody] ApiRequest request)
{
var response = await _altOptionManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
[HttpPost("role")]
[AllowAnonymous]
public async Task<IActionResult> RoleExecute([FromBody] ApiRequest request)
{
var response = await _roleManager.Execute(request, HttpContext);
return StatusCode(response.StatusCode, response);
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace AuthHex.DTOs
{
public class OtpResponse
{
public int? StatusCode { get; set; }
public string? Message { get; set; }
public string? ApiResponse { get; set; }
public string? Error { get; set; }
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace AuthHex.DTOs
{
public class SmsRequest
{
public string? api_key { get; set; }
public string? phone_number { get; set; }
public string? msg_text { get; set; }
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace AuthHex.DTOs.Tute
{
public class UpdateTuteItemRequest
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public string? Notes { get; set; }
}
}
+22
View File
@@ -0,0 +1,22 @@
namespace Core_Archi.Exceptions
{
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class BadRequestException : Exception
{
public BadRequestException(string message) : base(message) { }
}
public class UnauthorizedException : Exception
{
public UnauthorizedException(string message) : base(message) { }
}
public class ForbiddenException : Exception
{
public ForbiddenException(string message) : base(message) { }
}
}
+32
View File
@@ -0,0 +1,32 @@
using System.Security.Cryptography;
namespace AuthHex;
/// <summary>
/// Utility class to generate RSA key pairs for JWT authentication.
/// Run this once to generate keys and copy them to appsettings.json
/// </summary>
public class RsaKeyGenerator
{
public static void GenerateAndPrintKeys()
{
using var rsa = RSA.Create(2048);
var privateKey = rsa.ToXmlString(true); // includes private key
var publicKey = rsa.ToXmlString(false); // public key only
Console.WriteLine("=== RSA Private Key (Keep this SECRET!) ===");
Console.WriteLine(privateKey);
Console.WriteLine();
Console.WriteLine("=== RSA Public Key ===");
Console.WriteLine(publicKey);
Console.WriteLine();
Console.WriteLine("Copy the Private Key to appsettings.json -> Jwt:RsaPrivateKey");
Console.WriteLine("Copy the Public Key to appsettings.json -> Jwt:RsaPublicKey");
Console.WriteLine();
Console.WriteLine("IMPORTANT: Never commit the private key to source control!");
Console.WriteLine("Consider using Azure Key Vault or environment variables for production.");
}
}
+195
View File
@@ -0,0 +1,195 @@
using System.Text.Json;
using Core_Archi.Exceptions;
namespace Core_Archi.Helpers
{
public class DataExtractor
{
public static string GetRequiredString(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind != JsonValueKind.String)
return "";
//throw new BadRequestException($"Field '{key}' must be a string.");
var value = element.GetString();
if (string.IsNullOrWhiteSpace(value))
throw new BadRequestException($"Field '{key}' cannot be empty.");
return value;
}
// ===================== INT =====================
public static int GetRequiredInt(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.TryGetInt32(out var value))
return value;
throw new BadRequestException($"Field '{key}' must be a valid integer.");
}
// ===================== FLOAT =====================
public static float GetRequiredFloat(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.TryGetSingle(out var value))
return value;
// Allow numeric strings: "12.5"
if (element.ValueKind == JsonValueKind.String &&
float.TryParse(element.GetString(), out value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid float.");
}
// ===================== DECIMAL =====================
public static decimal GetRequiredDecimal(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new ArgumentException($"Missing required field '{key}'.");
if (element.TryGetDecimal(out var value))
return value;
// Allow numeric strings: "12.5"
if (element.ValueKind == JsonValueKind.String &&
decimal.TryParse(element.GetString(), out value))
{
return value;
}
throw new ArgumentException($"Field '{key}' must be a valid decimal.");
}
// ===================== BOOLEAN =====================
public static bool GetRequiredBool(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.True) return true;
if (element.ValueKind == JsonValueKind.False) return false;
// Allow string values: "true" / "false"
if (element.ValueKind == JsonValueKind.String &&
bool.TryParse(element.GetString(), out var value))
{
return value;
}
throw new BadRequestException($"Field '{key}' must be a valid boolean.");
}
// ===================== GUID =====================
public static Guid GetRequiredGuid(
Dictionary<string, JsonElement> data,
string key)
{
var value = GetRequiredString(data, key);
if (!Guid.TryParse(value, out var guid))
throw new BadRequestException($"Field '{key}' must be a valid GUID.");
return guid;
}
// ===================== DATETIME =====================
public static DateTime GetRequiredDateTime(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.Null)
throw new BadRequestException($"Field '{key}' cannot be null.");
// ISO 8601 string
if (element.ValueKind == JsonValueKind.String &&
DateTime.TryParse(element.GetString(), out var date))
{
return date;
}
// Native JSON DateTime
if (element.TryGetDateTime(out date))
{
return date;
}
throw new BadRequestException($"Field '{key}' must be a valid DateTime.");
}
// ===================== DATEONLY =====================
public static DateOnly GetRequiredDateOnly(
Dictionary<string, JsonElement> data,
string key)
{
if (!data.TryGetValue(key, out var element))
throw new BadRequestException($"Missing required field '{key}'.");
if (element.ValueKind == JsonValueKind.Null)
throw new BadRequestException($"Field '{key}' cannot be null.");
DateTime date;
// ISO 8601 string (e.g., "2025-02-25")
if (element.ValueKind == JsonValueKind.String &&
DateTime.TryParse(element.GetString(), out date))
{
return DateOnly.FromDateTime(date);
}
if (element.TryGetDateTime(out date))
{
return DateOnly.FromDateTime(date);
}
throw new BadRequestException($"Field '{key}' must be a valid date.");
}
// ===================== USER ID FROM HttpContext =====================
public static Guid GetUserIdFromContext(HttpContext httpContext)
{
if (httpContext == null) throw new ArgumentNullException(nameof(httpContext));
var userIdFromContext = httpContext.Items["UserId"]?.ToString()
?? httpContext.User?.FindFirst("UserId")?.Value;
if (string.IsNullOrWhiteSpace(userIdFromContext) || !Guid.TryParse(userIdFromContext, out var userId))
{
throw new UnauthorizedAccessException("User is not authenticated. Please login and send cookie.");
}
return userId;
}
}
}
+15
View File
@@ -0,0 +1,15 @@
using System.Text.Json;
namespace Core_Archi.Helpers
{
public static class DeserializePayload
{
public static Dictionary<string, JsonElement> Deserialize(object payload)
{
ArgumentNullException.ThrowIfNull(payload);
var json = JsonSerializer.Serialize(payload);
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json)
?? throw new InvalidOperationException("Failed to deserialize payload.");
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Http;
namespace Core_Archi.Helpers;
public class UserHelper
{
public string? ExtractBrowser(string? userAgent)
{
if (string.IsNullOrEmpty(userAgent)) return null;
if (userAgent.Contains("Chrome")) return "Chrome";
if (userAgent.Contains("Firefox")) return "Firefox";
if (userAgent.Contains("Safari") && !userAgent.Contains("Chrome")) return "Safari";
if (userAgent.Contains("Edge")) return "Edge";
if (userAgent.Contains("Opera")) return "Opera";
return "Other";
}
public string? ExtractOS(string? userAgent)
{
if (string.IsNullOrEmpty(userAgent)) return null;
if (userAgent.Contains("Windows")) return "Windows";
if (userAgent.Contains("Mac OS")) return "macOS";
if (userAgent.Contains("Linux")) return "Linux";
if (userAgent.Contains("Android")) return "Android";
if (userAgent.Contains("iOS") || userAgent.Contains("iPhone") || userAgent.Contains("iPad")) return "iOS";
return "Other";
}
public Guid GetUserIdFromClaims(HttpContext httpContext)
{
var userIdClaim = httpContext.User.FindFirst("userId")
?? httpContext.User.FindFirst("UserId")
?? httpContext.User.FindFirst("sub");
if (userIdClaim == null || !Guid.TryParse(userIdClaim.Value, out var userId))
throw new ArgumentException("User ID not found in token");
return userId;
}
}
+49
View File
@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Storage;
namespace AuthHex.Infra.UoW
{
public class EFUnitOfWork : IUnitOfWork
{
private readonly AppDBContext _dbContext;
private IDbContextTransaction? _transaction;
public EFUnitOfWork(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task BeginAsync(CancellationToken ct = default)
{
if (_transaction == null)
_transaction = await _dbContext.Database.BeginTransactionAsync(ct);
}
public async Task SaveChangesAsync(CancellationToken ct = default)
{
await _dbContext.SaveChangesAsync(ct);
}
public async Task CommitAsync(CancellationToken ct = default)
{
await _dbContext.SaveChangesAsync(ct);
if (_transaction != null)
{
await _transaction.CommitAsync(ct);
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackAsync(CancellationToken ct = default)
{
if (_transaction != null)
{
await _transaction.RollbackAsync(ct);
await _transaction.DisposeAsync();
_transaction = null;
}
}
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace AuthHex.Infra.UoW
{
public interface IUnitOfWork
{
Task BeginAsync(CancellationToken ct = default);
Task CommitAsync(CancellationToken ct = default);
Task RollbackAsync(CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}
}
+10
View File
@@ -0,0 +1,10 @@
using AuthHex.Models;
namespace AuthHex.Interfaces
{
public interface IAltOptionManagerRepository
{
}
}
@@ -0,0 +1,13 @@
using AuthHex.Models;
namespace AuthHex.Interfaces
{
public interface IRecoveryManagerRepository
{
Task<Recovery> AddRecoveryAsync(Recovery recovery, CancellationToken ct = default);
Task<Recovery?> GetRecoveryByTokenHashAsync(string tokenHash, CancellationToken ct = default);
Task<Recovery?> GetRecoveryByReferenceNumAsync(string referenceNum, CancellationToken ct = default);
Task UpdateRecoveryAsync(Recovery recovery, CancellationToken ct = default);
Task<List<Recovery>> GetPendingRecoveriesByUserIdAsync(Guid userId, CancellationToken ct = default);
}
}
+13
View File
@@ -0,0 +1,13 @@
using AuthHex.Models;
namespace AuthHex.Interfaces
{
public interface IRoleManageRepository
{
Task<Role> AddRoleAsync(Role role, CancellationToken ct = default);
Task<Role?> GetRoleByIdAsync(Guid roleId, CancellationToken ct = default);
Task<List<Role>> ListRolesAsync(CancellationToken ct = default);
//Task<bool> RoleInUseAsync(Guid roleId, CancellationToken ct = default);
Task DeleteRoleAsync(Role role, CancellationToken ct = default);
}
}
+19
View File
@@ -0,0 +1,19 @@
using AuthHex.Models;
namespace AuthHex.Interfaces
{
public interface IUserManageRepository
{
Task<User> AddUserAsync(User user, CancellationToken ct = default);
Task<Token> AddTokenAsync(Token token, CancellationToken ct = default);
Task<List<User>> GetUserByIdentifiers(string? email, string? mobileNumber, string? nic, string? username, string? Optional1);
Task<User?> GetUserByIdentifierAndType(string identifier);
Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default);
Task UpdateUserAsync(User user, CancellationToken ct = default);
Task<List<UserSession>> GetUserSessionsAsync(Guid userId, CancellationToken ct = default);
Task<UserSession?> GetActiveSessionByRefreshTokenAsync(string refreshToken, CancellationToken ct = default);
Task InvalidateUserSessionsAsync(Guid userId, CancellationToken ct = default);
//Task <bool> GetExUser (string identifier);
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace AuthHex.Models;
public class AuthEventLog
{
public Guid Id { get; set; } = Guid.NewGuid();
public Guid? UserId { get; set; }
public string? EventType { get; set; } // LOGIN_SUCCESS, LOGIN_FAILED, LOCKED
public string? IPAddress { get; set; }
public string? UserAgent { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
+17
View File
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace AuthHex.Models.IOs
{
[NotMapped]
public class ApiRequest
{
[Required]
public string FunctionName { get; set; } = string.Empty;
public object Payload { get; set; } = new();
[Required(AllowEmptyStrings = true)]
public string? Reference { get; set; }
}
}
+14
View File
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.ComponentModel.DataAnnotations;
namespace AuthHex.Models.IOs
{
[NotMapped]
public class ApiResponse
{
public int StatusCode { get; set; } = 200;
public bool Success { get; set; } = true;
public string? Message { get; set; }
public object? Data { get; set; }
}
}
+32
View File
@@ -0,0 +1,32 @@
namespace AuthHex.Models
{
public class Recovery
{
public Guid Id { get; set; }
public Guid? UserId { get; set; }
public User? User { get; set; }
public string? RecoveryReferenceNum { get; set; }
// For OTP-based recovery
public string? OTP { get; set; }
public string? OTPReferenceNum { get; set; }
// For Token-based reset (SECURE - hashed)
public string? ResetTokenHash { get; set; }
public string? ResetToken { get; set; } // Plain token (only for email, not saved)
public string Status { get; set; } = "Pending"; // Pending, Used, Expired
public bool IsUsed { get; set; } = false; // Prevent token reuse
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime ExpirationTime { get; set; } = DateTime.UtcNow.AddMinutes(15);
// Recovery type: OTP or ResetLink
public string RecoveryType { get; set; } = "OTP"; // OTP, ResetLink
}
}
+15
View File
@@ -0,0 +1,15 @@
namespace AuthHex.Models
{
public class Role
{
public Guid RoleId { get; set; }
public string Code { get; set; } = string.Empty; // ADMIN, USER, STAFF
public string?Name { get; set; } = string.Empty;
public bool? IsSystemRole { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
+17
View File
@@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace AuthHex.Models
{
public class SysConfig
{
[Key]
public int SysConfigId { get; set; }
public int TenantId { get; set; }
public string Configuration { get; set; }
public string Value { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime LastUpdatedAt { get; set; } = DateTime.UtcNow;
}
}
+22
View File
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace AuthHex.Models
{
public class Token
{
public Guid TokenId { get; set; }
public Guid UserId { get; set; }
public User? User { get; set; }
public string? TokenHash { get; set; }
public string? Type { get; set; } // email verify, password reset, etc.
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UsedAt { get; set; }
}
}
+38
View File
@@ -0,0 +1,38 @@
using System.Data;
namespace AuthHex.Models
{
public class User
{
public Guid Id { get; set; } = Guid.NewGuid();
public string? FullName { get; set; }
public string? UserName { get; set; }
public string? Nic { get; set; }
public string? Address { get; set; }
public string? Optional1 { get; set; } //in gym case use in secondary contact number
public bool? Optional1Verified { get; set; } = false;
public string? Optional2 { get; set; }
public string? PasswordHash { get; set; }
public string? Email { get; set; }
public bool? EmailVerified { get; set; } = false;
public string? MobileNumber { get; set; }
public bool? MobileNumberVerified { get; set; } = false;
public bool? IsActive { get; set; } = true;
public bool? IsLocked { get; set; } = false;
public DateTime? DeletedAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
//2FA
public bool IsMfaEnabled { get; set; }
public UserMfa? UserMfa { get; set; }
//User Role and Type
public string? RoleCode { get; set; }
public ICollection<UserSession>? Sessions { get; set; }
public ICollection<UserProvider>? Providers { get; set; }
}
}
+19
View File
@@ -0,0 +1,19 @@
namespace AuthHex.Models;
public class UserMfa
{
public Guid UserMfaId { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = null!;
public string? SecretKey { get; set; } // encrypted
public bool? IsEnabled { get; set; }
public bool? IsVerified { get; set; } = false;
public string? BackupCodes { get; set; } // JSON array of backup codes
public DateTime? LastUsedAt { get; set; }
public DateTime? VerifiedAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace AuthHex.Models;
public class UserProvider
{
public Guid ProviderId { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = null!;
public string? Provider { get; set; } // Google, facebook, etc.
public string? ProviderUserId { get; set; }
public DateTime LinkedAt { get; set; } = DateTime.UtcNow;
}
+25
View File
@@ -0,0 +1,25 @@
namespace AuthHex.Models
{
public class UserSession
{
public Guid SessionId { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = null!;
public string RefreshTokenHash { get; set; } = string.Empty;
public string? DeviceName { get; set; }
public string? OS { get; set; }
public string? Browser { get; set; }
public string? IPAddress { get; set; }
public DateTime ExpiresAt { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? RevokedAt { get; set; }
public bool IsActive => RevokedAt == null && ExpiresAt > DateTime.UtcNow;
}
}
+129
View File
@@ -0,0 +1,129 @@
using Core_Archi.Helpers;
using AuthHex;
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Repos;
using AuthHex.Services.UserManager;
using AuthHex.Services.RecoveryManager;
using AuthHex.Utility;
using AuthSystem.API.Utils;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<AppDBContext>(options =>
{
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
options.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 21)));
});
// Configure RSA Key Provider
var privateKeyXml = builder.Configuration["Jwt:RsaPrivateKey"]!;
var rsaKeyProvider = new RsaKeyProvider(privateKeyXml);
builder.Services.AddSingleton(rsaKeyProvider);
// Add JWT Authentication
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = rsaKeyProvider.GetPublicKey(),
ClockSkew = TimeSpan.Zero
};
});
builder.Services.AddAuthorization();
builder.Services.AddScoped<IUnitOfWork, EFUnitOfWork>();
builder.Services.AddScoped<IUserManageRepository, UserManageRepository>();
builder.Services.AddScoped<IRecoveryManagerRepository, RecoveryManagerRepository>();
builder.Services.AddScoped<IAltOptionManagerRepository, AltOptionManagerRepository>();
builder.Services.AddScoped<IRoleManageRepository, RoleManageRepository>();
builder.Services.AddScoped<UserHelper>();
builder.Services.AddScoped<UserManagerService>();
builder.Services.AddScoped<RecoveryManagerService>();
builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>();
builder.Services.AddScoped<AuthHex.Services.RoleManager.RoleManagerService>();
builder.Services.AddHttpClient<ThirdPartyService>();
builder.Services.AddScoped<JwtTokenHelper>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.UserManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RecoveryManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.AltOptionManager>();
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RoleManager>();
var configuredOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
var allowedOrigins = configuredOrigins
.Where(origin => !string.IsNullOrWhiteSpace(origin) && origin != "*")
.ToList();
var frontendUrl = builder.Configuration["AppSettings:FrontendUrl"];
if (!string.IsNullOrWhiteSpace(frontendUrl) &&
!allowedOrigins.Contains(frontendUrl, StringComparer.OrdinalIgnoreCase))
{
allowedOrigins.Add(frontendUrl);
}
// Add CORS policy
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAll", policy =>
{
if (allowedOrigins.Count > 0)
{
policy.WithOrigins(allowedOrigins.ToArray())
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
return;
}
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
if (!app.Environment.IsDevelopment())
{
app.UseHttpsRedirection();
}
app.UseRouting();
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
+41
View File
@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:47780",
"sslPort": 44395
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5044",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7152;http://localhost:5044",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
View File
+13
View File
@@ -0,0 +1,13 @@
using AuthHex.Models;
using AuthHex.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace AuthHex.Repos
{
public class AltOptionManagerRepository : IAltOptionManagerRepository
{
}
}
+50
View File
@@ -0,0 +1,50 @@
using AuthHex.Models;
using AuthHex.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace AuthHex.Repos
{
public class RecoveryManagerRepository : IRecoveryManagerRepository
{
private readonly AppDBContext _dbContext;
public RecoveryManagerRepository(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public async Task<Recovery> AddRecoveryAsync(Recovery recovery, CancellationToken ct = default)
{
_dbContext.Recovery.Add(recovery);
await _dbContext.SaveChangesAsync(ct);
return recovery;
}
public async Task<Recovery?> GetRecoveryByTokenHashAsync(string tokenHash, CancellationToken ct = default)
{
return await _dbContext.Recovery
.Include(r => r.User)
.FirstOrDefaultAsync(r => r.ResetTokenHash == tokenHash && !r.IsUsed, ct);
}
public async Task<Recovery?> GetRecoveryByReferenceNumAsync(string referenceNum, CancellationToken ct = default)
{
return await _dbContext.Recovery
.Include(r => r.User)
.FirstOrDefaultAsync(r => r.RecoveryReferenceNum == referenceNum, ct);
}
public async Task UpdateRecoveryAsync(Recovery recovery, CancellationToken ct = default)
{
_dbContext.Recovery.Update(recovery);
await _dbContext.SaveChangesAsync(ct);
}
public async Task<List<Recovery>> GetPendingRecoveriesByUserIdAsync(Guid userId, CancellationToken ct = default)
{
return await _dbContext.Recovery
.Where(r => r.UserId == userId && !r.IsUsed && r.Status == "Pending")
.ToListAsync(ct);
}
}
}
+43
View File
@@ -0,0 +1,43 @@
using AuthHex.Interfaces;
using AuthHex.Models;
using Microsoft.EntityFrameworkCore;
namespace AuthHex.Repos
{
public class RoleManageRepository : IRoleManageRepository
{
private readonly AppDBContext _dbContext;
public RoleManageRepository(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public Task<Role> AddRoleAsync(Role role, CancellationToken ct = default)
{
_dbContext.Roles.Add(role);
return Task.FromResult(role);
}
public async Task<Role?> GetRoleByIdAsync(Guid roleId, CancellationToken ct = default)
{
return await _dbContext.Roles.FirstOrDefaultAsync(r => r.RoleId == roleId, ct);
}
public async Task<List<Role>> ListRolesAsync(CancellationToken ct = default)
{
return await _dbContext.Roles.OrderBy(r => r.Code).ToListAsync(ct);
}
//public async Task<bool> RoleInUseAsync(Guid roleId, CancellationToken ct = default)
//{
// return await _dbContext.Users.AnyAsync(u => u.RoleId == roleId, ct);
//}
public Task DeleteRoleAsync(Role role, CancellationToken ct = default)
{
_dbContext.Roles.Remove(role);
return Task.CompletedTask;
}
}
}
+180
View File
@@ -0,0 +1,180 @@
using AuthHex.Interfaces;
using AuthHex.Models;
using AuthHex.Utility.passwordHasher;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AuthHex.Repos
{
public class UserManageRepository : IUserManageRepository
{
private readonly AppDBContext _dbContext;
public UserManageRepository(AppDBContext dbContext)
{
_dbContext = dbContext;
}
public Task<User> AddUserAsync(User user, CancellationToken ct = default)
{
_dbContext.Users.Add(user);
return Task.FromResult(user);
}
public Task<Token> AddTokenAsync(Token token, CancellationToken ct = default)
{
_dbContext.Token.Add(token);
return Task.FromResult(token);
}
public async Task<List<User>> GetUserByIdentifiers(string? email, string? mobileNumber, string? nic, string? username, string? Optional1)
{
var query = _dbContext.Users.AsQueryable();
query = query.Where(u =>
(!string.IsNullOrEmpty(email) && u.Email == email) ||
(!string.IsNullOrEmpty(mobileNumber) && u.MobileNumber == mobileNumber) ||
(!string.IsNullOrEmpty(nic) && u.Nic == nic) ||
(!string.IsNullOrEmpty(username) && u.UserName == username) ||
(!string.IsNullOrEmpty(Optional1) && u.Optional1 == Optional1)
);
return await query.ToListAsync();
}
public async Task<User?> GetUserByIdentifierAndType(string identifier)
{
User? user = await _dbContext.Users
.FirstOrDefaultAsync(u =>
u.Email == identifier ||
u.MobileNumber == identifier ||
u.Nic == identifier
);
//var query = _dbContext.Users.AsQueryable();
//query = query.Where(u =>
// u.Email == identifier ||
// u.MobileNumber == identifier ||
// u.Nic == identifier
//);
//if (userTypeId != Guid.Empty)
// query = query.Where(u => u.UserTypeId == userTypeId);
return user;
}
public async Task<User?> GetUserByIdAsync(Guid userId, CancellationToken ct = default)
{
return await _dbContext.Users
.FirstOrDefaultAsync(u => u.Id == userId, ct);
}
public async Task UpdateUserAsync(User user, CancellationToken ct = default)
{
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync(ct);
}
public async Task<List<UserSession>> GetUserSessionsAsync(Guid userId, CancellationToken ct = default)
{
return await _dbContext.UserSessions
.Where(s => s.UserId == userId)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync(ct);
}
public async Task<UserSession?> GetActiveSessionByRefreshTokenAsync(string refreshToken, CancellationToken ct = default)
{
var activeSessions = await _dbContext.UserSessions
.Include(s => s.User)
.Where(s => s.RevokedAt == null && s.ExpiresAt > DateTime.UtcNow)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync(ct);
foreach (var session in activeSessions)
{
if (PasswordHasher.Verify(refreshToken, session.RefreshTokenHash))
return session;
}
return null;
}
public async Task InvalidateUserSessionsAsync(Guid userId, CancellationToken ct = default)
{
var userSessions = await _dbContext.UserSessions
.Where(s => s.UserId == userId && s.RevokedAt == null)
.ToListAsync(ct);
foreach (var session in userSessions)
{
session.RevokedAt = DateTime.UtcNow;
}
await _dbContext.SaveChangesAsync(ct);
}
//public async Task<bool> GetExUser(string identifier)
//{
// var userExists = await _dbContext.Users
// .AnyAsync(u => u.Email == identifier || u.MobileNumber == identifier || u.Nic == identifier);
// return userExists;
//}
//public async Task<TuteItem?> GetByIdAsync(int id, CancellationToken ct = default)
//{
// return await _dbContext.TuteItems
// .AsNoTracking()
// .FirstOrDefaultAsync(x => x.Id == id, ct);
//}
//public async Task<List<TuteItem>> ListAsync(CancellationToken ct = default)
//{
// return await _dbContext.TuteItems
// .AsNoTracking()
// .OrderByDescending(x => x.Id)
// .ToListAsync(ct);
//}
//public async Task<TuteItem?> UpdateAsync(int id, string title, string? notes, CancellationToken ct = default)
//{
// var entity = await _dbContext.TuteItems.FirstOrDefaultAsync(x => x.Id == id, ct);
// if (entity == null)
// return null;
// entity.Title = title;
// entity.Notes = notes;
// return entity;
//}
//public async Task<bool> DeleteAsync(int id, CancellationToken ct = default)
//{
// var entity = await _dbContext.TuteItems.FirstOrDefaultAsync(x => x.Id == id, ct);
// if (entity == null)
// return false;
// _dbContext.TuteItems.Remove(entity);
// return true;
//}
}
}
@@ -0,0 +1,495 @@
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Utility;
using AuthHex.Utility.passwordHasher;
using AuthSystem.API.Utils;
using Core_Archi.Helpers;
using System.Security.Cryptography;
namespace AuthHex.Services.AltOptionManager;
public class AltOptionManagerService
{
private readonly IRecoveryManagerRepository _repository;
private readonly IUserManageRepository _userRepository;
private readonly IAltOptionManagerRepository _altOptionRepository;
private readonly IUnitOfWork _uow;
private readonly JwtTokenHelper _jwt;
private readonly AppDBContext _dbContext;
private readonly ThirdPartyService _thirdPartyService;
private readonly IConfiguration _configuration;
private readonly UserHelper _userHelper;
public AltOptionManagerService(
IRecoveryManagerRepository Recoveryrepository,
IUserManageRepository userRepository,
IAltOptionManagerRepository altOptionRepository,
IUnitOfWork uow,
JwtTokenHelper jwt,
AppDBContext dbContext,
ThirdPartyService thirdPartyService,
IConfiguration configuration,
UserHelper userHelper)
{
_repository = Recoveryrepository;
_userRepository = userRepository;
_altOptionRepository = altOptionRepository;
_uow = uow;
_jwt = jwt;
_dbContext = dbContext;
_thirdPartyService = thirdPartyService;
_configuration = configuration;
_userHelper = userHelper;
}
public async Task<object> IsAvailable(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
var Identifier = data.ContainsKey("Identifier") ? data["Identifier"].GetString() : null;
var Recovery = data.ContainsKey("Recovery") ? data["Recovery"].GetString() : null;
var existingUsers = await _userRepository.GetUserByIdentifiers(
email: Identifier,
mobileNumber: Identifier,
nic: Identifier,
username: Identifier,
Optional1: Identifier
);
if (existingUsers == null || !existingUsers.Any())
{
return new { IsAvailable = true, Message = "Identifier is available" };
}
if (Recovery == null)
{
return new { IsAvailable = false, Message = "Identifier already in use" };
}
return new { existingUsers };
}
public async Task<object> SendOtp(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
var numberOfDigits = data.ContainsKey("numberOfDigits") ? data["numberOfDigits"].GetInt32() : 6;
var newUser = data.ContainsKey("newUser") ? data["newUser"].GetBoolean() : false;
if (string.IsNullOrWhiteSpace(identifier))
throw new ArgumentException("Identifier is required.");
var otpCode = await GenerateOtpCode(numberOfDigits);
Recovery recovery;
if (newUser)
{
recovery = new Recovery
{
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
OTP = otpCode,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(10),
RecoveryType = "OTP FOR NEW USER"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(identifier))
{
if (IsEmail(identifier))
{
await _thirdPartyService.EmailConfiguration(
identifier,
otpCode,
"Verification OTP",
$@"
<h2>Verification OTP</h2>
<p>Your OTP code is:</p>
<h1>{otpCode}</h1>
<p>This code expires in 10 minutes.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
"
);
}
else if (IsMobileNumber(identifier))
{
await _thirdPartyService.SendSmsAsync(
identifier,
otpCode,
$"Your OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
);
}
else
{
throw new ArgumentException("Invalid identifier format. Must be a valid email or mobile number.");
}
}
return new
{
Success = true,
Message = "OTP sent successfully for new user.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = recovery.RecoveryType
}
};
}
//exisiting one
var user = await _userRepository.GetUserByIdentifierAndType(identifier);
if (user == null)
throw new KeyNotFoundException("No user found with the provided identifier.");
if (user.IsLocked == true)
throw new InvalidOperationException("User account is locked. Please contact support.");
if (user.IsActive == false)
throw new InvalidOperationException("User account is inactive. Please contact support.");
recovery = new Recovery
{
UserId = user.Id,
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
OTP = otpCode,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(10),
RecoveryType = "OTP FOR LOGIN"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(user.MobileNumber))
{
await _thirdPartyService.SendSmsAsync(
user.MobileNumber,
otpCode,
$"Your login OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
);
}
if (!string.IsNullOrWhiteSpace(user.Email))
{
await _thirdPartyService.EmailConfiguration(
user.Email,
otpCode,
"Login OTP",
$@"
<h2>Login OTP</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>Your OTP code is:</p>
<h1>{otpCode}</h1>
<p>This code expires in 10 minutes.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
"
);
}
return new
{
Success = true,
Message = "OTP sent successfully.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = recovery.RecoveryType
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> VerifyOTP(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var ipAddress = httpContext.Connection.RemoteIpAddress?.ToString();
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();
var data = DeserializePayload.Deserialize(payload);
Guid? userId = null;
if (data.ContainsKey("userId") && Guid.TryParse(data["userId"].GetString(), out var parsedUserId))
{
userId = parsedUserId;
}
var referenceNumber = data.ContainsKey("referenceNumber") ? data["referenceNumber"].GetString() : null;
var otpCode = data.ContainsKey("otpCode") ? data["otpCode"].GetString() : null;
var newUser = data.ContainsKey("newUser") ? data["newUser"].GetBoolean() : false;
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
var deviceName = data.ContainsKey("deviceName") ? data["deviceName"].GetString() : "Unknown Device";
if (string.IsNullOrEmpty(referenceNumber) || string.IsNullOrEmpty(otpCode))
throw new ArgumentException("Reference number and OTP code are required.");
var recovery = await _repository.GetRecoveryByReferenceNumAsync(referenceNumber);
if (recovery == null)
throw new KeyNotFoundException("Invalid reference number.");
if (recovery.IsUsed)
throw new InvalidOperationException("This OTP has already been used.");
if (recovery.Status != "Pending")
throw new InvalidOperationException("This OTP is no longer valid.");
if (DateTime.UtcNow > recovery.ExpirationTime)
{
recovery.Status = "Expired";
await _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
throw new InvalidOperationException("This OTP has expired. Please request a new one.");
}
if (recovery.OTP != otpCode)
throw new InvalidOperationException("Invalid OTP code.");
recovery.Status = "Verified";
recovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(recovery);
User? user = null;
if (!string.IsNullOrWhiteSpace(identifier))
{
user = await _userRepository.GetUserByIdentifierAndType(identifier);
}
if (user is null && userId.HasValue)
{
user = await _userRepository.GetUserByIdAsync(userId.Value);
}
if (user is null && recovery.UserId.HasValue)
{
user = await _userRepository.GetUserByIdAsync(recovery.UserId.Value);
}
if (user is null)
throw new KeyNotFoundException("No user found for OTP verification.");
if (!string.IsNullOrWhiteSpace(identifier))
{
if (IsEmail(identifier))
{
user.Email = identifier;
user.EmailVerified = true;
}
else if (IsMobileNumber(identifier))
{
user.MobileNumber = identifier;
user.MobileNumberVerified = true;
}
else
{
user.Optional1 = identifier;
user.Optional1Verified = true;
}
}
await _userRepository.UpdateUserAsync(user);
var jwtToken = await _jwt.GenerateToken(user);
var refreshToken = Guid.NewGuid().ToString();
var refreshTokenHash = PasswordHasher.Hash(refreshToken);
var userSession = new UserSession
{
UserId = user.Id,
RefreshTokenHash = refreshTokenHash,
DeviceName = deviceName,
IPAddress = ipAddress,
Browser = _userHelper.ExtractBrowser(userAgent),
OS = _userHelper.ExtractOS(userAgent),
ExpiresAt = DateTime.UtcNow.AddDays(30),
CreatedAt = DateTime.UtcNow
};
_dbContext.UserSessions.Add(userSession);
await LogAuthEvent(user.Id, "LOGIN_SUCCESS", ipAddress, userAgent);
await _uow.CommitAsync();
return new
{
Success = true,
Message = newUser
? "OTP verified successfully. Session started."
: "OTP verified successfully.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
UserId = user.Id,
Verified = true,
AccessToken = jwtToken,
RefreshToken = refreshToken,
ExpiresIn = 3600,
User = new
{
user.Id,
user.FullName,
user.UserName,
user.Email,
user.MobileNumber,
user.EmailVerified,
user.MobileNumberVerified,
user.IsMfaEnabled,
RoleId = user.RoleCode
}
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
private async Task<string> GenerateOtpCode(int numberOfDigits)
{
if (numberOfDigits <= 0)
throw new ArgumentException("Number of digits must be at least 1", nameof(numberOfDigits));
using var rng = RandomNumberGenerator.Create();
var otpCode = "";
for (int i = 0; i < numberOfDigits; i++)
{
byte[] randomBytes = new byte[4];
rng.GetBytes(randomBytes);
uint randomValue = BitConverter.ToUInt32(randomBytes, 0);
otpCode += (randomValue % 10).ToString();
}
return await Task.FromResult(otpCode);
}
private string GenerateSecureReferenceNumber()
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
using var rng = RandomNumberGenerator.Create();
byte[] randomBytes = new byte[8];
rng.GetBytes(randomBytes);
var randomPart = Convert.ToBase64String(randomBytes)
.Replace("/", "")
.Replace("+", "")
.Replace("=", "")
.Substring(0, 8).ToUpper();
return $"REC{timestamp}{randomPart}";
}
private bool IsEmail(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
return false;
try
{
var emailRegex = new System.Text.RegularExpressions.Regex(
@"^[^@\s]+@[^@\s]+\.[^@\s]+$",
System.Text.RegularExpressions.RegexOptions.IgnoreCase);
return emailRegex.IsMatch(identifier);
}
catch
{
return false;
}
}
private bool IsMobileNumber(string identifier)
{
if (string.IsNullOrWhiteSpace(identifier))
return false;
var cleaned = identifier.Replace(" ", "").Replace("-", "").Replace("(", "").Replace(")", "").Replace("+", "");
return System.Text.RegularExpressions.Regex.IsMatch(cleaned, @"^\d{7,15}$");
}
private async Task LogAuthEvent(Guid? userId, string eventType, string? ipAddress, string? userAgent)
{
var authEvent = new AuthEventLog
{
UserId = userId,
EventType = eventType,
IPAddress = ipAddress,
UserAgent = userAgent,
CreatedAt = DateTime.UtcNow
};
_dbContext.AuthEventLogs.Add(authEvent);
await Task.CompletedTask;
}
}
@@ -0,0 +1,74 @@
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Services.AltOptionManager;
using System.Text.Json;
namespace AuthHex.Services.FunctionHandler
{
public class AltOptionManager
{
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
public readonly AltOptionManagerService _altOptionManagerService;
public AltOptionManager(AltOptionManagerService altOptionManagerService)
{
_altOptionManagerService = altOptionManagerService;
_functionHandler = new()
{
{"IsAvailable", altOptionManagerService.IsAvailable},
{"sendOtp", altOptionManagerService.SendOtp },
{"VerifyOTP", altOptionManagerService.VerifyOTP }
};
}
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
{
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
{
return new ApiResponse
{
StatusCode = 404,
Message = $"Function '{request.FunctionName}' not found.",
Data = null
};
}
try
{
var results = await handler(request.Payload, httpContext);
return new ApiResponse
{
StatusCode = 200,
Message = "success",
Data = results
};
}
catch (KeyNotFoundException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (InvalidOperationException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (ArgumentException ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message,
Data = null
};
}
}
}
}
@@ -0,0 +1,73 @@
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Services.RecoveryManager;
using System.Text.Json;
namespace AuthHex.Services.FunctionHandler
{
public class RecoveryManager
{
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
public RecoveryManager(RecoveryManagerService RecoveryManagerService)
{
_functionHandler = new()
{
{ "forgotPassword", RecoveryManagerService.ForgotPassword },
{ "verifyOTP", RecoveryManagerService.VerifyOTP },
{ "resetPasswordWithToken", RecoveryManagerService.ResetPasswordWithToken },
{ "resetPassword", RecoveryManagerService.ResetPassword }
};
}
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
{
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
{
return new ApiResponse
{
StatusCode = 404,
Message = $"Function '{request.FunctionName}' not found.",
Data = null
};
}
try
{
var results = await handler(request.Payload, httpContext);
return new ApiResponse
{
StatusCode = 200,
Message = "success",
Data = results
};
}
catch (KeyNotFoundException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (InvalidOperationException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (ArgumentException ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message,
Data = null
};
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
using AuthHex.Models.IOs;
using AuthHex.Services.RoleManager;
namespace AuthHex.Services.FunctionHandler
{
public class RoleManager
{
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
public RoleManager(RoleManagerService roleManagerService)
{
_functionHandler = new()
{
{ "createRole", roleManagerService.CreateRole },
{ "getRole", roleManagerService.GetRole },
{ "listRoles", async (payload, context) => await roleManagerService.ListRoles(context) },
{ "updateRole", roleManagerService.UpdateRole },
{ "deleteRole", roleManagerService.DeleteRole }
};
}
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
{
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
{
return new ApiResponse
{
StatusCode = 404,
Message = $"Function '{request.FunctionName}' not found.",
Data = null
};
}
try
{
var results = await handler(request.Payload, httpContext);
return new ApiResponse
{
StatusCode = 200,
Message = "success",
Data = results
};
}
catch (KeyNotFoundException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (InvalidOperationException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (ArgumentException ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message,
Data = null
};
}
}
}
}
+88
View File
@@ -0,0 +1,88 @@
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Services.UserManager;
using System.Text.Json;
namespace AuthHex.Services.FunctionHandler
{
public class UserManager
{
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
public UserManager(UserManagerService UserManagerService)
{
_functionHandler = new()
{
{ "registerUser", UserManagerService.RegisterUser },
{ "loginUser", UserManagerService.LoginUser },
{ "refreshToken", UserManagerService.RefreshToken },
{ "getUserDetails", UserManagerService.GetUserDetails },
{ "getUserSessions", async (payload, context) => await UserManagerService.GetUserSessions(context) },
{ "ChangeUserStatus", UserManagerService.ChangeUserStatus},
{"LockUserAccount" , UserManagerService.LockUserAccount },
{"ChangeUserPassword" , UserManagerService.ChangeUserPassword },
{"VerifyPassword" , UserManagerService.VerifyPassword },
{"LogoutUser" , UserManagerService.LogoutUser},
{"UpdateUser", UserManagerService.UpdateUser},
{"VerifyOtpForLogin",UserManagerService.VerifyOtpForLogin},
// 2FA Management Functions
{ "initiateTwoFASetup", async (payload, context) => await UserManagerService.InitiateTwoFASetup(context) },
{ "completeTwoFASetup", UserManagerService.CompleteTwoFASetup },
{ "verifyTwoFA", UserManagerService.VerifyTwoFA },
{ "disableTwoFA", UserManagerService.DisableTwoFA },
{ "getTwoFAStatus", async (payload, context) => await UserManagerService.GetTwoFAStatus(context) }
};
}
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
{
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
{
return new ApiResponse
{
StatusCode = 404,
Message = $"Function '{request.FunctionName}' not found.",
Data = null
};
}
try
{
var results = await handler(request.Payload, httpContext);
return new ApiResponse
{
StatusCode = 200,
Message = "success",
Data = results
};
}
catch (KeyNotFoundException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (InvalidOperationException ex)
{
return new ApiResponse
{
StatusCode = 400,
Message = ex.Message,
Data = null
};
}
catch (ArgumentException ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message,
Data = null
};
}
}
}
}
@@ -0,0 +1,520 @@
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Models;
using AuthHex.Models.IOs;
using AuthHex.Utility.passwordHasher;
using AuthSystem.API.Utils;
using Core_Archi.Helpers;
using Microsoft.EntityFrameworkCore;
using System.Security.Cryptography;
using AuthHex.Utility;
namespace AuthHex.Services.RecoveryManager;
public class RecoveryManagerService
{
private readonly IRecoveryManagerRepository _repository;
private readonly IUserManageRepository _userRepository;
private readonly IUnitOfWork _uow;
private readonly JwtTokenHelper _jwt;
private readonly AppDBContext _dbContext;
private readonly ThirdPartyService _thirdPartyService;
private readonly IConfiguration _configuration;
public RecoveryManagerService(
IRecoveryManagerRepository Recoveryrepository,
IUserManageRepository userRepository,
IUnitOfWork uow,
JwtTokenHelper jwt,
AppDBContext dbContext,
ThirdPartyService thirdPartyService,
IConfiguration configuration)
{
_repository = Recoveryrepository;
_userRepository = userRepository;
_uow = uow;
_jwt = jwt;
_dbContext = dbContext;
_thirdPartyService = thirdPartyService;
_configuration = configuration;
}
public async Task<object> ForgotPassword(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var identifier = data.ContainsKey("identifier") ? data["identifier"].GetString() : null;
var useResetLink = data.ContainsKey("useResetLink") ? data["useResetLink"].GetBoolean() : false;
var numberOfDigits = data.ContainsKey("numberOfDigits") ? data["numberOfDigits"].GetInt32() : 6;
if (string.IsNullOrEmpty(identifier))
throw new ArgumentException("Identifier is required.");
var user = await _userRepository.GetUserByIdentifierAndType(identifier);
if (user == null)
throw new KeyNotFoundException("No user found with the provided identifier.");
if (user.IsLocked == true)
throw new InvalidOperationException("User account is locked. Please contact support.");
if (user.IsActive == false)
throw new InvalidOperationException("User account is inactive. Please contact support.");
// invalidate all previous pending recovery attempts for this user ..
await _userRepository.InvalidateUserSessionsAsync(user.Id);
var pendingRecoveries = await _repository.GetPendingRecoveriesByUserIdAsync(user.Id);
foreach (var oldRecovery in pendingRecoveries)
{
oldRecovery.Status = "Expired";
oldRecovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(oldRecovery);
}
Recovery recovery;
if (useResetLink)
{
var resetToken = GenerateSecureResetToken();
var tokenHash = HashToken(resetToken);
recovery = new Recovery
{
UserId = user.Id,
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
ResetTokenHash = tokenHash,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(15),
RecoveryType = "ResetLink"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
// send reset link via email
if (!string.IsNullOrWhiteSpace(user.Email))
{
var frontendUrl = _configuration["AppSettings:FrontendUrl"] ?? "http://localhost:3000"; //wanna add app.setting frontend URL
var resetLink = $"{frontendUrl}/reset-password?token={resetToken}";
await _thirdPartyService.EmailConfiguration(
user.Email!,
resetToken,
"Password Reset Request",
$@"
<h2>Password Reset Request</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>You requested to reset your password. Click the link below to proceed:</p>
<p><a href='{resetLink}'>Reset Password</a></p>
<p>This link will expire in 15 minutes.</p>
<p>If you didn't request this, please ignore this email.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
");
}
return new
{
Success = true,
Message = "Password reset link sent to your email.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = "ResetLink"
}
};
}
else
{
var otpCode = await GenerateOtpCode(numberOfDigits);
recovery = new Recovery
{
UserId = user.Id,
RecoveryReferenceNum = GenerateSecureReferenceNumber(),
OTP = otpCode,
Status = "Pending",
IsUsed = false,
CreatedAt = DateTime.UtcNow,
ExpirationTime = DateTime.UtcNow.AddMinutes(10),
RecoveryType = "OTP"
};
await _repository.AddRecoveryAsync(recovery);
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(user.MobileNumber))
{
await _thirdPartyService.SendSmsAsync(
user.MobileNumber!,
otpCode,
$"Your password recovery OTP is: {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}");
}
if (!string.IsNullOrWhiteSpace(user.Email))
{
await _thirdPartyService.EmailConfiguration(
user.Email!,
otpCode,
"Password Recovery OTP",
$@"
<h2>Password Recovery OTP</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>Your OTP code is: <strong style='font-size: 24px;'>{otpCode}</strong></p>
<p>This code expires in 10 minutes.</p>
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
");
}
return new
{
Success = true,
Message = "OTP sent successfully.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
ExpiresAt = recovery.ExpirationTime,
RecoveryType = "OTP"
}
};
}
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> VerifyOTP(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var referenceNumber = data.ContainsKey("referenceNumber") ? data["referenceNumber"].GetString() : null;
var otpCode = data.ContainsKey("otpCode") ? data["otpCode"].GetString() : null;
if (string.IsNullOrEmpty(referenceNumber) || string.IsNullOrEmpty(otpCode))
throw new ArgumentException("Reference number and OTP code are required.");
var recovery = await _repository.GetRecoveryByReferenceNumAsync(referenceNumber);
if (recovery == null)
throw new KeyNotFoundException("Invalid reference number.");
if (recovery.IsUsed)
throw new InvalidOperationException("This OTP has already been used.");
if (recovery.Status != "Pending")
throw new InvalidOperationException("This OTP is no longer valid.");
if (DateTime.UtcNow > recovery.ExpirationTime)
{
recovery.Status = "Expired";
await _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
throw new InvalidOperationException("This OTP has expired. Please request a new one.");
}
if (recovery.OTP != otpCode)
throw new InvalidOperationException("Invalid OTP code.");
recovery.Status = "Verified";
await _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
return new
{
Success = true,
Message = "OTP verified successfully. You can now reset your password.",
Data = new
{
ReferenceNumber = recovery.RecoveryReferenceNum,
UserId = recovery.UserId,
Verified = true
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> ResetPasswordWithToken(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var resetToken = data.ContainsKey("resetToken") ? data["resetToken"].GetString() : null;
var newPassword = data.ContainsKey("newPassword") ? data["newPassword"].GetString() : null;
var confirmPassword = data.ContainsKey("confirmPassword") ? data["confirmPassword"].GetString() : null;
if (string.IsNullOrEmpty(resetToken))
throw new ArgumentException("Reset token is required.");
if (string.IsNullOrEmpty(newPassword) || string.IsNullOrEmpty(confirmPassword))
throw new ArgumentException("New password and confirmation are required.");
if (newPassword != confirmPassword)
throw new ArgumentException("Passwords do not match.");
if (newPassword.Length < 8)
throw new ArgumentException("Password must be at least 8 characters long.");
var tokenHash = HashToken(resetToken);
var recovery = await _repository.GetRecoveryByTokenHashAsync(tokenHash);
if (recovery == null)
throw new KeyNotFoundException("Invalid or expired reset token.");
if (recovery.IsUsed)
throw new InvalidOperationException("This reset link has already been used.");
if (recovery.Status != "Pending")
throw new InvalidOperationException("This reset link is no longer valid.");
if (DateTime.UtcNow > recovery.ExpirationTime)
{
recovery.Status = "Expired";
recovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
throw new InvalidOperationException("This reset link has expired. Please request a new one.");
}
if (recovery.RecoveryType != "ResetLink")
throw new InvalidOperationException("Invalid recovery method.");
// Get user
var user = await _userRepository.GetUserByIdAsync((Guid)recovery.UserId);
if (user == null)
throw new KeyNotFoundException("User not found.");
if (user.IsLocked == true)
throw new InvalidOperationException("User account is locked. Please contact support.");
user.PasswordHash = PasswordHasher.Hash(newPassword);
// Update user
await _dbContext.SaveChangesAsync();
recovery.Status = "Used";
recovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(recovery);
// invalid all user sessions (force re-login)
await _userRepository.InvalidateUserSessionsAsync(user.Id); // can change BE requirement
await _uow.CommitAsync();
if (!string.IsNullOrWhiteSpace(user.Email))
{
await _thirdPartyService.EmailConfiguration(
user.Email!,
"SUCCESS",
"Password Reset Successful",
$@"
<h2>Password Reset Successful</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>Your password has been successfully reset.</p>
<p>If you did not make this change, please contact support immediately.</p>
<p><strong>Time:</strong> {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</p>
");
}
return new
{
Success = true,
Message = "Password reset successful. Please login with your new password.",
Data = new
{
UserId = user.Id,
Email = user.Email,
ResetAt = DateTime.UtcNow
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> ResetPassword(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var referenceNumber = data.ContainsKey("referenceNumber") ? data["referenceNumber"].GetString() : null;
var newPassword = data.ContainsKey("newPassword") ? data["newPassword"].GetString() : null;
var confirmPassword = data.ContainsKey("confirmPassword") ? data["confirmPassword"].GetString() : null;
if (string.IsNullOrEmpty(referenceNumber))
throw new ArgumentException("Reference number is required.");
if (string.IsNullOrEmpty(newPassword) || string.IsNullOrEmpty(confirmPassword))
throw new ArgumentException("New password and confirmation are required.");
if (newPassword != confirmPassword)
throw new ArgumentException("Passwords do not match.");
if (newPassword.Length < 8)
throw new ArgumentException("Password must be at least 8 characters long.");
var recovery = await _repository.GetRecoveryByReferenceNumAsync(referenceNumber);
if (recovery == null)
throw new KeyNotFoundException("Invalid reference number.");
if (recovery.Status != "Verified")
throw new InvalidOperationException("Please verify your OTP first before resetting password.");
if (recovery.IsUsed)
throw new InvalidOperationException("This recovery request has already been used.");
if (DateTime.UtcNow > recovery.ExpirationTime)
{
recovery.Status = "Expired";
recovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(recovery);
await _uow.CommitAsync();
throw new InvalidOperationException("This recovery request has expired.");
}
var user = await _userRepository.GetUserByIdAsync((Guid)recovery.UserId);
if (user == null)
throw new KeyNotFoundException("User not found.");
if (user.IsLocked == true)
throw new InvalidOperationException("User account is locked. Please contact support.");
user.PasswordHash = PasswordHasher.Hash(newPassword);
await _dbContext.SaveChangesAsync();
recovery.Status = "Used";
recovery.IsUsed = true;
await _repository.UpdateRecoveryAsync(recovery);
await _userRepository.InvalidateUserSessionsAsync(user.Id); //as previous comment can be changed BE requirement
await _uow.CommitAsync();
// Send confirmation email
if (!string.IsNullOrWhiteSpace(user.Email))
{
await _thirdPartyService.EmailConfiguration(
user.Email!,
"SUCCESS",
"Password Reset Successful",
$@"
<h2>Password Reset Successful</h2>
<p>Hello {user.FullName ?? user.UserName},</p>
<p>Your password has been successfully reset.</p>
<p>If you did not make this change, please contact support immediately.</p>
<p><strong>Time:</strong> {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC</p>
");
}
return new
{
Success = true,
Message = "Password reset successful. Please login with your new password.",
Data = new
{
UserId = user.Id,
Email = user.Email,
ResetAt = DateTime.UtcNow
}
};
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
//------------------------------- HELPER METHODS -------------------------------
private string GenerateSecureResetToken()
{
using var rng = RandomNumberGenerator.Create();
byte[] tokenBytes = new byte[32];
rng.GetBytes(tokenBytes);
// Convert to URL-safe base64 string
return Convert.ToBase64String(tokenBytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "");
}
private string HashToken(string token)
{
using var sha256 = SHA256.Create();
byte[] hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(token));
return Convert.ToBase64String(hashBytes);
}
private async Task<string> GenerateOtpCode(int numberOfDigits)
{
if (numberOfDigits <= 0 )
throw new ArgumentException("Number of digits must be at least 1", nameof(numberOfDigits));
using var rng = RandomNumberGenerator.Create();
var otpCode = "";
for (int i = 0; i < numberOfDigits; i++)
{
byte[] randomBytes = new byte[4];
rng.GetBytes(randomBytes);
uint randomValue = BitConverter.ToUInt32(randomBytes, 0);
otpCode += (randomValue % 10).ToString();
}
return await Task.FromResult(otpCode);
}
private string GenerateSecureReferenceNumber()
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
using var rng = RandomNumberGenerator.Create();
byte[] randomBytes = new byte[8];
rng.GetBytes(randomBytes);
var randomPart = Convert.ToBase64String(randomBytes)
.Replace("/", "")
.Replace("+", "")
.Replace("=", "")
.Substring(0, 8).ToUpper();
return $"REC{timestamp}{randomPart}";
}
}
+151
View File
@@ -0,0 +1,151 @@
using System.Text.Json;
using AuthHex.Infra.UoW;
using AuthHex.Interfaces;
using AuthHex.Models;
using Core_Archi.Helpers;
using Microsoft.AspNetCore.Http;
namespace AuthHex.Services.RoleManager;
public class RoleManagerService
{
private readonly IRoleManageRepository _repository;
private readonly IUnitOfWork _uow;
public RoleManagerService(IRoleManageRepository repository, IUnitOfWork uow)
{
_repository = repository;
_uow = uow;
}
public async Task<object> CreateRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
var code = data.ContainsKey("code") ? data["code"].GetString() : null;
var name = data.ContainsKey("name") ? data["name"].GetString() : null;
var isSystemRole = data.ContainsKey("isSystemRole")
&& data["isSystemRole"].ValueKind != JsonValueKind.Null
&& data["isSystemRole"].GetBoolean();
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("code is required");
var role = new Role
{
RoleId = Guid.NewGuid(),
Code = code,
Name = string.IsNullOrWhiteSpace(name) ? code : name,
IsSystemRole = isSystemRole,
CreatedAt = DateTime.UtcNow
};
await _repository.AddRoleAsync(role);
await _uow.CommitAsync();
return ToDto(role);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> GetRole(object payload, HttpContext httpContext)
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
return ToDto(role);
}
public async Task<object> ListRoles(HttpContext httpContext)
{
var roles = await _repository.ListRolesAsync();
return roles.Select(ToDto).ToList();
}
public async Task<object> UpdateRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
if (data.ContainsKey("code"))
{
var code = data["code"].GetString();
if (string.IsNullOrWhiteSpace(code))
throw new ArgumentException("code cannot be empty");
role.Code = code;
}
if (data.ContainsKey("name"))
role.Name = data["name"].GetString();
if (data.ContainsKey("isSystemRole") && data["isSystemRole"].ValueKind != JsonValueKind.Null)
role.IsSystemRole = data["isSystemRole"].GetBoolean();
await _uow.CommitAsync();
return ToDto(role);
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
public async Task<object> DeleteRole(object payload, HttpContext httpContext)
{
await _uow.BeginAsync();
try
{
var data = DeserializePayload.Deserialize(payload);
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
throw new ArgumentException("Invalid roleId");
var role = await _repository.GetRoleByIdAsync(roleId)
?? throw new KeyNotFoundException("Role not found");
//if (await _repository.RoleInUseAsync(roleId))
// throw new InvalidOperationException("ROLE_IN_USE: role is assigned to one or more users");
await _repository.DeleteRoleAsync(role);
await _uow.CommitAsync();
return new { message = "Role deleted successfully" };
}
catch
{
await _uow.RollbackAsync();
throw;
}
}
private static object ToDto(Role role) => new
{
role.RoleId,
role.Code,
role.Name,
role.IsSystemRole,
role.CreatedAt
};
}
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
using AuthHex;
using AuthHex.Models;
using AuthHex.Utility;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
namespace AuthSystem.API.Utils;
public class JwtTokenHelper
{
private readonly IConfiguration _config;
private readonly AppDBContext _dbContext;
private readonly RsaKeyProvider _rsaKeyProvider;
public JwtTokenHelper(IConfiguration config, AppDBContext dbContext, RsaKeyProvider rsaKeyProvider)
{
_config = config;
_dbContext = dbContext;
_rsaKeyProvider = rsaKeyProvider;
}
public async Task<string> GenerateToken(User user)
{
var claims = new List<Claim>
{
new Claim("UserId", user.Id.ToString()),
new Claim("RoleId", user.RoleCode ?? ""),
new Claim("RoleCode", user.RoleCode ?? ""),
new Claim("NIC", user.Nic ?? ""),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat,
new DateTimeOffset(DateTime.UtcNow).ToUnixTimeSeconds().ToString(),
ClaimValueTypes.Integer64)
};
var privateKey = _rsaKeyProvider.GetPrivateKey();
var creds = new SigningCredentials(privateKey, SecurityAlgorithms.RsaSha256);
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: DateTime.UtcNow.AddMinutes(Convert.ToDouble(_config["Jwt:ExpiresInMinutes"]!)),
signingCredentials: creds
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
public Guid ExtractUserIdFromToken(string token)
{
try
{
var tokenHandler = new JwtSecurityTokenHandler();
var publicKey = _rsaKeyProvider.GetPublicKey();
tokenHandler.ValidateToken(token, new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = publicKey,
ValidateIssuer = true,
ValidIssuer = _config["Jwt:Issuer"],
ValidateAudience = true,
ValidAudience = _config["Jwt:Audience"],
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
}, out SecurityToken validatedToken);
var jwtToken = (JwtSecurityToken)validatedToken;
var userIdClaim = jwtToken.Claims.FirstOrDefault(x => x.Type == "UserId");
if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out Guid userId))
{
return userId;
}
return Guid.Empty;
}
catch
{
return Guid.Empty;
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using BCrypt.Net;
namespace AuthHex.Utility.passwordHasher;
public static class PasswordHasher
{
public static string Hash(string password) => BCrypt.Net.BCrypt.HashPassword(password);
public static bool Verify(string password, string hash) => BCrypt.Net.BCrypt.Verify(password, hash);
}
+53
View File
@@ -0,0 +1,53 @@
using System.Security.Cryptography;
using System.Text;
using Microsoft.IdentityModel.Tokens;
namespace AuthHex.Utility;
public class RsaKeyProvider
{
private readonly RSA _rsa;
public RsaKeyProvider(string privateKeyXml)
{
_rsa = RSA.Create();
_rsa.FromXmlString(privateKeyXml);
}
public RsaSecurityKey GetPrivateKey()
{
var key = new RsaSecurityKey(_rsa);
var publicParameters = _rsa.ExportParameters(false);
key.KeyId = ComputeKeyId(publicParameters);
return key;
}
public RsaSecurityKey GetPublicKey()
{
var publicKeyRsa = RSA.Create();
var publicParameters = _rsa.ExportParameters(false);
publicKeyRsa.ImportParameters(publicParameters);
var key = new RsaSecurityKey(publicKeyRsa);
key.KeyId = ComputeKeyId(publicParameters);
return key;
}
private static string ComputeKeyId(RSAParameters publicParameters)
{
var modulus = publicParameters.Modulus ?? Array.Empty<byte>();
var exponent = publicParameters.Exponent ?? Array.Empty<byte>();
var payload = $"{Convert.ToBase64String(modulus)}.{Convert.ToBase64String(exponent)}";
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(payload));
return Base64UrlEncoder.Encode(hash);
}
public static (string privateKey, string publicKey) GenerateKeys()
{
using var rsa = RSA.Create(2048);
var privateKey = rsa.ToXmlString(true); // includes private key
var publicKey = rsa.ToXmlString(false); // public key only
return (privateKey, publicKey);
}
}
+567
View File
@@ -0,0 +1,567 @@
using AuthHex.Models.IOs;
using AuthHex.DTOs;
using System.Text.Json;
using System.Net.Mail;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace AuthHex.Utility
{
public class ThirdPartyService
{
private readonly IConfiguration _config;
private readonly HttpClient _httpClient;
public ThirdPartyService(IConfiguration config, HttpClient httpClient)
{
_config = config;
_httpClient = httpClient;
}
public async Task<ApiResponse> EmailConfiguration(string email, string otpCode, string? subject = null, string? body = null)
{
try
{
if (string.IsNullOrWhiteSpace(email))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Email is required"
};
}
if (string.IsNullOrWhiteSpace(otpCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "OTP code is required"
};
}
var testEmail = email;
var result = await SendEmailAsync(testEmail, otpCode, subject, body);
return new ApiResponse
{
StatusCode = result.Success ? 200 : 500,
Data = new
{
testEmail,
success = result.Success
},
Message = result.Message
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<OtpResponse> SendSmsAsync(string mobileNumber, string otpCode, string? message = null)
{
try
{
if (string.IsNullOrWhiteSpace(mobileNumber))
{
return new OtpResponse
{
StatusCode = 400,
Message = "Mobile number is required"
};
}
if (string.IsNullOrWhiteSpace(otpCode))
{
return new OtpResponse
{
StatusCode = 400,
Message = "OTP code is required"
};
}
string contactNumber = mobileNumber;
string apiUrl = _config["Sms:ApiUrl"] ?? "https://backup.introps.com/Sms_common/send_sms";
string apiKey = Environment.GetEnvironmentVariable("SMS_API_KEY") ?? _config["Sms:ApiKey"] ?? "8989898989";
var msgText = string.IsNullOrWhiteSpace(message) ? $"Your otp code is: {otpCode}" : message;
var smsRequest = new SmsRequest
{
api_key = apiKey,
phone_number = contactNumber,
msg_text = msgText
};
var formContent = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("api_key", smsRequest.api_key),
new KeyValuePair<string, string>("phone_number", smsRequest.phone_number),
new KeyValuePair<string, string>("msg_text", smsRequest.msg_text)
});
var response = await _httpClient.PostAsync(apiUrl, formContent);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode && responseContent.Contains("agent:d"))
{
string jsonString = responseContent.Split(new[] { "agent:d" }, StringSplitOptions.None)[1];
var responseJson = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonString);
if (responseJson is not null &&
responseJson.TryGetValue("stt", out var statusValue) &&
statusValue?.ToString() == "ok")
{
Console.WriteLine($"SMS OTP {otpCode} sent to {contactNumber}");
return new OtpResponse
{
StatusCode = 200,
Message = "OTP sent successfully."
};
}
}
Console.WriteLine($"Failed to send SMS OTP to {contactNumber}. Response: {responseContent}");
return new OtpResponse
{
StatusCode = 500,
Message = "Failed to send OTP.",
ApiResponse = responseContent
};
}
catch (Exception ex)
{
Console.WriteLine($"Exception sending OTP: {ex.Message}");
return new OtpResponse
{
StatusCode = 500,
Message = "Exception occurred while sending OTP.",
Error = ex.Message
};
}
}
private async Task<(bool Success, string Message)> SendEmailAsync(string email, string code, string? subject = null, string? body = null)
{
try
{
if (string.IsNullOrWhiteSpace(email))
{
return (false, "Email address is required");
}
string smtpHost = _config["SmtpSettings:SmtpServer"] ?? "smtp.hostinger.com";
int smtpPort = int.Parse(_config["SmtpSettings:SmtpPort"] ?? "587");
string? senderEmail = _config["SmtpSettings:SenderEmail"] ?? Environment.GetEnvironmentVariable("SMTP_USER");
string? senderPassword = _config["SmtpSettings:SmtpPassword"] ?? Environment.GetEnvironmentVariable("SMTP_PASSWORD");
string senderName = _config["SmtpSettings:SenderName"] ?? "Hexa Fitness";
if (string.IsNullOrWhiteSpace(senderEmail) || string.IsNullOrWhiteSpace(senderPassword))
{
return (false, "Email configuration is missing");
}
using (var smtpClient = new SmtpClient(smtpHost, smtpPort))
{
smtpClient.Credentials = new NetworkCredential(senderEmail, senderPassword);
smtpClient.EnableSsl = true;
smtpClient.Timeout = 10000;
var emailSubject = string.IsNullOrWhiteSpace(subject) ? "Your OTP Code" : subject;
var emailBody = string.IsNullOrWhiteSpace(body) ? $@"
<html>
<body style='font-family: Arial, sans-serif;'>
<h2>OTP Verification</h2>
<p>Your OTP code is:</p>
<h1 style='color: #007bff; letter-spacing: 2px;'>{code}</h1>
<p>This code will expire in 10 minutes.</p>
<p>If you did not request this code, please ignore this email.</p>
</body>
</html>" : body.Replace("{code}", code);
var mailMessage = new MailMessage
{
From = new MailAddress(senderEmail, senderName),
Subject = emailSubject,
Body = emailBody,
IsBodyHtml = true
};
mailMessage.To.Add(email);
await smtpClient.SendMailAsync(mailMessage);
Console.WriteLine($"Email OTP {code} sent to {email}");
return (true, "Email sent successfully");
}
}
catch (SmtpException ex)
{
Console.WriteLine($"SMTP Error sending email: {ex.Message}");
return (false, $"Failed to send email: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Exception sending email: {ex.Message}");
return (false, $"Exception occurred: {ex.Message}");
}
}
public string GenerateSecretKey(int? length = null)
{
var keyLength = length ?? int.Parse(_config["GoogleAuthenticator:SecretKeyLength"] ?? "32");
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
var random = new Random();
var secret = new StringBuilder(keyLength);
for (int i = 0; i < keyLength; i++)
{
secret.Append(chars[random.Next(chars.Length)]);
}
return secret.ToString();
}
public string GenerateQrCodeData(string userEmail, string secretKey, string? issuer = null)
{
var appIssuer = issuer ?? _config["GoogleAuthenticator:Issuer"] ?? "AuthHex";
var encodedIssuer = Uri.EscapeDataString(appIssuer);
var encodedEmail = Uri.EscapeDataString(userEmail);
return $"otpauth://totp/{encodedIssuer}:{encodedEmail}?secret={secretKey}&issuer={encodedIssuer}";
}
public string GenerateTotpCode(string secretKey, DateTime? timeStamp = null)
{
var time = timeStamp ?? DateTime.UtcNow;
var unixTime = ((DateTimeOffset)time).ToUnixTimeSeconds();
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var timeStep = unixTime / timeWindow;
var secretBytes = Base32Decode(secretKey);
var timeBytes = BitConverter.GetBytes(timeStep);
if (BitConverter.IsLittleEndian)
{
Array.Reverse(timeBytes);
}
using (var hmac = new HMACSHA1(secretBytes))
{
var hash = hmac.ComputeHash(timeBytes);
var offset = hash[hash.Length - 1] & 0x0F;
var code = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);
return (code % 1000000).ToString("D6");
}
}
public bool ValidateTotpCode(string secretKey, string userCode, int? windowSize = null)
{
if (string.IsNullOrWhiteSpace(secretKey) || string.IsNullOrWhiteSpace(userCode))
{
return false;
}
var validationWindow = windowSize ?? int.Parse(_config["GoogleAuthenticator:ValidationWindowSize"] ?? "1");
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var currentTime = DateTime.UtcNow;
for (int i = -validationWindow; i <= validationWindow; i++)
{
var timeToCheck = currentTime.AddSeconds(i * timeWindow);
var generatedCode = GenerateTotpCode(secretKey, timeToCheck);
if (generatedCode == userCode)
{
return true;
}
}
return false;
}
public async Task<ApiResponse> InitiateTwoFASetup(string userEmail, string? userName = null)
{
try
{
if (string.IsNullOrWhiteSpace(userEmail))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Email is required"
};
}
var secretKey = GenerateSecretKey();
var issuer = _config["GoogleAuthenticator:Issuer"] ?? "AuthHex";
var qrCodeData = GenerateQrCodeData(userEmail, secretKey, issuer);
var displayName = string.IsNullOrWhiteSpace(userName) ? userEmail : userName;
return new ApiResponse
{
StatusCode = 200,
Data = new
{
secretKey,
qrCodeData,
manualEntryKey = FormatSecretForManualEntry(secretKey),
userEmail,
displayName,
issuer,
setupInstructions = new
{
step1 = "Install Google Authenticator app on your mobile device",
step2 = "Scan the QR code or enter the manual key",
step3 = "Enter the 6-digit code from your app to verify setup"
}
},
Message = "2FA setup initiated. Please verify with your authenticator app to complete setup."
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> CompleteTwoFASetup(string secretKey, string verificationCode, string? userEmail = null)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(verificationCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required"
};
}
// Validate the code to ensure user has successfully set up their authenticator
var isValid = ValidateTotpCode(secretKey, verificationCode);
if (!isValid)
{
return new ApiResponse
{
StatusCode = 400,
Message = "Invalid verification code. Please check your authenticator app and try again."
};
}
return new ApiResponse
{
StatusCode = 200,
Data = new
{
secretKey,
isVerified = true,
verifiedAt = DateTime.UtcNow,
userEmail,
backupCodes = GenerateBackupCodes() // Generate backup codes for recovery
},
Message = "2FA setup completed successfully. Please save your backup codes in a secure location."
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> VerifyTwoFACode(string secretKey, string code)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(code))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required"
};
}
var isValid = ValidateTotpCode(secretKey, code);
return new ApiResponse
{
StatusCode = isValid ? 200 : 400,
Data = new
{
isValid,
timestamp = DateTime.UtcNow,
remainingTime = GetRemainingTimeInCurrentWindow()
},
Message = isValid ? "Code verified successfully" : "Invalid or expired code"
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
public async Task<ApiResponse> DisableTwoFA(string secretKey, string verificationCode)
{
try
{
if (string.IsNullOrWhiteSpace(secretKey))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Secret key is required"
};
}
if (string.IsNullOrWhiteSpace(verificationCode))
{
return new ApiResponse
{
StatusCode = 400,
Message = "Verification code is required to disable 2FA"
};
}
// Require valid code to disable 2FA for security
var isValid = ValidateTotpCode(secretKey, verificationCode);
if (!isValid)
{
return new ApiResponse
{
StatusCode = 400,
Message = "Invalid verification code. Cannot disable 2FA without valid code."
};
}
return new ApiResponse
{
StatusCode = 200,
Data = new
{
disabled = true,
disabledAt = DateTime.UtcNow
},
Message = "2FA has been disabled successfully"
};
}
catch (Exception ex)
{
return new ApiResponse
{
StatusCode = 500,
Message = ex.Message
};
}
}
private string[] GenerateBackupCodes(int count = 10)
{
var codes = new string[count];
var random = new Random();
for (int i = 0; i < count; i++)
{
codes[i] = random.Next(100000, 999999).ToString();
}
return codes;
}
private int GetRemainingTimeInCurrentWindow()
{
var timeWindow = int.Parse(_config["GoogleAuthenticator:TimeWindowSeconds"] ?? "30");
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var remainingSeconds = timeWindow - (currentTime % timeWindow);
return (int)remainingSeconds;
}
private byte[] Base32Decode(string input)
{
if (string.IsNullOrEmpty(input))
throw new ArgumentException("Input cannot be null or empty");
input = input.TrimEnd('=').ToUpper();
var output = new List<byte>();
var bits = 0;
var bitsCount = 0;
foreach (char c in input)
{
var value = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".IndexOf(c);
if (value < 0)
throw new ArgumentException($"Invalid character in Base32 string: {c}");
bits = (bits << 5) | value;
bitsCount += 5;
if (bitsCount >= 8)
{
output.Add((byte)(bits >> (bitsCount - 8)));
bitsCount -= 8;
}
}
return output.ToArray();
}
private string FormatSecretForManualEntry(string secret)
{
return string.Join(" ", Enumerable.Range(0, secret.Length / 4)
.Select(i => secret.Substring(i * 4, 4)));
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Jwt": {
"Issuer": "AuthHex",
"Audience": "AuthHexClient",
"ExpiresInMinutes": "60",
"RsaPrivateKey": "<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent><P>8eD+LRKe1Rh+mLlSxxxj0UmifzS5dlpjtwlmtbRSjk8/WCiMiDHVUqXv3ga4HM9/hbYVyLrq9BOrYANA+itxCI1mZY5+o3PAzowuHuqrEvMHayu/s9U9uvZx54+d/SJEzLeu6uV9/vDDc8KHBtXvRx4LoGXteI4o7QqwWjIKjeM=</P><Q>4SSSnceuHQ9oKCBI8vQuivn7sPW3JdfPSUTiqcWX5+ZWUPv1akopsyqdzunDCf9uNjlSd71HW9TEJHfE8F45bJlsuKzBXnh2g1OsrrmLHumOOsBjvrhCxPltNC2wJC9kIPBuiK0c/U3KL1VC6X98rr2i7sKVAv4nYM7dZtprCXc=</Q><DP>oXYzthrL9CwZthFf9JI6/6ZL1uZ3N043aiPYcEsjIOQ9wLewt+msITOr32ZU2MaatcNK32gHF09aAwmpAlalabQaOojYeHv4pOhmkTTNZiTdOulFWuJqBrgkaRMxv5x0mMO0/BCd/uTxVADy6dk9lRe1YyknUGZ1Y2bTOiJVcUk=</DP><DQ>tRBWoXGPQ2u6llqwkEN0kuxMUJqr+lE/MWum5mY0rpl93Y9kZWDTfHXPiDJTWt0D47Ph8M5WbzIDtFhmF/GPORmCaFr3AGTc2u0WOUaa7fdjHTjQfvdtK0B1ZLtBpzg9zIfQPTcL02MWMU7eiy6uNVHpkR8H++ot/Rsgz7Dk2C0=</DQ><InverseQ>GJGZYaPDiE+QeGE1ZrFF4vU+HmOJxG8/+Mxgi5DzPTOSJ9eVjPcXJWeJdC6P1NewWzWk33wlKQ6WOpm78WfTRWIeh9udI3IFqxaW1qs/0fu3ogrF7C5uAaLEqJlLEXAC+Buj2bpLlQlC/qOIWiYJOmYzyQGALfTmkpcqJmc4bxc=</InverseQ><D>FBGzbq+mIZ6K0E8TMHL3cdDsMBZIRvrmRPT52byod8jamtWcd7j+h8uvSifa4ms5LPcmJdFnF7vyc8fGJImMdoL4vz3O19aANekDkuNXtZ7Ru5PQ+O0Ve/FCuZ/AgbwOGGILZC2x2dljiaahOQXQce1F3A68Juoox65Yye5OqNtOmtx0AHBbcO+rpUrJK3BC86hF1br88MQqudhjccq/0ViVncpdt2YkZ5Ny2yEtm3S0oVJ8u9QQ7sQ8oFnVlwHxGH41z8E7/oQYWSKfcDcVjlNp42jim2q1ToKhS56VraytIadUj9g3gIaSxifyXS2nzC2Enm8tkH2NpAnFfMyOtQ==</D></RSAKeyValue>",
"RsaPublicKey": "<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"
}
}
+68
View File
@@ -0,0 +1,68 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
//old connection strings
//"DefaultConnection": "server=187.127.102.190;port=3306;database=hexafitnessauth;user=dbuser;password=dbpass"
//"DefaultConnection": "server=localhost;port=3306;database=authhex;user=root;password="
"DefaultConnection": "server=187.127.102.190;port=3306;database=distibutionauth;user=poojathmi;password=poojathmi@hexdive.com"
},
"AllowedHosts": "*",
"AppSettings": {
"FrontendUrl": "http://localhost:3000"
},
"Kestrel": {
"Endpoints": {
"Http": {
"Url": "http://0.0.0.0:6703"
}
},
"AllowedOrigins": [ "*" ]
},
"Cors": {
"AllowedOrigins": [ "*" ],
"AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ],
"AllowedHeaders": [ "*" ]
},
"Jwt": {
"Issuer": "AuthHex",
"Audience": "AuthHexClient",
"ExpiresInMinutes": "1000",
"RsaPrivateKey": "<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent><P>8eD+LRKe1Rh+mLlSxxxj0UmifzS5dlpjtwlmtbRSjk8/WCiMiDHVUqXv3ga4HM9/hbYVyLrq9BOrYANA+itxCI1mZY5+o3PAzowuHuqrEvMHayu/s9U9uvZx54+d/SJEzLeu6uV9/vDDc8KHBtXvRx4LoGXteI4o7QqwWjIKjeM=</P><Q>4SSSnceuHQ9oKCBI8vQuivn7sPW3JdfPSUTiqcWX5+ZWUPv1akopsyqdzunDCf9uNjlSd71HW9TEJHfE8F45bJlsuKzBXnh2g1OsrrmLHumOOsBjvrhCxPltNC2wJC9kIPBuiK0c/U3KL1VC6X98rr2i7sKVAv4nYM7dZtprCXc=</Q><DP>oXYzthrL9CwZthFf9JI6/6ZL1uZ3N043aiPYcEsjIOQ9wLewt+msITOr32ZU2MaatcNK32gHF09aAwmpAlalabQaOojYeHv4pOhmkTTNZiTdOulFWuJqBrgkaRMxv5x0mMO0/BCd/uTxVADy6dk9lRe1YyknUGZ1Y2bTOiJVcUk=</DP><DQ>tRBWoXGPQ2u6llqwkEN0kuxMUJqr+lE/MWum5mY0rpl93Y9kZWDTfHXPiDJTWt0D47Ph8M5WbzIDtFhmF/GPORmCaFr3AGTc2u0WOUaa7fdjHTjQfvdtK0B1ZLtBpzg9zIfQPTcL02MWMU7eiy6uNVHpkR8H++ot/Rsgz7Dk2C0=</DQ><InverseQ>GJGZYaPDiE+QeGE1ZrFF4vU+HmOJxG8/+Mxgi5DzPTOSJ9eVjPcXJWeJdC6P1NewWzWk33wlKQ6WOpm78WfTRWIeh9udI3IFqxaW1qs/0fu3ogrF7C5uAaLEqJlLEXAC+Buj2bpLlQlC/qOIWiYJOmYzyQGALfTmkpcqJmc4bxc=</InverseQ><D>FBGzbq+mIZ6K0E8TMHL3cdDsMBZIRvrmRPT52byod8jamtWcd7j+h8uvSifa4ms5LPcmJdFnF7vyc8fGJImMdoL4vz3O19aANekDkuNXtZ7Ru5PQ+O0Ve/FCuZ/AgbwOGGILZC2x2dljiaahOQXQce1F3A68Juoox65Yye5OqNtOmtx0AHBbcO+rpUrJK3BC86hF1br88MQqudhjccq/0ViVncpdt2YkZ5Ny2yEtm3S0oVJ8u9QQ7sQ8oFnVlwHxGH41z8E7/oQYWSKfcDcVjlNp42jim2q1ToKhS56VraytIadUj9g3gIaSxifyXS2nzC2Enm8tkH2NpAnFfMyOtQ==</D></RSAKeyValue>",
"RsaPublicKey": "<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"
},
"EmailSettings": {
"SmtpServer": "smtp.hostinger.com",
"SmtpPort": 587,
"SmtpUsername": "tester@hexdive.com",
"SenderEmail": "tester@hexdive.com",
"SenderPassword": "Aa@hexdive1",
"SenderName": "Gym Os"
},
"Sms": {
"ApiUrl": "https://backup.introps.com/Sms_common/send_sms",
"ApiKey": "8989898989"
},
"SmtpSettings": {
"SmtpServer": "smtp.hostinger.com",
"SmtpPort": 587,
"SmtpUsername": "tester@hexdive.com",
"SmtpPassword": "Aa@hexdive1",
"SenderEmail": "tester@hexdive.com",
"SenderName": "Gym Os",
"SmsServiceUrl": "https://backup.introps.com/Sms_common/send_sms",
"SenderPhone": "0787731643",
"SmsApiKey": "8989898989"
},
"GoogleAuthenticator": {
"Issuer": "AuthHex",
"SecretKeyLength": 32,
"TimeWindowSeconds": 30,
"ValidationWindowSize": 1,
"QrCodeSize": 200
}
}
+2
View File
@@ -0,0 +1,2 @@
PRIVATE=<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent><P>8eD+LRKe1Rh+mLlSxxxj0UmifzS5dlpjtwlmtbRSjk8/WCiMiDHVUqXv3ga4HM9/hbYVyLrq9BOrYANA+itxCI1mZY5+o3PAzowuHuqrEvMHayu/s9U9uvZx54+d/SJEzLeu6uV9/vDDc8KHBtXvRx4LoGXteI4o7QqwWjIKjeM=</P><Q>4SSSnceuHQ9oKCBI8vQuivn7sPW3JdfPSUTiqcWX5+ZWUPv1akopsyqdzunDCf9uNjlSd71HW9TEJHfE8F45bJlsuKzBXnh2g1OsrrmLHumOOsBjvrhCxPltNC2wJC9kIPBuiK0c/U3KL1VC6X98rr2i7sKVAv4nYM7dZtprCXc=</Q><DP>oXYzthrL9CwZthFf9JI6/6ZL1uZ3N043aiPYcEsjIOQ9wLewt+msITOr32ZU2MaatcNK32gHF09aAwmpAlalabQaOojYeHv4pOhmkTTNZiTdOulFWuJqBrgkaRMxv5x0mMO0/BCd/uTxVADy6dk9lRe1YyknUGZ1Y2bTOiJVcUk=</DP><DQ>tRBWoXGPQ2u6llqwkEN0kuxMUJqr+lE/MWum5mY0rpl93Y9kZWDTfHXPiDJTWt0D47Ph8M5WbzIDtFhmF/GPORmCaFr3AGTc2u0WOUaa7fdjHTjQfvdtK0B1ZLtBpzg9zIfQPTcL02MWMU7eiy6uNVHpkR8H++ot/Rsgz7Dk2C0=</DQ><InverseQ>GJGZYaPDiE+QeGE1ZrFF4vU+HmOJxG8/+Mxgi5DzPTOSJ9eVjPcXJWeJdC6P1NewWzWk33wlKQ6WOpm78WfTRWIeh9udI3IFqxaW1qs/0fu3ogrF7C5uAaLEqJlLEXAC+Buj2bpLlQlC/qOIWiYJOmYzyQGALfTmkpcqJmc4bxc=</InverseQ><D>FBGzbq+mIZ6K0E8TMHL3cdDsMBZIRvrmRPT52byod8jamtWcd7j+h8uvSifa4ms5LPcmJdFnF7vyc8fGJImMdoL4vz3O19aANekDkuNXtZ7Ru5PQ+O0Ve/FCuZ/AgbwOGGILZC2x2dljiaahOQXQce1F3A68Juoox65Yye5OqNtOmtx0AHBbcO+rpUrJK3BC86hF1br88MQqudhjccq/0ViVncpdt2YkZ5Ny2yEtm3S0oVJ8u9QQ7sQ8oFnVlwHxGH41z8E7/oQYWSKfcDcVjlNp42jim2q1ToKhS56VraytIadUj9g3gIaSxifyXS2nzC2Enm8tkH2NpAnFfMyOtQ==</D></RSAKeyValue>
PUBLIC=<RSAKeyValue><Modulus>1LlNkMBQNdpXJiDal7XMxkG/3ad+YBsMCuY9JD/abHMzniFXtQlovjfbeaaHJ0v1kvSo9731CJ0YC1qhPU5rPQwZwxOWZ9BOBZlMDghONdjOH/HyCUbb5Z18ibqc0QenFSnEYz+jkVZiayj8DV/+VUe+eKzpQTlU6aWtHvlbwfuXaDu+QvFlpLJ7/m8na+0s2nYhLX8Wfi4C/2AoNaYhFkIwYhMMGoaSHuIoQ5R6181Rh0gKvYopRW+IpTD5RV8bXV3AM6zOcoisOifBYROHwA5ZZpoHXuTvHYmPWW8kL8PKme7BwBldPi8KrJUroRE+WXA87aAA5Wtt1oxePcXvhQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>