From 2481f056ab94f6d3d35eec7ca20c7b8efcf7ef18 Mon Sep 17 00:00:00 2001 From: Dhananjaya99 <152056742+ashanruu@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:43:19 +0530 Subject: [PATCH] add role api --- API_DOCUMENTATION.md | 59 +++++++- Controllers/APIController.cs | 13 +- Interfaces/IRoleManageRepository.cs | 13 ++ Program.cs | 3 + Repos/RoleManageRepository.cs | 43 ++++++ Services/FunctionHandler/RoleManager.cs | 72 ++++++++++ Services/FunctionHandler/UserManager.cs | 1 + Services/RoleManager/RoleManagerService.cs | 151 +++++++++++++++++++++ Services/UserManager/UserManagerService.cs | 41 +++++- appsettings.json | 4 +- 10 files changed, 392 insertions(+), 8 deletions(-) create mode 100644 Interfaces/IRoleManageRepository.cs create mode 100644 Repos/RoleManageRepository.cs create mode 100644 Services/FunctionHandler/RoleManager.cs create mode 100644 Services/RoleManager/RoleManagerService.cs diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 91c0416..ac9c594 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -38,6 +38,7 @@ Errors: `404` unknown function, `400` (`KeyNotFoundException`/`InvalidOperationE | `/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: @@ -63,9 +64,12 @@ Payload: "roleId": "guid (required)", "userTypeId": "guid (required)", "chkUser": "bool? (if true, checks for existing conflicting user)", - "password": "string? (auto-generated if empty)" + "password": "string? (auto-generated if empty)", + "sendCredentialsEmail": "bool? (default true; emails `email` the username + password when set)" } ``` +The generated/supplied password is now persisted (hashed) against the user record (previously a bug left it unset). When `email` is present and `sendCredentialsEmail` is not `false`, an email with the username and password is sent best-effort after the user is committed (failures do not roll back registration). + Data: ```json { @@ -129,6 +133,13 @@ Data: } ``` +### listUserTypes +No payload. +Data: array of +```json +{ "userTypeId": "guid", "code": "", "description": "" } +``` + ### getUserSessions _Requires auth (userId from claims). No payload needed._ Data: array of @@ -323,8 +334,52 @@ Data: --- +## /api/role — RoleManager functions + +> **Null-tolerant fields (fixed 2026-07-18):** `isSystemRole` is read via `data["isSystemRole"].ValueKind != JsonValueKind.Null` +> before calling `GetBoolean()` in both `createRole` and `updateRole` — an explicit JSON `null` (as opposed to the key being +> absent) previously threw an unhandled `InvalidOperationException`, surfaced to ERPCore callers as a generic `500`/`AUTH_UPSTREAM_ERROR`. + +### createRole +Payload: +```json +{ "code": "string (required)", "name": "string? (defaults to code)", "isSystemRole": "bool? (default false)" } +``` +Data: +```json +{ "roleId": "guid", "code": "", "name": "", "isSystemRole": false, "createdAt": "" } +``` + +### getRole +Payload: +```json +{ "roleId": "guid (required)" } +``` +Data: same shape as `createRole`. + +### listRoles +No payload. +Data: array of `createRole`-shaped objects. + +### updateRole +Payload (all fields optional except `roleId`): +```json +{ "roleId": "guid (required)", "code": "string?", "name": "string?", "isSystemRole": "bool?" } +``` +Data: same shape as `createRole`. + +### deleteRole +Payload: +```json +{ "roleId": "guid (required)" } +``` +Blocked with a `400` (`"ROLE_IN_USE: role is assigned to one or more users"`) if any `User.RoleId` references it (FK is `Restrict`). +Data: `{ "message": "Role deleted successfully" }` + +--- + ## Notes - All timestamps are UTC. - `deviceName`, `Browser`, `OS`, `IPAddress` are captured per session from request headers for session tracking (`getUserSessions`). -- Password login validation (`PasswordHasher.Verify`) is currently commented out in `loginUser` — passwords are not checked on login as of this version. +- Password login validation (`PasswordHasher.Verify`) is performed in `loginUser` — invalid/missing password hashes are rejected. - JWT access tokens issued with `expiresIn: 3600` (1 hour); refresh tokens/sessions expire after 30 days. diff --git a/Controllers/APIController.cs b/Controllers/APIController.cs index 1e5bfb0..5567d97 100644 --- a/Controllers/APIController.cs +++ b/Controllers/APIController.cs @@ -10,11 +10,12 @@ namespace AuthHex.Controllers [ApiController] [Route("api")] [EnableCors("AllowAll")] - public class APIController(UserManager userManager, RecoveryManager recoveryManager, AltOptionManager altOptionManager) : ControllerBase + 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] @@ -70,10 +71,18 @@ namespace AuthHex.Controllers [AllowAnonymous] public async Task IsAvailable([FromBody] ApiRequest request) { - + var response = await _altOptionManager.Execute(request, HttpContext); return StatusCode(response.StatusCode, response); } + [HttpPost("role")] + [AllowAnonymous] + public async Task RoleExecute([FromBody] ApiRequest request) + { + var response = await _roleManager.Execute(request, HttpContext); + return StatusCode(response.StatusCode, response); + } + } } diff --git a/Interfaces/IRoleManageRepository.cs b/Interfaces/IRoleManageRepository.cs new file mode 100644 index 0000000..d7de6d4 --- /dev/null +++ b/Interfaces/IRoleManageRepository.cs @@ -0,0 +1,13 @@ +using AuthHex.Models; + +namespace AuthHex.Interfaces +{ + public interface IRoleManageRepository + { + Task AddRoleAsync(Role role, CancellationToken ct = default); + Task GetRoleByIdAsync(Guid roleId, CancellationToken ct = default); + Task> ListRolesAsync(CancellationToken ct = default); + Task RoleInUseAsync(Guid roleId, CancellationToken ct = default); + Task DeleteRoleAsync(Role role, CancellationToken ct = default); + } +} diff --git a/Program.cs b/Program.cs index 73c076a..174b969 100644 --- a/Program.cs +++ b/Program.cs @@ -57,15 +57,18 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); var configuredOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() ?? Array.Empty(); var allowedOrigins = configuredOrigins diff --git a/Repos/RoleManageRepository.cs b/Repos/RoleManageRepository.cs new file mode 100644 index 0000000..0bc9a00 --- /dev/null +++ b/Repos/RoleManageRepository.cs @@ -0,0 +1,43 @@ +using AuthHex.Interfaces; +using AuthHex.Models; +using Microsoft.EntityFrameworkCore; + +namespace AuthHex.Repos +{ + public class RoleManageRepository : IRoleManageRepository + { + private readonly AppDBContext _dbContext; + + public RoleManageRepository(AppDBContext dbContext) + { + _dbContext = dbContext; + } + + public Task AddRoleAsync(Role role, CancellationToken ct = default) + { + _dbContext.Roles.Add(role); + return Task.FromResult(role); + } + + public async Task GetRoleByIdAsync(Guid roleId, CancellationToken ct = default) + { + return await _dbContext.Roles.FirstOrDefaultAsync(r => r.RoleId == roleId, ct); + } + + public async Task> ListRolesAsync(CancellationToken ct = default) + { + return await _dbContext.Roles.OrderBy(r => r.Code).ToListAsync(ct); + } + + public async Task RoleInUseAsync(Guid roleId, CancellationToken ct = default) + { + return await _dbContext.Users.AnyAsync(u => u.RoleId == roleId, ct); + } + + public Task DeleteRoleAsync(Role role, CancellationToken ct = default) + { + _dbContext.Roles.Remove(role); + return Task.CompletedTask; + } + } +} diff --git a/Services/FunctionHandler/RoleManager.cs b/Services/FunctionHandler/RoleManager.cs new file mode 100644 index 0000000..3d6e071 --- /dev/null +++ b/Services/FunctionHandler/RoleManager.cs @@ -0,0 +1,72 @@ +using AuthHex.Models.IOs; +using AuthHex.Services.RoleManager; + +namespace AuthHex.Services.FunctionHandler +{ + public class RoleManager + { + public readonly Dictionary>> _functionHandler; + + public RoleManager(RoleManagerService roleManagerService) + { + _functionHandler = new() + { + { "createRole", roleManagerService.CreateRole }, + { "getRole", roleManagerService.GetRole }, + { "listRoles", async (payload, context) => await roleManagerService.ListRoles(context) }, + { "updateRole", roleManagerService.UpdateRole }, + { "deleteRole", roleManagerService.DeleteRole } + }; + } + + public async Task 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 + }; + } + } + } +} diff --git a/Services/FunctionHandler/UserManager.cs b/Services/FunctionHandler/UserManager.cs index 0c26c4b..8ce73c6 100644 --- a/Services/FunctionHandler/UserManager.cs +++ b/Services/FunctionHandler/UserManager.cs @@ -18,6 +18,7 @@ namespace AuthHex.Services.FunctionHandler { "refreshToken", UserManagerService.RefreshToken }, { "getUserDetails", UserManagerService.GetUserDetails }, { "getUserSessions", async (payload, context) => await UserManagerService.GetUserSessions(context) }, + { "listUserTypes", async (payload, context) => await UserManagerService.ListUserTypes(context) }, { "ChangeUserStatus", UserManagerService.ChangeUserStatus}, {"LockUserAccount" , UserManagerService.LockUserAccount }, {"ChangeUserPassword" , UserManagerService.ChangeUserPassword }, diff --git a/Services/RoleManager/RoleManagerService.cs b/Services/RoleManager/RoleManagerService.cs new file mode 100644 index 0000000..6a00889 --- /dev/null +++ b/Services/RoleManager/RoleManagerService.cs @@ -0,0 +1,151 @@ +using System.Text.Json; +using AuthHex.Infra.UoW; +using AuthHex.Interfaces; +using AuthHex.Models; +using Core_Archi.Helpers; +using Microsoft.AspNetCore.Http; + +namespace AuthHex.Services.RoleManager; + +public class RoleManagerService +{ + private readonly IRoleManageRepository _repository; + private readonly IUnitOfWork _uow; + + public RoleManagerService(IRoleManageRepository repository, IUnitOfWork uow) + { + _repository = repository; + _uow = uow; + } + + public async Task CreateRole(object payload, HttpContext httpContext) + { + await _uow.BeginAsync(); + try + { + var data = DeserializePayload.Deserialize(payload); + + var code = data.ContainsKey("code") ? data["code"].GetString() : null; + var name = data.ContainsKey("name") ? data["name"].GetString() : null; + var isSystemRole = data.ContainsKey("isSystemRole") + && data["isSystemRole"].ValueKind != JsonValueKind.Null + && data["isSystemRole"].GetBoolean(); + + if (string.IsNullOrWhiteSpace(code)) + throw new ArgumentException("code is required"); + + var role = new Role + { + RoleId = Guid.NewGuid(), + Code = code, + Name = string.IsNullOrWhiteSpace(name) ? code : name, + IsSystemRole = isSystemRole, + CreatedAt = DateTime.UtcNow + }; + + await _repository.AddRoleAsync(role); + await _uow.CommitAsync(); + + return ToDto(role); + } + catch + { + await _uow.RollbackAsync(); + throw; + } + } + + public async Task GetRole(object payload, HttpContext httpContext) + { + var data = DeserializePayload.Deserialize(payload); + + if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId)) + throw new ArgumentException("Invalid roleId"); + + var role = await _repository.GetRoleByIdAsync(roleId) + ?? throw new KeyNotFoundException("Role not found"); + + return ToDto(role); + } + + public async Task ListRoles(HttpContext httpContext) + { + var roles = await _repository.ListRolesAsync(); + return roles.Select(ToDto).ToList(); + } + + public async Task UpdateRole(object payload, HttpContext httpContext) + { + await _uow.BeginAsync(); + try + { + var data = DeserializePayload.Deserialize(payload); + + if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId)) + throw new ArgumentException("Invalid roleId"); + + var role = await _repository.GetRoleByIdAsync(roleId) + ?? throw new KeyNotFoundException("Role not found"); + + if (data.ContainsKey("code")) + { + var code = data["code"].GetString(); + if (string.IsNullOrWhiteSpace(code)) + throw new ArgumentException("code cannot be empty"); + role.Code = code; + } + + if (data.ContainsKey("name")) + role.Name = data["name"].GetString(); + + if (data.ContainsKey("isSystemRole") && data["isSystemRole"].ValueKind != JsonValueKind.Null) + role.IsSystemRole = data["isSystemRole"].GetBoolean(); + + await _uow.CommitAsync(); + + return ToDto(role); + } + catch + { + await _uow.RollbackAsync(); + throw; + } + } + + public async Task DeleteRole(object payload, HttpContext httpContext) + { + await _uow.BeginAsync(); + try + { + var data = DeserializePayload.Deserialize(payload); + + if (!data.ContainsKey("roleId") || !Guid.TryParse(data["roleId"].GetString(), out var roleId)) + throw new ArgumentException("Invalid roleId"); + + var role = await _repository.GetRoleByIdAsync(roleId) + ?? throw new KeyNotFoundException("Role not found"); + + if (await _repository.RoleInUseAsync(roleId)) + throw new InvalidOperationException("ROLE_IN_USE: role is assigned to one or more users"); + + await _repository.DeleteRoleAsync(role); + await _uow.CommitAsync(); + + return new { message = "Role deleted successfully" }; + } + catch + { + await _uow.RollbackAsync(); + throw; + } + } + + private static object ToDto(Role role) => new + { + role.RoleId, + role.Code, + role.Name, + role.IsSystemRole, + role.CreatedAt + }; +} diff --git a/Services/UserManager/UserManagerService.cs b/Services/UserManager/UserManagerService.cs index e9a7623..dbe14f0 100644 --- a/Services/UserManager/UserManagerService.cs +++ b/Services/UserManager/UserManagerService.cs @@ -94,8 +94,10 @@ public class UserManagerService data["password"] = JsonDocument.Parse($"\"{Guid.NewGuid().ToString().Substring(0, 8)}\"").RootElement; } - var PasswordHash = data.ContainsKey("password") ? PasswordHasher.Hash(data["password"].GetString()!) : null; + var RawPassword = data["password"].GetString()!; + var PasswordHash = PasswordHasher.Hash(RawPassword); + var sendCredentialsEmail = !data.ContainsKey("sendCredentialsEmail") || data["sendCredentialsEmail"].GetBoolean(); // Create User object User user = new User @@ -113,7 +115,7 @@ public class UserManagerService IsLocked = false, EmailVerified = false, MobileNumberVerified = false, - //PasswordHash = PasswordHash + PasswordHash = PasswordHash }; @@ -151,6 +153,30 @@ public class UserManagerService await _uow.CommitAsync(); + if (sendCredentialsEmail && !string.IsNullOrWhiteSpace(Email)) + { + try + { + await _thirdPartyService.EmailConfiguration( + Email, + RawPassword, + "Your ERP System Account", + $@" + + +

Your ERP System account has been created

+

Username: {WebUtility.HtmlEncode(UserName ?? Email)}

+

Temporary password: {{code}}

+

Please log in and change your password as soon as possible.

+ + "); + } + catch + { + // Best-effort: account creation already committed; do not fail the request on email delivery issues. + } + } + return new { AccessToken = jwtToken, @@ -544,6 +570,17 @@ public class UserManagerService } } + public async Task ListUserTypes(HttpContext httpContext) + { + var userTypes = await _dbContext.UserType.OrderBy(t => t.Code).ToListAsync(); + return userTypes.Select(t => new + { + t.UserTypeId, + t.Code, + t.Description + }).ToList(); + } + public async Task GetUserSessions(HttpContext httpContext) { try diff --git a/appsettings.json b/appsettings.json index 102a087..e2a4ab2 100644 --- a/appsettings.json +++ b/appsettings.json @@ -8,8 +8,8 @@ "ConnectionStrings": { //"DefaultConnection": "server=187.127.102.190;port=3306;database=hexafitnessauth;user=dbuser;password=dbpass" - "DefaultConnection": "server=localhost;port=3306;database=authhex;user=root;password=" - //"DefaultConnection": "server=187.127.102.190;port=3306;database=omsauth;user=poojathmi;password=poojathmi@hexdive.com" + //"DefaultConnection": "server=localhost;port=3306;database=authhex;user=root;password=" + "DefaultConnection": "server=187.127.102.190;port=3306;database=erpauth;user=poojathmi;password=poojathmi@hexdive.com" }, "AllowedHosts": "*", "AppSettings": {