ini
This commit is contained in:
+22
@@ -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
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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();
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
using AuthHex.Models;
|
||||||
|
using AuthHex.Interfaces;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace AuthHex.Repos
|
||||||
|
{
|
||||||
|
public class AltOptionManagerRepository : IAltOptionManagerRepository
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user