This commit is contained in:
Dhananjaya99
2026-07-08 11:11:50 +05:30
parent 2cd2741f77
commit de7b40a147
51 changed files with 6839 additions and 0 deletions
+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
+330
View File
@@ -0,0 +1,330 @@
# 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 |
### 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)"
}
```
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": "" }
}
```
### 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" }
}
}
```
---
## 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 currently commented out in `loginUser` — passwords are not checked on login as of this version.
- JWT access tokens issued with `expiresIn: 3600` (1 hour); refresh tokens/sessions expire after 30 days.
+116
View File
@@ -0,0 +1,116 @@
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.UserType> UserType => Set<Models.UserType>();
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 UserType primary key
modelBuilder.Entity<Models.UserType>()
.HasKey(ut => ut.UserTypeId);
// Configure User primary key and relationships
modelBuilder.Entity<Models.User>()
.HasKey(u => u.Id);
modelBuilder.Entity<Models.User>()
.HasOne(u => u.Role)
.WithMany()
.HasForeignKey(u => u.RoleId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<Models.User>()
.HasOne(u => u.UserType)
.WithMany()
.HasForeignKey(u => u.UserTypeId)
.OnDelete(DeleteBehavior.Restrict);
}
}
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": [
""
]
}
}
]
}
+79
View File
@@ -0,0 +1,79 @@
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) : ControllerBase
{
private readonly UserManager _userManager = userManager;
private readonly RecoveryManager _recoveryManager = recoveryManager;
private readonly AltOptionManager _altOptionManager = altOptionManager;
[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);
}
}
}
+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);
}
}
+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, Guid? userTypeId);
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; }
}
}
+44
View File
@@ -0,0 +1,44 @@
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 Guid RoleId { get; set; }
public Role? Role { get; set; }
public Guid UserTypeId { get; set; }
public UserType? UserType { 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;
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace AuthHex.Models
{
public class UserType
{
public Guid UserTypeId { get; set; }
public string Code { get; set; } = string.Empty; // INDIVIDUAL, MERCHANT, AGENT
public string? Description { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
+126
View File
@@ -0,0 +1,126 @@
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<UserHelper>();
builder.Services.AddScoped<UserManagerService>();
builder.Services.AddScoped<RecoveryManagerService>();
builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>();
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>();
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);
}
}
}
+183
View File
@@ -0,0 +1,183 @@
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, Guid? userTypeId)
{
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
.Include(u => u.Role)
.Include(u => u.UserType)
.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,496 @@
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, null);
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, null);
}
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.RoleId,
UserTypeId = user.UserTypeId
}
}
};
}
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
};
}
}
}
}
+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, null);
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}";
}
}
File diff suppressed because it is too large Load Diff
+101
View File
@@ -0,0 +1,101 @@
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)
{
// Load user's role and usertype if not already loaded
if (user.Role == null)
{
await _dbContext.Entry(user)
.Reference(u => u.Role)
.LoadAsync();
}
if (user.UserType == null)
{
await _dbContext.Entry(user)
.Reference(u => u.UserType)
.LoadAsync();
}
var claims = new List<Claim>
{
new Claim("UserId", user.Id.ToString()),
new Claim("UserTypeId", user.UserTypeId.ToString()),
new Claim("UserTypeCode", user.UserType?.Code ?? ""),
new Claim("RoleId", user.RoleId.ToString()),
new Claim("RoleCode", user.Role?.Code ?? ""),
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)));
}
}
}
+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>