first commit
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,383 @@
|
||||
# AuthHex API Documentation
|
||||
|
||||
Base route: `api`
|
||||
All endpoints are `[AllowAnonymous]` at the HTTP layer, but individual functions may require a Bearer JWT (read via `HttpContext` claims) — noted per function below.
|
||||
|
||||
## Common envelope
|
||||
|
||||
**Request** (`ApiRequest`)
|
||||
```json
|
||||
{
|
||||
"functionName": "string (required, ignored for dedicated routes below)",
|
||||
"payload": { "...": "function-specific object" },
|
||||
"reference": "string (required, can be empty)"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (`ApiResponse`)
|
||||
```json
|
||||
{
|
||||
"statusCode": 200,
|
||||
"success": true,
|
||||
"message": "success",
|
||||
"data": { "...": "function-specific object" }
|
||||
}
|
||||
```
|
||||
Errors: `404` unknown function, `400` (`KeyNotFoundException`/`InvalidOperationException`), `500` (`ArgumentException`).
|
||||
|
||||
---
|
||||
|
||||
## Routes
|
||||
|
||||
| Route | Method | Dispatches to | FunctionName |
|
||||
|---|---|---|---|
|
||||
| `/api/status` | GET | — | — (health check) |
|
||||
| `/api/user` | POST | UserManager | from body |
|
||||
| `/api/recovery` | POST | RecoveryManager | from body |
|
||||
| `/api/registerUser` | POST | UserManager | forced `registerUser` |
|
||||
| `/api/loginUser` | POST | UserManager | forced `loginUser` |
|
||||
| `/api/forgotPassword` | POST | RecoveryManager | forced `forgotPassword` |
|
||||
| `/api/alt` | POST | AltOptionManager | from body |
|
||||
| `/api/role` | POST | RoleManager | from body |
|
||||
|
||||
### GET /api/status
|
||||
Response:
|
||||
```json
|
||||
{ "status": "API is running", "timestamp": "2026-07-03T00:00:00Z" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## /api/user — UserManager functions
|
||||
|
||||
### registerUser
|
||||
Payload:
|
||||
```json
|
||||
{
|
||||
"userId": "guid (required)",
|
||||
"fullname": "string?",
|
||||
"userName": "string?",
|
||||
"nic": "string?",
|
||||
"email": "string?",
|
||||
"mobileNumber": "string?",
|
||||
"deviceName": "string? (default 'Unknown Device')",
|
||||
"roleId": "guid (required)",
|
||||
"userTypeId": "guid (required)",
|
||||
"chkUser": "bool? (if true, checks for existing conflicting user)",
|
||||
"password": "string? (auto-generated if empty)"
|
||||
}
|
||||
```
|
||||
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 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Admin user management (userId-targeted)
|
||||
These act on an explicit `userId` in the payload (unlike `ChangeUserStatus`/`LockUserAccount`/`UpdateUser`,
|
||||
which act on the token's own user). Used by ERP-core's `UsersController`.
|
||||
|
||||
#### listUsers
|
||||
Payload: `{ "page": 1, "pageSize": 20, "q": "search?" }`. Data: `{ "items": [ { id, fullName, userName, email, mobileNumber, nic, isActive, isLocked, createdAt, roleId, roleCode, roleName, userTypeId, userTypeCode } ], "total": N, "page": 1, "pageSize": 20 }`.
|
||||
|
||||
#### adminUpdateUser
|
||||
Payload: `{ "userId": "guid (required)", "fullName?", "userName?", "nic?", "email?", "mobileNumber?", "address?", "roleId?", "userTypeId?", "newPassword?" }` — only keys present are applied. Data: the updated user summary.
|
||||
|
||||
#### adminSetStatus
|
||||
Payload: `{ "userId": "guid (required)", "isActive": true|false }`. Deactivating also invalidates the user's sessions. Data: `{ id, isActive }`.
|
||||
|
||||
#### adminSetLock
|
||||
Payload: `{ "userId": "guid (required)", "isLocked": true|false }`. Locking also invalidates the user's sessions. Data: `{ id, isLocked }`.
|
||||
|
||||
#### listUserTypes
|
||||
Payload: `{}`. Data: `[ { userTypeId, code, description } ]`. (Seeded at startup: `ERP_ADMIN`, `ERP_STAFF`.)
|
||||
|
||||
## /api/recovery — RecoveryManager functions
|
||||
|
||||
### forgotPassword
|
||||
Payload:
|
||||
```json
|
||||
{ "identifier": "string (required)", "useResetLink": "bool? (default false)", "numberOfDigits": "int? (default 6)" }
|
||||
```
|
||||
Data (useResetLink = true):
|
||||
```json
|
||||
{ "success": true, "message": "Password reset link sent to your email.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "ResetLink" } }
|
||||
```
|
||||
Data (useResetLink = false, OTP flow):
|
||||
```json
|
||||
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP" } }
|
||||
```
|
||||
|
||||
### verifyOTP
|
||||
Payload:
|
||||
```json
|
||||
{ "referenceNumber": "string (required)", "otpCode": "string (required)" }
|
||||
```
|
||||
Data:
|
||||
```json
|
||||
{ "success": true, "message": "OTP verified successfully. You can now reset your password.", "data": { "referenceNumber": "", "userId": "guid", "verified": true } }
|
||||
```
|
||||
|
||||
### resetPasswordWithToken
|
||||
Payload:
|
||||
```json
|
||||
{ "resetToken": "string (required)", "newPassword": "string (required, min 8 chars)", "confirmPassword": "string (required, must match)" }
|
||||
```
|
||||
Data:
|
||||
```json
|
||||
{ "success": true, "message": "Password reset successful. Please login with your new password.", "data": { "userId": "guid", "email": "", "resetAt": "" } }
|
||||
```
|
||||
|
||||
### resetPassword
|
||||
Payload:
|
||||
```json
|
||||
{ "referenceNumber": "string (required)", "newPassword": "string (required, min 8 chars)", "confirmPassword": "string (required, must match)" }
|
||||
```
|
||||
(Requires recovery status = "Verified" via prior `verifyOTP` call.)
|
||||
Data: same shape as `resetPasswordWithToken`.
|
||||
|
||||
---
|
||||
|
||||
## /api/alt — AltOptionManager functions
|
||||
|
||||
### IsAvailable
|
||||
Payload:
|
||||
```json
|
||||
{ "Identifier": "string?", "Recovery": "string? (any non-null value flags recovery-mode)" }
|
||||
```
|
||||
Data (available): `{ "isAvailable": true, "message": "Identifier is available" }`
|
||||
Data (taken, no Recovery): `{ "isAvailable": false, "message": "Identifier already in use" }`
|
||||
Data (taken, Recovery set): `{ "existingUsers": [...] }`
|
||||
|
||||
### sendOtp
|
||||
Payload:
|
||||
```json
|
||||
{ "identifier": "string (required, email or mobile)", "numberOfDigits": "int? (default 4)", "newUser": "bool? (default false)" }
|
||||
```
|
||||
Data (newUser = true):
|
||||
```json
|
||||
{ "success": true, "message": "OTP sent successfully for new user.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR NEW USER" } }
|
||||
```
|
||||
Data (existing user):
|
||||
```json
|
||||
{ "success": true, "message": "OTP sent successfully.", "data": { "referenceNumber": "", "expiresAt": "", "recoveryType": "OTP FOR LOGIN" } }
|
||||
```
|
||||
|
||||
### VerifyOTP (alt)
|
||||
Payload:
|
||||
```json
|
||||
{
|
||||
"userId": "guid?", "referenceNumber": "string (required)", "otpCode": "string (required)",
|
||||
"newUser": "bool? (default false)", "identifier": "string? (email/mobile/other — marks it verified on user)",
|
||||
"deviceName": "string? (default 'Unknown Device')"
|
||||
}
|
||||
```
|
||||
Data:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "OTP verified successfully.",
|
||||
"data": {
|
||||
"referenceNumber": "", "userId": "guid", "verified": true,
|
||||
"accessToken": "jwt", "refreshToken": "", "expiresIn": 3600,
|
||||
"user": { "id", "fullName", "userName", "email", "mobileNumber", "emailVerified", "mobileNumberVerified", "isMfaEnabled", "roleId", "userTypeId" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## /api/role — RoleManager functions
|
||||
|
||||
AuthHex owns only the `Role` lookup table (identity source for the JWT `RoleId`/`RoleCode` claims).
|
||||
Roles are authored in the main system (**ERP-core**) and cloned here via `createRole` using the **same
|
||||
`RoleId`** so the two role stores stay in sync. This route is currently `AllowAnonymous` (server-to-server
|
||||
call from ERP-core); tighten it with a service credential when dual-auth is introduced.
|
||||
|
||||
### createRole
|
||||
Clone a role created in ERP-core. Accepts the caller-supplied `roleId` so the AuthHex row matches.
|
||||
Payload:
|
||||
```json
|
||||
{ "roleId": "guid (required)", "code": "string (required, unique)", "name": "string?", "isSystemRole": "bool?" }
|
||||
```
|
||||
Data:
|
||||
```json
|
||||
{ "roleId": "guid", "code": "ADMIN", "name": "Administrator", "isSystemRole": false, "createdAt": "2026-07-16T00:00:00Z" }
|
||||
```
|
||||
Errors: duplicate `roleId` or `code` → `409`; missing/invalid `roleId`/`code` → `400`.
|
||||
|
||||
### listRoles
|
||||
Payload: `{}` (none). Data: array of the role object shown above.
|
||||
|
||||
### getRole
|
||||
Payload: `{ "roleId": "guid (required)" }`. Data: the role object above; unknown id → `404`.
|
||||
|
||||
### deleteRole
|
||||
Payload: `{ "roleId": "guid (required)" }`. Deletes the role. Blocked with `409` if any user is still assigned
|
||||
(the `User.RoleId` FK is Restrict) so ERP-core — which calls this before removing its own copy — never orphans an
|
||||
in-use role. Data: `{ "roleId": "guid", "deleted": true }`; unknown id → `404`.
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
- All timestamps are UTC.
|
||||
- `deviceName`, `Browser`, `OS`, `IPAddress` are captured per session from request headers for session tracking (`getUserSessions`).
|
||||
- Password login validation is **active** — `loginUser` verifies the BCrypt hash via `PasswordHasher.Verify` and throws `Invalid credentials` on mismatch. Login identifiers are **email / mobile / NIC** (not username). Register with an explicit `password` (a blank one is randomized → unusable).
|
||||
- **Bootstrap seed** (idempotent, at startup — `Infra/Seed/AuthHexSeeder.cs`): user types `ERP_ADMIN`/`ERP_STAFF`, an `ADMIN` role (fixed GUID `11111111-…`, mirrored by ERP-core), and an admin user (`Bootstrap:AdminEmail` default `admin@erp.local` / `Bootstrap:AdminPassword` default `Admin@123`).
|
||||
- JWT access tokens issued with `expiresIn: 3600` (1 hour); refresh tokens/sessions expire after 30 days.
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
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 env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development";
|
||||
var config = new ConfigurationBuilder()
|
||||
.SetBasePath(Directory.GetCurrentDirectory())
|
||||
.AddJsonFile("appsettings.json", optional: false)
|
||||
.AddJsonFile($"appsettings.{env}.json", optional: true)
|
||||
.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,91 @@
|
||||
using AuthHex.Models.IOs;
|
||||
using AuthHex.Services.AltOptionManager;
|
||||
using AuthHex.Services.FunctionHandler;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace AuthHex.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
[EnableCors("AllowAll")]
|
||||
public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager, RoleManager roleManager) : ControllerBase
|
||||
{
|
||||
private readonly UserManager _userManager = userManager;
|
||||
private readonly RecoveryManager _recoveryManager = recoveryManager;
|
||||
private readonly AltOptionManager _altOptionManager = altOptionManager;
|
||||
private readonly RoleManager _roleManager = roleManager;
|
||||
|
||||
[HttpGet("status")]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetStatus()
|
||||
{
|
||||
return Ok(new { status = "API is running", timestamp = System.DateTime.UtcNow });
|
||||
}
|
||||
|
||||
[HttpPost("user")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> UserExecute([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _userManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("recovery")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RecoveryExecute([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _recoveryManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("registerUser")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RegisterUser([FromBody] ApiRequest request)
|
||||
{
|
||||
request.FunctionName = "registerUser";
|
||||
var response = await _userManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("loginUser")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> LoginUser([FromBody] ApiRequest request)
|
||||
{
|
||||
request.FunctionName = "loginUser";
|
||||
var response = await _userManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("forgotPassword")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> ForgotPassword([FromBody] ApiRequest request)
|
||||
{
|
||||
request.FunctionName = "forgotPassword";
|
||||
var response = await _recoveryManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
[HttpPost("alt")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> IsAvailable([FromBody] ApiRequest request)
|
||||
{
|
||||
|
||||
var response = await _altOptionManager.Execute(request, HttpContext);
|
||||
return StatusCode(response.StatusCode, response);
|
||||
}
|
||||
|
||||
// Role dispatcher. Roles are authored in ERP-core and cloned here via `createRole`
|
||||
// (same RoleId) so AuthHex's Roles table stays in sync. Server-to-server call, so
|
||||
// AllowAnonymous for now — tighten to a service credential when dual-auth lands.
|
||||
[HttpPost("role")]
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> RoleExecute([FromBody] ApiRequest request)
|
||||
{
|
||||
var response = await _roleManager.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;
|
||||
//comment
|
||||
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,85 @@
|
||||
using AuthHex.Models;
|
||||
using AuthHex.Utility.passwordHasher;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AuthHex.Infra.Seed
|
||||
{
|
||||
/// <summary>
|
||||
/// Idempotent bootstrap seed so the system is usable on a fresh database:
|
||||
/// two UserTypes, one ADMIN Role (fixed well-known GUID, mirrored by ERP-core's
|
||||
/// DataSeeder so the two role stores line up), and one admin User to log in with.
|
||||
/// Admin credentials are config-overridable (Bootstrap:AdminEmail / :AdminPassword).
|
||||
/// </summary>
|
||||
public static class AuthHexSeeder
|
||||
{
|
||||
// Fixed GUIDs shared with ERP-core (ERPCore DataSeeder seeds the same ADMIN RoleId).
|
||||
public static readonly Guid AdminRoleId = Guid.Parse("11111111-1111-1111-1111-111111111111");
|
||||
public static readonly Guid ErpAdminUserTypeId = Guid.Parse("21111111-1111-1111-1111-111111111111");
|
||||
public static readonly Guid ErpStaffUserTypeId = Guid.Parse("22222222-2222-2222-2222-222222222222");
|
||||
public static readonly Guid AdminUserId = Guid.Parse("a1111111-1111-1111-1111-111111111111");
|
||||
|
||||
// core-api hardcodes these exact GUIDs when it registers a new Customer/Client/Staff
|
||||
// user via this service's registerUser endpoint (see Fitness_core_BE ClientService.cs
|
||||
// / StaffService.cs) -- they must exist here as real Role/UserType rows or that
|
||||
// registration call fails its FK check. UserRoleHelper.GetUserType in core-api doesn't
|
||||
// actually read these Code strings for authorization (it goes by which central table --
|
||||
// Admin/Customer/Client/Staff -- has a row for the user), so only the GUIDs themselves
|
||||
// are load-bearing; the Code/Name text is just for readability.
|
||||
public static readonly Guid CustomerUserTypeId = Guid.Parse("00000000-0000-0000-0000-000000000001");
|
||||
public static readonly Guid ClientUserTypeId = Guid.Parse("00000000-0000-0000-0000-000000000002");
|
||||
public static readonly Guid StaffUserTypeId = Guid.Parse("00000000-0000-0000-0000-000000000003");
|
||||
public static readonly Guid CustomerRoleId = Guid.Parse("08de6a11-9e9f-4401-8a10-685cc57b41ed");
|
||||
public static readonly Guid ClientRoleId = Guid.Parse("08de6a11-9e9f-4401-8a10-6859860b41ed");
|
||||
public static readonly Guid StaffRoleId = Guid.Parse("08de6a11-9e9f-4401-8a10-6854567b41ed");
|
||||
|
||||
public static async Task SeedAsync(AppDBContext db, IConfiguration config, CancellationToken ct = default)
|
||||
{
|
||||
// --- UserTypes ---
|
||||
if (!await db.UserType.AnyAsync(t => t.UserTypeId == ErpAdminUserTypeId, ct))
|
||||
db.UserType.Add(new UserType { UserTypeId = ErpAdminUserTypeId, Code = "ERP_ADMIN", Description = "ERP administrator", CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.UserType.AnyAsync(t => t.UserTypeId == ErpStaffUserTypeId, ct))
|
||||
db.UserType.Add(new UserType { UserTypeId = ErpStaffUserTypeId, Code = "ERP_STAFF", Description = "ERP staff", CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.UserType.AnyAsync(t => t.UserTypeId == CustomerUserTypeId, ct))
|
||||
db.UserType.Add(new UserType { UserTypeId = CustomerUserTypeId, Code = "Customer", Description = "Gym member", CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.UserType.AnyAsync(t => t.UserTypeId == ClientUserTypeId, ct))
|
||||
db.UserType.Add(new UserType { UserTypeId = ClientUserTypeId, Code = "Client", Description = "Club owner", CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.UserType.AnyAsync(t => t.UserTypeId == StaffUserTypeId, ct))
|
||||
db.UserType.Add(new UserType { UserTypeId = StaffUserTypeId, Code = "Staff", Description = "Club staff", CreatedAt = DateTime.UtcNow });
|
||||
|
||||
// --- ADMIN role (same RoleId ERP-core seeds) ---
|
||||
if (!await db.Roles.AnyAsync(r => r.RoleId == AdminRoleId, ct))
|
||||
db.Roles.Add(new Role { RoleId = AdminRoleId, Code = "ADMIN", Name = "Administrator", IsSystemRole = true, CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.Roles.AnyAsync(r => r.RoleId == CustomerRoleId, ct))
|
||||
db.Roles.Add(new Role { RoleId = CustomerRoleId, Code = "CUSTOMER", Name = "Customer", IsSystemRole = true, CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.Roles.AnyAsync(r => r.RoleId == ClientRoleId, ct))
|
||||
db.Roles.Add(new Role { RoleId = ClientRoleId, Code = "CLIENT", Name = "Client", IsSystemRole = true, CreatedAt = DateTime.UtcNow });
|
||||
if (!await db.Roles.AnyAsync(r => r.RoleId == StaffRoleId, ct))
|
||||
db.Roles.Add(new Role { RoleId = StaffRoleId, Code = "STAFF", Name = "Staff", IsSystemRole = true, CreatedAt = DateTime.UtcNow });
|
||||
|
||||
await db.SaveChangesAsync(ct);
|
||||
|
||||
// --- Admin user (after role/type exist for the FK) ---
|
||||
var adminEmail = config["Bootstrap:AdminEmail"] ?? "admin@erp.local";
|
||||
var adminPassword = config["Bootstrap:AdminPassword"] ?? "Admin@123";
|
||||
|
||||
if (!await db.Users.AnyAsync(u => u.Id == AdminUserId, ct))
|
||||
{
|
||||
db.Users.Add(new User
|
||||
{
|
||||
Id = AdminUserId,
|
||||
FullName = "ERP Administrator",
|
||||
UserName = "admin",
|
||||
Email = adminEmail,
|
||||
EmailVerified = true,
|
||||
PasswordHash = PasswordHasher.Hash(adminPassword),
|
||||
RoleId = AdminRoleId,
|
||||
UserTypeId = ErpAdminUserTypeId,
|
||||
IsActive = true,
|
||||
IsLocked = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
});
|
||||
await db.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,14 @@
|
||||
using AuthHex.Models;
|
||||
|
||||
namespace AuthHex.Interfaces
|
||||
{
|
||||
public interface IRoleRepository
|
||||
{
|
||||
Task<Role> AddRoleAsync(Role role, CancellationToken ct = default);
|
||||
Task<Role?> GetByIdAsync(Guid roleId, CancellationToken ct = default);
|
||||
Task<Role?> GetByCodeAsync(string code, CancellationToken ct = default);
|
||||
Task<List<Role>> ListAsync(CancellationToken ct = default);
|
||||
Task<bool> IsAssignedToAnyUserAsync(Guid roleId, CancellationToken ct = default);
|
||||
Task DeleteRoleAsync(Role role, 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,16 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace AuthHex.Models
|
||||
{
|
||||
public class SysConfig
|
||||
{
|
||||
[Key]
|
||||
public int SysConfigId { 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;
|
||||
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
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 .....
|
||||
// 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<IRoleRepository, RoleRepository>();
|
||||
builder.Services.AddScoped<UserHelper>();
|
||||
builder.Services.AddScoped<UserManagerService>();
|
||||
builder.Services.AddScoped<RecoveryManagerService>();
|
||||
builder.Services.AddScoped<AuthHex.Services.AltOptionManager.AltOptionManagerService>();
|
||||
builder.Services.AddScoped<AuthHex.Services.RoleManager.RoleManagerService>();
|
||||
builder.Services.AddHttpClient<ThirdPartyService>();
|
||||
builder.Services.AddScoped<JwtTokenHelper>();
|
||||
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.UserManager>();
|
||||
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RecoveryManager>();
|
||||
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.AltOptionManager>();
|
||||
builder.Services.AddScoped<AuthHex.Services.FunctionHandler.RoleManager>();
|
||||
|
||||
var configuredOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? Array.Empty<string>();
|
||||
var allowedOrigins = configuredOrigins
|
||||
.Where(origin => !string.IsNullOrWhiteSpace(origin) && origin != "*")
|
||||
.ToList();
|
||||
|
||||
var frontendUrl = builder.Configuration["AppSettings:FrontendUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(frontendUrl) &&
|
||||
!allowedOrigins.Contains(frontendUrl, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
allowedOrigins.Add(frontendUrl);
|
||||
}
|
||||
|
||||
// Add CORS policy
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("AllowAll", policy =>
|
||||
{
|
||||
if (allowedOrigins.Count > 0)
|
||||
{
|
||||
policy.WithOrigins(allowedOrigins.ToArray())
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader()
|
||||
.AllowCredentials();
|
||||
return;
|
||||
}
|
||||
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Idempotent bootstrap seed (UserTypes, ADMIN role, admin user) so a fresh DB is usable.
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDBContext>();
|
||||
await AuthHex.Infra.Seed.AuthHexSeeder.SeedAsync(db, app.Configuration);
|
||||
}
|
||||
|
||||
// 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,53 @@
|
||||
using AuthHex.Interfaces;
|
||||
using AuthHex.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace AuthHex.Repos
|
||||
{
|
||||
public class RoleRepository : IRoleRepository
|
||||
{
|
||||
private readonly AppDBContext _dbContext;
|
||||
|
||||
public RoleRepository(AppDBContext dbContext)
|
||||
{
|
||||
_dbContext = dbContext;
|
||||
}
|
||||
|
||||
public Task<Role> AddRoleAsync(Role role, CancellationToken ct = default)
|
||||
{
|
||||
// SaveChanges is deferred to the Unit of Work commit, mirroring UserManageRepository.
|
||||
_dbContext.Roles.Add(role);
|
||||
return Task.FromResult(role);
|
||||
}
|
||||
|
||||
public async Task<Role?> GetByIdAsync(Guid roleId, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Roles.FirstOrDefaultAsync(r => r.RoleId == roleId, ct);
|
||||
}
|
||||
|
||||
public async Task<Role?> GetByCodeAsync(string code, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Roles.FirstOrDefaultAsync(r => r.Code == code, ct);
|
||||
}
|
||||
|
||||
public async Task<List<Role>> ListAsync(CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Roles
|
||||
.AsNoTracking()
|
||||
.OrderBy(r => r.Code)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<bool> IsAssignedToAnyUserAsync(Guid roleId, CancellationToken ct = default)
|
||||
{
|
||||
return await _dbContext.Users.AnyAsync(u => u.RoleId == roleId, ct);
|
||||
}
|
||||
|
||||
public Task DeleteRoleAsync(Role role, CancellationToken ct = default)
|
||||
{
|
||||
// SaveChanges is deferred to the Unit of Work commit.
|
||||
_dbContext.Roles.Remove(role);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,527 @@
|
||||
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();
|
||||
|
||||
// Send to exactly the channel the caller selected (identifier), not every channel
|
||||
// the user happens to have on file -- picking "Send OTP to Email" must not also
|
||||
// fire off an SMS, and vice versa.
|
||||
if (IsEmail(identifier) && !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>
|
||||
"
|
||||
);
|
||||
}
|
||||
else if (IsMobileNumber(identifier) && !string.IsNullOrWhiteSpace(user.MobileNumber))
|
||||
{
|
||||
await _thirdPartyService.SendSmsAsync(
|
||||
user.MobileNumber,
|
||||
otpCode,
|
||||
$"Your login OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
// identifier didn't look like either shape (e.g. looked up by username/NIC) --
|
||||
// fall back to whichever contact channel the user actually has, preferring SMS.
|
||||
if (!string.IsNullOrWhiteSpace(user.MobileNumber))
|
||||
{
|
||||
await _thirdPartyService.SendSmsAsync(
|
||||
user.MobileNumber,
|
||||
otpCode,
|
||||
$"Your login OTP is {otpCode}. It expires in 10 minutes. Ref: {recovery.RecoveryReferenceNum}"
|
||||
);
|
||||
}
|
||||
else 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,71 @@
|
||||
using AuthHex.Models.IOs;
|
||||
using AuthHex.Services.RoleManager;
|
||||
|
||||
namespace AuthHex.Services.FunctionHandler
|
||||
{
|
||||
public class RoleManager
|
||||
{
|
||||
public readonly Dictionary<string, Func<object, HttpContext, Task<object>>> _functionHandler;
|
||||
|
||||
public RoleManager(RoleManagerService RoleManagerService)
|
||||
{
|
||||
_functionHandler = new()
|
||||
{
|
||||
{ "createRole", RoleManagerService.CreateRole },
|
||||
{ "listRoles", RoleManagerService.ListRoles },
|
||||
{ "getRole", RoleManagerService.GetRole },
|
||||
{ "deleteRole", RoleManagerService.DeleteRole }
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<ApiResponse> Execute(ApiRequest request, HttpContext httpContext)
|
||||
{
|
||||
if (!_functionHandler.TryGetValue(request.FunctionName, out var handler))
|
||||
{
|
||||
return new ApiResponse
|
||||
{
|
||||
StatusCode = 404,
|
||||
Message = $"Function '{request.FunctionName}' not found.",
|
||||
Data = null
|
||||
};
|
||||
}
|
||||
try
|
||||
{
|
||||
var results = await handler(request.Payload, httpContext);
|
||||
return new ApiResponse
|
||||
{
|
||||
StatusCode = 200,
|
||||
Message = "success",
|
||||
Data = results
|
||||
};
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
return new ApiResponse
|
||||
{
|
||||
StatusCode = 404,
|
||||
Message = ex.Message,
|
||||
Data = null
|
||||
};
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return new ApiResponse
|
||||
{
|
||||
StatusCode = 409,
|
||||
Message = ex.Message,
|
||||
Data = null
|
||||
};
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return new ApiResponse
|
||||
{
|
||||
StatusCode = 400,
|
||||
Message = ex.Message,
|
||||
Data = null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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},
|
||||
|
||||
// Admin (userId-targeted) user management — used by ERP-core UsersController
|
||||
{ "listUsers", UserManagerService.ListUsers },
|
||||
{ "adminUpdateUser", UserManagerService.AdminUpdateUser },
|
||||
{ "adminSetStatus", UserManagerService.AdminSetStatus },
|
||||
{ "adminSetLock", UserManagerService.AdminSetLock },
|
||||
{ "listUserTypes", UserManagerService.ListUserTypes },
|
||||
|
||||
// 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,585 @@
|
||||
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;
|
||||
|
||||
var welcome = data.ContainsKey("welcome") && data["welcome"].GetBoolean();
|
||||
|
||||
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);
|
||||
|
||||
// Welcome emails (new account creation, or an admin resending it) also set an
|
||||
// instant temporary password so the account is usable even if the link is lost
|
||||
// or its 15-minute window expires. The link still lets them pick their own
|
||||
// password; logging in with the temp password does not consume it.
|
||||
string? tempPassword = null;
|
||||
if (welcome)
|
||||
{
|
||||
tempPassword = GenerateTempPassword();
|
||||
user.PasswordHash = PasswordHasher.Hash(tempPassword);
|
||||
await _dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await _uow.CommitAsync();
|
||||
|
||||
// send reset link via email
|
||||
if (!string.IsNullOrWhiteSpace(user.Email))
|
||||
{
|
||||
var frontendUrl = (_configuration["AppSettings:FrontendUrl"] ?? "https://hexafit.hexdive.com").TrimEnd('/');
|
||||
var resetLink = $"{frontendUrl}/reset-password?token={resetToken}";
|
||||
|
||||
var subject = welcome ? "Welcome — set your password" : "Password Reset Request";
|
||||
var body = welcome
|
||||
? $@"
|
||||
<h2>Welcome to Hexa Fitness</h2>
|
||||
<p>Hello {user.FullName ?? user.UserName},</p>
|
||||
<p>An account has been created for you. You can log in right away with the temporary credentials below, or click the link to set your own password.</p>
|
||||
<p><strong>Email:</strong> {user.Email}<br/>
|
||||
<strong>Temporary password:</strong> {tempPassword}</p>
|
||||
<p><a href='{resetLink}'>Set Password</a></p>
|
||||
<p>The link will expire in 15 minutes. Please change your password after logging in.</p>
|
||||
<p><strong>Reference Number:</strong> {recovery.RecoveryReferenceNum}</p>
|
||||
"
|
||||
: $@"
|
||||
<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>
|
||||
";
|
||||
|
||||
await _thirdPartyService.EmailConfiguration(
|
||||
user.Email!,
|
||||
resetToken,
|
||||
subject,
|
||||
body);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// Excludes visually-ambiguous characters (0/O, 1/l/I) since this gets typed by hand.
|
||||
private string GenerateTempPassword(int length = 10)
|
||||
{
|
||||
const string upper = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
||||
const string lower = "abcdefghjkmnpqrstuvwxyz";
|
||||
const string digits = "23456789";
|
||||
const string special = "!@#$%";
|
||||
const string all = upper + lower + digits + special;
|
||||
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
var randomBytes = new byte[length];
|
||||
rng.GetBytes(randomBytes);
|
||||
|
||||
// Guarantee at least one char from each class, then fill the rest randomly.
|
||||
var chars = new List<char>
|
||||
{
|
||||
upper[randomBytes[0] % upper.Length],
|
||||
lower[randomBytes[1] % lower.Length],
|
||||
digits[randomBytes[2] % digits.Length],
|
||||
special[randomBytes[3] % special.Length],
|
||||
};
|
||||
for (int i = 4; i < length; i++)
|
||||
chars.Add(all[randomBytes[i] % all.Length]);
|
||||
|
||||
// Fisher-Yates shuffle so the guaranteed classes aren't always in the same positions.
|
||||
var shuffleBytes = new byte[chars.Count];
|
||||
rng.GetBytes(shuffleBytes);
|
||||
for (int i = chars.Count - 1; i > 0; i--)
|
||||
{
|
||||
int j = shuffleBytes[i] % (i + 1);
|
||||
(chars[i], chars[j]) = (chars[j], chars[i]);
|
||||
}
|
||||
|
||||
return new string(chars.ToArray());
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using AuthHex.Infra.UoW;
|
||||
using AuthHex.Interfaces;
|
||||
using AuthHex.Models;
|
||||
using Core_Archi.Helpers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace AuthHex.Services.RoleManager;
|
||||
|
||||
/// <summary>
|
||||
/// Role use-cases for AuthHex. AuthHex only owns the <see cref="Role"/> lookup table
|
||||
/// (identity source for the JWT RoleId/RoleCode claims). Roles are authored in ERP-core
|
||||
/// and cloned here via <c>createRole</c> using the SAME RoleId so the two stores stay in
|
||||
/// sync. See docs/11-BACKEND-PHASE1.md (AuthHex proxy section).
|
||||
/// </summary>
|
||||
public class RoleManagerService
|
||||
{
|
||||
private readonly IRoleRepository _repository;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public RoleManagerService(IRoleRepository repository, IUnitOfWork uow)
|
||||
{
|
||||
_repository = repository;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clone a role created in the main system. Accepts the caller-supplied RoleId so the
|
||||
/// AuthHex row matches ERP-core's. Idempotent-ish: a duplicate RoleId or Code is rejected.
|
||||
/// </summary>
|
||||
public async Task<object> CreateRole(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (data == null || !data.Any())
|
||||
throw new ArgumentException("Invalid payload data");
|
||||
|
||||
if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
|
||||
throw new ArgumentException("Invalid roleId");
|
||||
|
||||
var code = data.ContainsKey("code") ? data["code"].GetString() : null;
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
throw new ArgumentException("Role code is required");
|
||||
code = code.Trim();
|
||||
|
||||
var name = data.ContainsKey("name") ? data["name"].GetString() : null;
|
||||
|
||||
bool? isSystemRole = null;
|
||||
if (data.ContainsKey("isSystemRole"))
|
||||
{
|
||||
var el = data["isSystemRole"];
|
||||
isSystemRole = el.ValueKind == System.Text.Json.JsonValueKind.True
|
||||
|| el.ValueKind == System.Text.Json.JsonValueKind.False
|
||||
? el.GetBoolean()
|
||||
: (bool?)null;
|
||||
}
|
||||
|
||||
if (await _repository.GetByIdAsync(roleId) != null)
|
||||
throw new InvalidOperationException($"Role '{roleId}' already exists.");
|
||||
|
||||
if (await _repository.GetByCodeAsync(code) != null)
|
||||
throw new InvalidOperationException($"Role code '{code}' already exists.");
|
||||
|
||||
var role = new Role
|
||||
{
|
||||
RoleId = roleId,
|
||||
Code = code,
|
||||
Name = name,
|
||||
IsSystemRole = isSystemRole,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _repository.AddRoleAsync(role);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return ToDto(role);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<object> ListRoles(object payload, HttpContext httpContext)
|
||||
{
|
||||
var roles = await _repository.ListAsync();
|
||||
return roles.Select(ToDto).ToList();
|
||||
}
|
||||
|
||||
public async Task<object> GetRole(object payload, HttpContext httpContext)
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (data == null || !data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
|
||||
throw new ArgumentException("Invalid roleId");
|
||||
|
||||
var role = await _repository.GetByIdAsync(roleId);
|
||||
if (role == null)
|
||||
throw new KeyNotFoundException($"Role '{roleId}' not found.");
|
||||
|
||||
return ToDto(role);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a role. The User.RoleId FK is Restrict, so if any user is still assigned
|
||||
/// the DB rejects the delete and we surface a conflict — keeping ERP-core (which calls
|
||||
/// this before deleting its own copy) from ever orphaning an in-use role.
|
||||
/// </summary>
|
||||
public async Task<object> DeleteRole(object payload, HttpContext httpContext)
|
||||
{
|
||||
await _uow.BeginAsync();
|
||||
try
|
||||
{
|
||||
var data = DeserializePayload.Deserialize(payload);
|
||||
if (data == null || !data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId))
|
||||
throw new ArgumentException("Invalid roleId");
|
||||
|
||||
var role = await _repository.GetByIdAsync(roleId)
|
||||
?? throw new KeyNotFoundException($"Role '{roleId}' not found.");
|
||||
|
||||
var inUse = await _repository.IsAssignedToAnyUserAsync(roleId);
|
||||
if (inUse)
|
||||
throw new InvalidOperationException($"Role '{role.Code}' is assigned to one or more users and cannot be deleted.");
|
||||
|
||||
await _repository.DeleteRoleAsync(role);
|
||||
await _uow.CommitAsync();
|
||||
|
||||
return new { roleId = role.RoleId, deleted = true };
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _uow.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static object ToDto(Role role) => new
|
||||
{
|
||||
roleId = role.RoleId,
|
||||
code = role.Code,
|
||||
name = role.Name,
|
||||
isSystemRole = role.IsSystemRole,
|
||||
createdAt = role.CreatedAt
|
||||
};
|
||||
}
|
||||
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,21 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "server=localhost;port=3306;database=vijaya_auth;user=root;password="
|
||||
},
|
||||
"AppSettings": {
|
||||
"FrontendUrl": "http://localhost:3000"
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "VijayaAuth",
|
||||
"Audience": "VijayaClient",
|
||||
"ExpiresInMinutes": "1000",
|
||||
"RsaPrivateKey": "<RSAKeyValue><Modulus>wO62C07ky/HpJG60atCSddBjSMqY3ZUyYUk9WkhXRwWBDdw1GhyuqgjCZVSyKs7ZRsPUhSEaQsYE2qCAQURtMk2nVLy3HmNqPfSIBbA82nMq9oel1wdnAeXcJWc3V168FvzuRZu+JQS4OFT2OrNnMgdUDDB2nerLzbk71OP6WCqKvGY8YIfUr7pBbu6IWSq0Df498gE1oejsySAzlpSt8t39OhwJySm+d3atXo/JOyfQqnvVTIdNa47MrVztShElIXLLBqw4fuXy7UiyH1fjZSveaS98n673XTiCHUEZd1X7HbJq8B6bXa/7K2uL4MqofeyKZ61NnF8UnVXVnwP0wQ==</Modulus><Exponent>AQAB</Exponent><P>xgj3ZrdQAOv66eBxh+MfnLNEYcKdA8TJFer/qBQYzN7uFzTfBVBoc/ZS8ej8V39VnKjPk2XGaqZtwM6/yaQPkRUnRuz73zO9pzY70xTUwME7VLliSzcLavdcj1y2WpV6hiJIK+LyAbHxRylFZFCTeq7lenkghxRZcIZRL5YN6Xs=</P><Q>+WdnKGaLL1RiX7K+4HKZk0dp410wb/GqHkvRnX+HsM9UNTY4quss73dSaoNOC3Va08asyUCC69DGu/P4Nm2Ax1BN3l6hy0ie6a/Mxtd5t/wIH9vCjmtEQQpqx0BuDbSkUzhTqpF/igiE0jl8no1GpcJjNEWJ/3NVileIaqLJb/M=</Q><DP>j4B71mDLhn95qPRNEI0SoTh+OuEName01z8c1y0I85wvp31WhsFHj5BVdprAEJ9i6Eyjbkn3x9yl92taKsHJzfYoKgscURdrPLneDA0yRiM/3RshESPoEDEDT5d9RJLA4GYDKlofx/6rukc7039X/zSxcVhIq9mhHhsWxYbUufU=</DP><DQ>ifVk3rg6bafj8sPRSh6aTRnHvPkjfgjT7YZLITAWQvusvPKNTh5g8OW0NS8E0NnNsC/KDFkfEoqeJAHEX3cbMSV+rW7zWJuHLAUO5T70GY3UgKrzaAx16PR2NFmzhulRrKcPwN5TCmvD7PXXtWy4k6B/d76voxqNSsU4tKPvgXk=</DQ><InverseQ>UNfEe4sIvZvxAEtFuR+rWVQO0gP/CXepzTgFtaUoDRtFvA3O1s7DVCS9UstGmgxlV871LT0DBBILF4K0LfpkOX8Rth0URVauy5dbDCMLk2XznHBJOOhZrYuUk15/c7zsw7WkVqUmidJvbsPffLP1qChrc19hfHwhKkvOosVDYIE=</InverseQ><D>kcBEmsphIomAndtcRQ2Id6SUPNuBHHSn+MMBrIDWHntUAYqIO0/Ly/yOD0sqxhg6ILG0jVZypls+M4PpIGV3MPdlICjpFfX4vxFK6TE457b1kATuDkIzWUWzSDZvonuZEKzHvKJpxUGJglT9J1986FiM8CB0m9xTVvlG8A/CgnNfEfMCu0n9d3v+a214deF967EQwowDRcRLSYyDVovvIwWTTvF0Tfbzx7jvs/clyrpIYom5Na2+MN9BXHx4EaWMBcEbQPM/6T6sZdnP7k6A2pRDzsKyW0NwhIY8XK5QU15BDUazqbMWCzOhFJyvOx9HSHVJbqcAiup2qShLH2c3eQ==</D></RSAKeyValue>",
|
||||
"RsaPublicKey": "<RSAKeyValue><Modulus>wO62C07ky/HpJG60atCSddBjSMqY3ZUyYUk9WkhXRwWBDdw1GhyuqgjCZVSyKs7ZRsPUhSEaQsYE2qCAQURtMk2nVLy3HmNqPfSIBbA82nMq9oel1wdnAeXcJWc3V168FvzuRZu+JQS4OFT2OrNnMgdUDDB2nerLzbk71OP6WCqKvGY8YIfUr7pBbu6IWSq0Df498gE1oejsySAzlpSt8t39OhwJySm+d3atXo/JOyfQqnvVTIdNa47MrVztShElIXLLBqw4fuXy7UiyH1fjZSveaS98n673XTiCHUEZd1X7HbJq8B6bXa/7K2uL4MqofeyKZ61NnF8UnVXVnwP0wQ==</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "server=<db-host>;port=3306;database=vijaya_auth;user=<db-user>;password=<db-password>"
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"AppSettings": {
|
||||
"FrontendUrl": "<frontend-url>"
|
||||
},
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://0.0.0.0:5602"
|
||||
}
|
||||
},
|
||||
"AllowedOrigins": [ "*" ]
|
||||
},
|
||||
"Cors": {
|
||||
"AllowedOrigins": [ "*" ],
|
||||
"AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ],
|
||||
"AllowedHeaders": [ "*" ]
|
||||
},
|
||||
"Jwt": {
|
||||
"Issuer": "VijayaAuth",
|
||||
"Audience": "VijayaClient",
|
||||
"ExpiresInMinutes": "1000",
|
||||
"RsaPrivateKey": "<generate-fresh-with-GenerateRsaKeys.cs-store-in-appsettings.Development.json-or-env-vars>",
|
||||
"RsaPublicKey": "<generate-fresh-with-GenerateRsaKeys.cs-store-in-appsettings.Development.json-or-env-vars>"
|
||||
},
|
||||
"EmailSettings": {
|
||||
"SmtpServer": "<smtp-host>",
|
||||
"SmtpPort": 587,
|
||||
"SmtpUsername": "<smtp-username>",
|
||||
"SenderEmail": "<sender-email>",
|
||||
"SenderPassword": "<smtp-password>",
|
||||
"SenderName": "Vijaya Sport Club"
|
||||
},
|
||||
"Sms": {
|
||||
"ApiUrl": "<sms-service-url>",
|
||||
"ApiKey": "<sms-api-key>"
|
||||
},
|
||||
"SmtpSettings": {
|
||||
"SmtpServer": "<smtp-host>",
|
||||
"SmtpPort": 587,
|
||||
"SmtpUsername": "<smtp-username>",
|
||||
"SmtpPassword": "<smtp-password>",
|
||||
"SenderEmail": "<sender-email>",
|
||||
"SenderName": "Vijaya Sport Club",
|
||||
"SmsServiceUrl": "<sms-service-url>",
|
||||
"SenderPhone": "<sender-phone>",
|
||||
"SmsApiKey": "<sms-api-key>"
|
||||
},
|
||||
"GoogleAuthenticator": {
|
||||
"Issuer": "VijayaAuth",
|
||||
"SecretKeyLength": 32,
|
||||
"TimeWindowSeconds": 30,
|
||||
"ValidationWindowSize": 1,
|
||||
"QrCodeSize": 200
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user