add rbac
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using ERPCore.Dtos.Auth;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
using ERPCore.Infra.Auth;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
@@ -25,12 +26,27 @@ public sealed class AuthController : ControllerBase
|
||||
private readonly IAuthUserService _users;
|
||||
private readonly IAuthRecoveryService _recovery;
|
||||
private readonly IAuthAltService _alt;
|
||||
private readonly IRoleService _roles;
|
||||
|
||||
public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt)
|
||||
public AuthController(IAuthUserService users, IAuthRecoveryService recovery, IAuthAltService alt, IRoleService roles)
|
||||
{
|
||||
_users = users;
|
||||
_recovery = recovery;
|
||||
_alt = alt;
|
||||
_roles = roles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Authoritative current-session info for the frontend: role + the sidebar nav
|
||||
/// codes it may see (docs/10 C.9 shadow-Role sync). Replaces the frontend's
|
||||
/// previous reliance on a stale, untrusted `roleId` cached in localStorage.
|
||||
/// </summary>
|
||||
[HttpGet("me")]
|
||||
[ProducesResponseType(typeof(MeResponseDto), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<MeResponseDto>> Me(CancellationToken ct)
|
||||
{
|
||||
var roleCode = User.FindFirst(AuthHexClaims.RoleCode)?.Value;
|
||||
return Ok(await _roles.GetMeAsync(roleCode, ct));
|
||||
}
|
||||
|
||||
// ---- Session-issuing (UserManager) ------------------------------------
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only sidebar nav tree, used by the Role permission-assignment checkbox
|
||||
/// UI and by `GET /auth/me` (see AuthController) to resolve a role's visible codes.
|
||||
/// NavItem/SubNavItem rows are seeded (NavItemConfiguration/SubNavItemConfiguration)
|
||||
/// to match the frontend's hardcoded sidebar — not admin-editable in this phase.
|
||||
/// </summary>
|
||||
[Route("api/v1/nav")]
|
||||
public sealed class NavController : ApiControllerBase
|
||||
{
|
||||
private readonly IRepository<NavItem> _navItems;
|
||||
|
||||
public NavController(IRepository<NavItem> navItems) => _navItems = navItems;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(List<NavItemDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<NavItemDto>>> GetTree(CancellationToken ct)
|
||||
{
|
||||
var items = await _navItems.Query().AsNoTracking()
|
||||
.Include(n => n.Children)
|
||||
.OrderBy(n => n.SortOrder)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var dto = items.Select(n => new NavItemDto(
|
||||
n.NavItemId, n.Code, n.Label, n.Icon, n.Href, n.SortOrder,
|
||||
n.Children.OrderBy(c => c.SortOrder)
|
||||
.Select(c => new SubNavItemDto(c.SubNavItemId, c.Code, c.Label, c.Icon, c.Href, c.SortOrder))
|
||||
.ToList())).ToList();
|
||||
|
||||
return Ok(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>Role CRUD + permission assignment (mirrors AuthHex's Role; see docs/10 C.9).</summary>
|
||||
[Route("api/v1/roles")]
|
||||
public sealed class RolesController : ApiControllerBase
|
||||
{
|
||||
private readonly IRoleService _roles;
|
||||
|
||||
public RolesController(IRoleService roles) => _roles = roles;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<RoleDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<RoleDto>>> List(
|
||||
[FromQuery] PageQuery query, [FromQuery] EntityStatus? status, CancellationToken ct)
|
||||
=> Ok(await _roles.ListAsync(query, status, ct));
|
||||
|
||||
[HttpGet("{roleId:int}")]
|
||||
[ProducesResponseType(typeof(RoleDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RoleDto>> GetById(int roleId, CancellationToken ct)
|
||||
{
|
||||
var result = await _roles.GetAsync(roleId, ct);
|
||||
if (result is null) return NotFound();
|
||||
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(RoleDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<RoleDto>> Create([FromBody] CreateRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _roles.CreateAsync(request, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Created($"/api/v1/roles/{result.Value.RoleId}", result.Value);
|
||||
}
|
||||
|
||||
[HttpPut("{roleId:int}")]
|
||||
[ProducesResponseType(typeof(RoleDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status412PreconditionFailed)]
|
||||
public async Task<ActionResult<RoleDto>> Update(int roleId, [FromBody] UpdateRoleRequest request, CancellationToken ct)
|
||||
{
|
||||
var expected = RequireIfMatch();
|
||||
var result = await _roles.UpdateAsync(roleId, request, expected, ct);
|
||||
SetETag(result.RowVersion);
|
||||
return Ok(result.Value);
|
||||
}
|
||||
|
||||
[HttpPatch("{roleId:int}/status")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> SetStatus(int roleId, [FromBody] UpdateRoleStatusRequest request, CancellationToken ct)
|
||||
{
|
||||
await _roles.SetStatusAsync(roleId, request.Status, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpDelete("{roleId:int}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Delete(int roleId, CancellationToken ct)
|
||||
{
|
||||
await _roles.DeleteAsync(roleId, ct);
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpGet("{roleId:int}/permissions")]
|
||||
[ProducesResponseType(typeof(RolePermissionsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RolePermissionsDto>> GetPermissions(int roleId, CancellationToken ct)
|
||||
=> Ok(await _roles.GetPermissionsAsync(roleId, ct));
|
||||
|
||||
[HttpPut("{roleId:int}/permissions")]
|
||||
[ProducesResponseType(typeof(RolePermissionsDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<RolePermissionsDto>> AssignPermissions(
|
||||
int roleId, [FromBody] AssignRolePermissionsRequest request, CancellationToken ct)
|
||||
=> Ok(await _roles.AssignPermissionsAsync(roleId, request, ct));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Users;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ERPCore.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// User management: local shadow `User` list/detail + role assignment, and
|
||||
/// account creation orchestrated against AuthHex (see <see cref="IUserManagementService.CreateAsync"/>).
|
||||
/// </summary>
|
||||
[Route("api/v1/users")]
|
||||
public sealed class UsersController : ApiControllerBase
|
||||
{
|
||||
private readonly IUserManagementService _users;
|
||||
|
||||
public UsersController(IUserManagementService users) => _users = users;
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PagedResponse<ManagedUserDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedResponse<ManagedUserDto>>> List([FromQuery] PageQuery query, CancellationToken ct)
|
||||
=> Ok(await _users.ListAsync(query, ct));
|
||||
|
||||
/// <summary>AuthHex UserType options for the create-user form's select.</summary>
|
||||
[HttpGet("user-types")]
|
||||
[ProducesResponseType(typeof(List<UserTypeOptionDto>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<List<UserTypeOptionDto>>> ListUserTypes(CancellationToken ct)
|
||||
=> Ok(await _users.ListUserTypesAsync(ct));
|
||||
|
||||
[HttpGet("{userId:int}")]
|
||||
[ProducesResponseType(typeof(ManagedUserDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ManagedUserDto>> GetById(int userId, CancellationToken ct)
|
||||
{
|
||||
var result = await _users.GetAsync(userId, ct);
|
||||
return result is null ? NotFound() : Ok(result);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ManagedUserDto), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<ActionResult<ManagedUserDto>> Create([FromBody] CreateUserRequest request, CancellationToken ct)
|
||||
{
|
||||
var result = await _users.CreateAsync(request, ct);
|
||||
return Created($"/api/v1/users/{result.UserId}", result);
|
||||
}
|
||||
|
||||
[HttpPut("{userId:int}/role")]
|
||||
[ProducesResponseType(typeof(ManagedUserDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<ManagedUserDto>> UpdateRole(int userId, [FromBody] UpdateUserRoleRequest request, CancellationToken ct)
|
||||
=> Ok(await _users.UpdateRoleAsync(userId, request, ct));
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A top-level sidebar entry (mirrors the frontend's hardcoded nav list,
|
||||
/// components/Layouts/AppSidebar.tsx). Seeded to match the current app routes;
|
||||
/// per-role visibility is controlled via <see cref="Permission"/>/<see cref="RolePermission"/>,
|
||||
/// not by editing these rows through the UI.
|
||||
/// </summary>
|
||||
public class NavItem
|
||||
{
|
||||
public int NavItemId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string? Icon { get; set; }
|
||||
public string? Href { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public ICollection<SubNavItem> Children { get; set; } = new List<SubNavItem>();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// A grantable sidebar-visibility unit — exactly one of <see cref="NavItemId"/> /
|
||||
/// <see cref="SubNavItemId"/> is set (enforced in <c>NavSeedService</c>/service layer,
|
||||
/// not by a DB constraint). One row is seeded per <see cref="NavItem"/>/<see cref="SubNavItem"/>;
|
||||
/// <see cref="RolePermission"/> grants it to a role.
|
||||
/// </summary>
|
||||
public class Permission
|
||||
{
|
||||
public int PermissionId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public int? NavItemId { get; set; }
|
||||
public int? SubNavItemId { get; set; }
|
||||
|
||||
public NavItem? NavItem { get; set; }
|
||||
public SubNavItem? SubNavItem { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Local shadow/projection of an AuthHex <c>Role</c> row, mirroring the same
|
||||
/// pattern <see cref="User"/> uses for AuthHex identities: <see cref="AuthRoleId"/>
|
||||
/// maps to AuthHex's Guid <c>RoleId</c>, while the local <see cref="RoleId"/> (int)
|
||||
/// is what <see cref="Permission"/>/<see cref="RolePermission"/>/<see cref="User.RoleId"/>
|
||||
/// FKs reference. AuthHex remains the source of truth; writes are forwarded there
|
||||
/// first (<c>IAuthHexClient</c>) and mirrored here on success.
|
||||
/// </summary>
|
||||
public class Role
|
||||
{
|
||||
public int RoleId { get; set; }
|
||||
public Guid AuthRoleId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public bool IsSystemRole { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>PostgreSQL xmin-backed optimistic concurrency token (ETag source).</summary>
|
||||
public uint RowVersion { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>Join row granting a <see cref="Role"/> visibility of a <see cref="Permission"/> (nav node).</summary>
|
||||
public class RolePermission
|
||||
{
|
||||
public int RoleId { get; set; }
|
||||
public int PermissionId { get; set; }
|
||||
|
||||
public Role? Role { get; set; }
|
||||
public Permission? Permission { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Domain.Entities;
|
||||
|
||||
/// <summary>A child sidebar entry under a <see cref="NavItem"/> (e.g. Products' children).</summary>
|
||||
public class SubNavItem
|
||||
{
|
||||
public int SubNavItemId { get; set; }
|
||||
public int NavItemId { get; set; }
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string? Icon { get; set; }
|
||||
public string? Href { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public EntityStatus Status { get; set; } = EntityStatus.Active;
|
||||
|
||||
public NavItem? NavItem { get; set; }
|
||||
}
|
||||
@@ -22,4 +22,8 @@ public class User
|
||||
|
||||
/// <summary>AuthHex identity (token <c>UserId</c> GUID); null for the seeded system user.</summary>
|
||||
public Guid? AuthUserId { get; set; }
|
||||
|
||||
/// <summary>Local shadow <see cref="Role"/> assignment; null until an admin assigns one.</summary>
|
||||
public int? RoleId { get; set; }
|
||||
public Role? Role { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ERPCore.Dtos.Auth;
|
||||
|
||||
/// <summary>AuthHex's Role projection (ERP_Auth_Service/API_DOCUMENTATION.md, RoleManager section).</summary>
|
||||
public sealed class AuthHexRoleDto
|
||||
{
|
||||
public Guid RoleId { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool? IsSystemRole { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CreateAuthHexRoleRequest
|
||||
{
|
||||
[Required] public string Code { get; set; } = string.Empty;
|
||||
public string? Name { get; set; }
|
||||
public bool? IsSystemRole { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateAuthHexRoleRequest
|
||||
{
|
||||
[Required] public Guid RoleId { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public bool? IsSystemRole { get; set; }
|
||||
}
|
||||
@@ -82,6 +82,14 @@ public sealed class GetUserDetailsResponse
|
||||
public JsonElement? UserType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>AuthHex's UserType lookup (ERP_Auth_Service/API_DOCUMENTATION.md, listUserTypes).</summary>
|
||||
public sealed class UserTypeDto
|
||||
{
|
||||
public Guid UserTypeId { get; set; }
|
||||
public string? Code { get; set; }
|
||||
public string? Description { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SessionDto
|
||||
{
|
||||
public string? SessionId { get; set; }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace ERPCore.Dtos.Rbac;
|
||||
|
||||
/// <summary>Response for `GET /api/v1/auth/me` — the frontend's authoritative source
|
||||
/// for the current user's role and permitted sidebar nav codes (replaces trusting
|
||||
/// the stale, client-only `roleId` cached in localStorage).</summary>
|
||||
public sealed record MeResponseDto(string? RoleCode, string? RoleName, IReadOnlyList<string> NavCodes);
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace ERPCore.Dtos.Rbac;
|
||||
|
||||
public sealed record SubNavItemDto(int SubNavItemId, string Code, string Label, string? Icon, string? Href, int SortOrder);
|
||||
|
||||
public sealed record NavItemDto(
|
||||
int NavItemId, string Code, string Label, string? Icon, string? Href, int SortOrder,
|
||||
IReadOnlyList<SubNavItemDto> Children);
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Rbac;
|
||||
|
||||
public sealed record RoleDto(
|
||||
int RoleId, string Code, string Name, bool IsSystemRole, EntityStatus Status,
|
||||
DateTime CreatedAt, DateTime? UpdatedAt);
|
||||
|
||||
public sealed class CreateRoleRequest
|
||||
{
|
||||
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class UpdateRoleRequest
|
||||
{
|
||||
[Required, StringLength(50)] public string Code { get; set; } = string.Empty;
|
||||
[Required, StringLength(200)] public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class UpdateRoleStatusRequest
|
||||
{
|
||||
[Required, EnumDataType(typeof(EntityStatus))] public EntityStatus Status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Replaces a role's full permission set (checkbox-tree save from the frontend).</summary>
|
||||
public sealed class AssignRolePermissionsRequest
|
||||
{
|
||||
public List<int> NavItemIds { get; set; } = new();
|
||||
public List<int> SubNavItemIds { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed record RolePermissionsDto(int RoleId, List<int> NavItemIds, List<int> SubNavItemIds);
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using ERPCore.Domain.Enums;
|
||||
|
||||
namespace ERPCore.Dtos.Users;
|
||||
|
||||
public sealed record ManagedUserDto(
|
||||
int UserId, string Username, string DisplayName, EntityStatus Status,
|
||||
int? RoleId, string? RoleCode, string? RoleName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a user in both backends: forwards to AuthHex's `registerUser` (source
|
||||
/// of truth for credentials), then mirrors the account into ERPCore's local
|
||||
/// shadow `User` row immediately (rather than waiting for next-login JIT
|
||||
/// provisioning). AuthHex emails the generated/supplied password to `Email`.
|
||||
/// </summary>
|
||||
public sealed class CreateUserRequest
|
||||
{
|
||||
[Required, StringLength(100)] public string Username { get; set; } = string.Empty;
|
||||
[Required, StringLength(200)] public string FullName { get; set; } = string.Empty;
|
||||
[Required] public int RoleId { get; set; }
|
||||
[Required] public Guid UserTypeId { get; set; }
|
||||
[Required, EmailAddress] public string Email { get; set; } = string.Empty;
|
||||
public string? Nic { get; set; }
|
||||
public string? MobileNumber { get; set; }
|
||||
/// <summary>Left empty to auto-generate (AuthHex emails it to <see cref="Email"/>).</summary>
|
||||
public string? Password { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UpdateUserRoleRequest
|
||||
{
|
||||
[Required] public int RoleId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>AuthHex UserType lookup, for populating the create-user form's select (no local shadow — read-only passthrough).</summary>
|
||||
public sealed record UserTypeOptionDto(Guid UserTypeId, string? Code, string? Description);
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ERPCore.Dtos.Auth;
|
||||
using ERPCore.System.Errors;
|
||||
|
||||
@@ -16,9 +17,14 @@ namespace ERPCore.Infra.Auth.AuthHex;
|
||||
/// </summary>
|
||||
public sealed class AuthHexClient : IAuthHexClient
|
||||
{
|
||||
// WhenWritingNull: AuthHex's dispatcher reads payload fields as raw JsonElements and some
|
||||
// (e.g. RoleManager's isSystemRole) call type-specific getters like GetBoolean() that throw
|
||||
// on an explicit JSON null rather than treating it as "absent" — omit null properties instead
|
||||
// of serializing them, so unset nullable request fields behave as ContainsKey == false upstream.
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
PropertyNameCaseInsensitive = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private readonly HttpClient _http;
|
||||
@@ -42,6 +48,9 @@ public sealed class AuthHexClient : IAuthHexClient
|
||||
public Task<GetUserDetailsResponse> GetUserDetailsAsync(Guid userId, CancellationToken ct)
|
||||
=> CallAsync<GetUserDetailsResponse>("user", "getUserDetails", new { userId }, null, ct);
|
||||
|
||||
public Task<List<UserTypeDto>> ListUserTypesAsync(CancellationToken ct)
|
||||
=> CallAsync<List<UserTypeDto>>("user", "listUserTypes", new { }, null, ct);
|
||||
|
||||
public Task<List<SessionDto>> GetUserSessionsAsync(string bearerToken, CancellationToken ct)
|
||||
=> CallAsync<List<SessionDto>>("user", "getUserSessions", new { }, bearerToken, ct);
|
||||
|
||||
@@ -103,6 +112,23 @@ public sealed class AuthHexClient : IAuthHexClient
|
||||
public Task<AuthHexSessionResult> VerifyAltOtpAsync(VerifyAltOtpRequest request, CancellationToken ct)
|
||||
=> CallAsync<AuthHexSessionResult>("alt", "VerifyOTP", request, null, ct);
|
||||
|
||||
// ---- RoleManager --------------------------------------------------
|
||||
|
||||
public Task<AuthHexRoleDto> CreateRoleAsync(CreateAuthHexRoleRequest request, CancellationToken ct)
|
||||
=> CallAsync<AuthHexRoleDto>("role", "createRole", request, null, ct);
|
||||
|
||||
public Task<List<AuthHexRoleDto>> ListRolesAsync(CancellationToken ct)
|
||||
=> CallAsync<List<AuthHexRoleDto>>("role", "listRoles", new { }, null, ct);
|
||||
|
||||
public Task<AuthHexRoleDto> GetRoleAsync(Guid roleId, CancellationToken ct)
|
||||
=> CallAsync<AuthHexRoleDto>("role", "getRole", new { roleId }, null, ct);
|
||||
|
||||
public Task<AuthHexRoleDto> UpdateRoleAsync(UpdateAuthHexRoleRequest request, CancellationToken ct)
|
||||
=> CallAsync<AuthHexRoleDto>("role", "updateRole", request, null, ct);
|
||||
|
||||
public Task DeleteRoleAsync(Guid roleId, CancellationToken ct)
|
||||
=> CallVoidAsync("role", "deleteRole", new { roleId }, null, ct);
|
||||
|
||||
// ---- Transport --------------------------------------------------------
|
||||
|
||||
private async Task CallVoidAsync(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct)
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface IAuthHexClient
|
||||
Task<AuthHexSessionResult> VerifyOtpForLoginAsync(VerifyOtpForLoginRequest request, CancellationToken ct);
|
||||
Task<AuthHexSessionResult> RefreshTokenAsync(string refreshToken, string? deviceName, CancellationToken ct);
|
||||
Task<GetUserDetailsResponse> GetUserDetailsAsync(Guid userId, CancellationToken ct);
|
||||
Task<List<UserTypeDto>> ListUserTypesAsync(CancellationToken ct);
|
||||
Task<List<SessionDto>> GetUserSessionsAsync(string bearerToken, CancellationToken ct);
|
||||
Task ChangeUserStatusAsync(bool isActive, string bearerToken, CancellationToken ct);
|
||||
Task LockUserAccountAsync(bool isLocked, string bearerToken, CancellationToken ct);
|
||||
@@ -39,4 +40,12 @@ public interface IAuthHexClient
|
||||
Task<IsAvailableResponse> IsAvailableAsync(IsAvailableRequest request, CancellationToken ct);
|
||||
Task<SendOtpResponse> SendOtpAsync(SendOtpRequest request, CancellationToken ct);
|
||||
Task<AuthHexSessionResult> VerifyAltOtpAsync(VerifyAltOtpRequest request, CancellationToken ct);
|
||||
|
||||
// RoleManager (POST /api/role) — AuthHex is the source of truth for Role;
|
||||
// ERPCore mirrors the result into a local shadow Role row (see RoleService).
|
||||
Task<AuthHexRoleDto> CreateRoleAsync(CreateAuthHexRoleRequest request, CancellationToken ct);
|
||||
Task<List<AuthHexRoleDto>> ListRolesAsync(CancellationToken ct);
|
||||
Task<AuthHexRoleDto> GetRoleAsync(Guid roleId, CancellationToken ct);
|
||||
Task<AuthHexRoleDto> UpdateRoleAsync(UpdateAuthHexRoleRequest request, CancellationToken ct);
|
||||
Task DeleteRoleAsync(Guid roleId, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// Seeded to mirror the frontend's hardcoded sidebar
|
||||
/// (ERP-core/Frontend/erp-system/components/Layouts/AppSidebar.tsx). Codes here
|
||||
/// must match the <c>code</c> given to each frontend nav entry.
|
||||
/// </summary>
|
||||
public sealed class NavItemConfiguration : IEntityTypeConfiguration<NavItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NavItem> builder)
|
||||
{
|
||||
builder.ToTable("nav_items");
|
||||
builder.HasKey(n => n.NavItemId);
|
||||
|
||||
builder.Property(n => n.Code).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(n => n.Code).IsUnique();
|
||||
builder.Property(n => n.Label).IsRequired().HasMaxLength(100);
|
||||
builder.Property(n => n.Icon).HasMaxLength(50);
|
||||
builder.Property(n => n.Href).HasMaxLength(200);
|
||||
builder.Property(n => n.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
|
||||
builder.HasData(
|
||||
new NavItem { NavItemId = 1, Code = "dashboard", Label = "Dashboard", Href = "/dashboard", SortOrder = 1 },
|
||||
new NavItem { NavItemId = 2, Code = "products", Label = "Products", Href = "/dashboard/products", SortOrder = 2 },
|
||||
new NavItem { NavItemId = 3, Code = "vendors", Label = "Vendors", Href = "/dashboard/vendors", SortOrder = 3 },
|
||||
new NavItem { NavItemId = 4, Code = "procurement", Label = "Procurement", Href = "/dashboard/procurement", SortOrder = 4 },
|
||||
new NavItem { NavItemId = 5, Code = "receiving", Label = "Receiving", Href = "/dashboard/receiving/grn", SortOrder = 5 },
|
||||
new NavItem { NavItemId = 6, Code = "stock", Label = "Stock", Href = "/dashboard/stock", SortOrder = 6 },
|
||||
new NavItem { NavItemId = 7, Code = "warehouses", Label = "Warehouses", Href = "/dashboard/warehouse", SortOrder = 7 },
|
||||
new NavItem { NavItemId = 8, Code = "orders", Label = "Orders", Href = "/dashboard/orders", SortOrder = 8 },
|
||||
new NavItem { NavItemId = 9, Code = "settings", Label = "Settings", Href = "/dashboard/settings", SortOrder = 9 },
|
||||
new NavItem { NavItemId = 10, Code = "help", Label = "Help", Href = "/dashboard/help", SortOrder = 10 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
/// <summary>
|
||||
/// One row per <see cref="NavItem"/>/<see cref="SubNavItem"/>, seeded in lockstep
|
||||
/// with <see cref="NavItemConfiguration"/>/<see cref="SubNavItemConfiguration"/>.
|
||||
/// </summary>
|
||||
public sealed class PermissionConfiguration : IEntityTypeConfiguration<Permission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Permission> builder)
|
||||
{
|
||||
builder.ToTable("permissions");
|
||||
builder.HasKey(p => p.PermissionId);
|
||||
|
||||
builder.Property(p => p.Code).IsRequired().HasMaxLength(80);
|
||||
builder.HasIndex(p => p.Code).IsUnique();
|
||||
|
||||
builder.HasOne(p => p.NavItem).WithMany()
|
||||
.HasForeignKey(p => p.NavItemId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(p => p.SubNavItem).WithMany()
|
||||
.HasForeignKey(p => p.SubNavItemId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasData(
|
||||
new Permission { PermissionId = 1, Code = "NAV:dashboard", NavItemId = 1 },
|
||||
new Permission { PermissionId = 2, Code = "NAV:products", NavItemId = 2 },
|
||||
new Permission { PermissionId = 3, Code = "NAV:vendors", NavItemId = 3 },
|
||||
new Permission { PermissionId = 4, Code = "NAV:procurement", NavItemId = 4 },
|
||||
new Permission { PermissionId = 5, Code = "NAV:receiving", NavItemId = 5 },
|
||||
new Permission { PermissionId = 6, Code = "NAV:stock", NavItemId = 6 },
|
||||
new Permission { PermissionId = 7, Code = "NAV:warehouses", NavItemId = 7 },
|
||||
new Permission { PermissionId = 8, Code = "NAV:orders", NavItemId = 8 },
|
||||
new Permission { PermissionId = 9, Code = "NAV:settings", NavItemId = 9 },
|
||||
new Permission { PermissionId = 10, Code = "NAV:help", NavItemId = 10 },
|
||||
new Permission { PermissionId = 11, Code = "NAV:products.item", SubNavItemId = 1 },
|
||||
new Permission { PermissionId = 12, Code = "NAV:products.category", SubNavItemId = 2 },
|
||||
new Permission { PermissionId = 13, Code = "NAV:products.brand", SubNavItemId = 3 },
|
||||
new Permission { PermissionId = 14, Code = "NAV:products.item-type", SubNavItemId = 4 },
|
||||
new Permission { PermissionId = 15, Code = "NAV:products.uom", SubNavItemId = 5 },
|
||||
new Permission { PermissionId = 16, Code = "NAV:products.configuration", SubNavItemId = 6 },
|
||||
new Permission { PermissionId = 17, Code = "NAV:settings.roles", SubNavItemId = 7 },
|
||||
new Permission { PermissionId = 18, Code = "NAV:settings.users", SubNavItemId = 8 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class RoleConfiguration : IEntityTypeConfiguration<Role>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.ToTable("roles");
|
||||
builder.HasKey(r => r.RoleId);
|
||||
|
||||
builder.Property(r => r.AuthRoleId).HasColumnName("auth_role_id").IsRequired();
|
||||
builder.HasIndex(r => r.AuthRoleId).IsUnique();
|
||||
|
||||
builder.Property(r => r.Code).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(r => r.Code).IsUnique();
|
||||
|
||||
builder.Property(r => r.Name).IsRequired().HasMaxLength(200);
|
||||
builder.Property(r => r.IsSystemRole).IsRequired().HasDefaultValue(false);
|
||||
|
||||
builder.Property(r => r.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
|
||||
builder.Property(r => r.CreatedAt).IsRequired();
|
||||
|
||||
// PostgreSQL xmin system column as the optimistic concurrency token (ETag).
|
||||
builder.Property(r => r.RowVersion).IsRowVersion();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class RolePermissionConfiguration : IEntityTypeConfiguration<RolePermission>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<RolePermission> builder)
|
||||
{
|
||||
builder.ToTable("role_permissions");
|
||||
builder.HasKey(rp => new { rp.RoleId, rp.PermissionId });
|
||||
|
||||
builder.HasOne(rp => rp.Role).WithMany()
|
||||
.HasForeignKey(rp => rp.RoleId).OnDelete(DeleteBehavior.Cascade);
|
||||
builder.HasOne(rp => rp.Permission).WithMany()
|
||||
.HasForeignKey(rp => rp.PermissionId).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Configurations;
|
||||
|
||||
public sealed class SubNavItemConfiguration : IEntityTypeConfiguration<SubNavItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<SubNavItem> builder)
|
||||
{
|
||||
builder.ToTable("sub_nav_items");
|
||||
builder.HasKey(n => n.SubNavItemId);
|
||||
|
||||
builder.Property(n => n.Code).IsRequired().HasMaxLength(50);
|
||||
builder.HasIndex(n => n.Code).IsUnique();
|
||||
builder.Property(n => n.Label).IsRequired().HasMaxLength(100);
|
||||
builder.Property(n => n.Icon).HasMaxLength(50);
|
||||
builder.Property(n => n.Href).HasMaxLength(200);
|
||||
builder.Property(n => n.Status)
|
||||
.HasConversion<string>().HasMaxLength(20).IsRequired()
|
||||
.HasDefaultValue(EntityStatus.Active);
|
||||
|
||||
builder.HasOne(n => n.NavItem).WithMany(n => n.Children)
|
||||
.HasForeignKey(n => n.NavItemId).OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.HasData(
|
||||
new SubNavItem { SubNavItemId = 1, NavItemId = 2, Code = "products.item", Label = "Item", Href = "/dashboard/products", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 2, NavItemId = 2, Code = "products.category", Label = "Category", Href = "/dashboard/products/categories", SortOrder = 2 },
|
||||
new SubNavItem { SubNavItemId = 3, NavItemId = 2, Code = "products.brand", Label = "Brand", Href = "/dashboard/products/brands", SortOrder = 3 },
|
||||
new SubNavItem { SubNavItemId = 4, NavItemId = 2, Code = "products.item-type", Label = "Item Type", Href = "/dashboard/products/item-types", SortOrder = 4 },
|
||||
new SubNavItem { SubNavItemId = 5, NavItemId = 2, Code = "products.uom", Label = "UOM", Href = "/dashboard/products/uoms", SortOrder = 5 },
|
||||
new SubNavItem { SubNavItemId = 6, NavItemId = 2, Code = "products.configuration", Label = "Configuration", Href = "/dashboard/products/settings", SortOrder = 6 },
|
||||
new SubNavItem { SubNavItemId = 7, NavItemId = 9, Code = "settings.roles", Label = "Roles", Href = "/dashboard/settings/roles", SortOrder = 1 },
|
||||
new SubNavItem { SubNavItemId = 8, NavItemId = 9, Code = "settings.users", Label = "Users", Href = "/dashboard/settings/users", SortOrder = 2 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,10 @@ public sealed class UserConfiguration : IEntityTypeConfiguration<User>
|
||||
builder.Property(u => u.AuthUserId).HasColumnName("auth_user_id");
|
||||
builder.HasIndex(u => u.AuthUserId).IsUnique();
|
||||
|
||||
// Local shadow Role assignment (nullable — unset until an admin assigns one).
|
||||
builder.HasOne(u => u.Role).WithMany()
|
||||
.HasForeignKey(u => u.RoleId).OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
// Seeded fallback audit actor while auth is deferred (§6).
|
||||
builder.HasData(new User
|
||||
{
|
||||
|
||||
@@ -41,6 +41,13 @@ public class ErpDbContext : DbContext
|
||||
public DbSet<User> Users => Set<User>();
|
||||
public DbSet<NumberSequence> NumberSequences => Set<NumberSequence>();
|
||||
|
||||
// --- RBAC / sidebar (docs/10 Part C.8) ---
|
||||
public DbSet<Role> Roles => Set<Role>();
|
||||
public DbSet<NavItem> NavItems => Set<NavItem>();
|
||||
public DbSet<SubNavItem> SubNavItems => Set<SubNavItem>();
|
||||
public DbSet<Permission> Permissions => Set<Permission>();
|
||||
public DbSet<RolePermission> RolePermissions => Set<RolePermission>();
|
||||
|
||||
// --- Procurement (docs/10 Part C.2) ---
|
||||
public DbSet<Requisition> Requisitions => Set<Requisition>();
|
||||
public DbSet<RequisitionLine> RequisitionLines => Set<RequisitionLine>();
|
||||
|
||||
+2454
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ini2 : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+3001
File diff suppressed because it is too large
Load Diff
+303
@@ -0,0 +1,303 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace ERPCore.Infra.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRolesNavPermissions : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "RoleId",
|
||||
table: "users",
|
||||
type: "integer",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "nav_items",
|
||||
columns: table => new
|
||||
{
|
||||
NavItemId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Label = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Icon = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
Href = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_nav_items", x => x.NavItemId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "roles",
|
||||
columns: table => new
|
||||
{
|
||||
RoleId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
auth_role_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
IsSystemRole = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active"),
|
||||
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_roles", x => x.RoleId);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "sub_nav_items",
|
||||
columns: table => new
|
||||
{
|
||||
SubNavItemId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
NavItemId = table.Column<int>(type: "integer", nullable: false),
|
||||
Code = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
Label = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Icon = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
|
||||
Href = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false, defaultValue: "Active")
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_sub_nav_items", x => x.SubNavItemId);
|
||||
table.ForeignKey(
|
||||
name: "FK_sub_nav_items_nav_items_NavItemId",
|
||||
column: x => x.NavItemId,
|
||||
principalTable: "nav_items",
|
||||
principalColumn: "NavItemId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "permissions",
|
||||
columns: table => new
|
||||
{
|
||||
PermissionId = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Code = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
|
||||
NavItemId = table.Column<int>(type: "integer", nullable: true),
|
||||
SubNavItemId = table.Column<int>(type: "integer", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_permissions", x => x.PermissionId);
|
||||
table.ForeignKey(
|
||||
name: "FK_permissions_nav_items_NavItemId",
|
||||
column: x => x.NavItemId,
|
||||
principalTable: "nav_items",
|
||||
principalColumn: "NavItemId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_permissions_sub_nav_items_SubNavItemId",
|
||||
column: x => x.SubNavItemId,
|
||||
principalTable: "sub_nav_items",
|
||||
principalColumn: "SubNavItemId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "role_permissions",
|
||||
columns: table => new
|
||||
{
|
||||
RoleId = table.Column<int>(type: "integer", nullable: false),
|
||||
PermissionId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_role_permissions", x => new { x.RoleId, x.PermissionId });
|
||||
table.ForeignKey(
|
||||
name: "FK_role_permissions_permissions_PermissionId",
|
||||
column: x => x.PermissionId,
|
||||
principalTable: "permissions",
|
||||
principalColumn: "PermissionId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_role_permissions_roles_RoleId",
|
||||
column: x => x.RoleId,
|
||||
principalTable: "roles",
|
||||
principalColumn: "RoleId",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "nav_items",
|
||||
columns: new[] { "NavItemId", "Code", "Href", "Icon", "Label", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "dashboard", "/dashboard", null, "Dashboard", 1 },
|
||||
{ 2, "products", "/dashboard/products", null, "Products", 2 },
|
||||
{ 3, "vendors", "/dashboard/vendors", null, "Vendors", 3 },
|
||||
{ 4, "procurement", "/dashboard/procurement", null, "Procurement", 4 },
|
||||
{ 5, "receiving", "/dashboard/receiving/grn", null, "Receiving", 5 },
|
||||
{ 6, "stock", "/dashboard/stock", null, "Stock", 6 },
|
||||
{ 7, "warehouses", "/dashboard/warehouse", null, "Warehouses", 7 },
|
||||
{ 8, "orders", "/dashboard/orders", null, "Orders", 8 },
|
||||
{ 9, "settings", "/dashboard/settings", null, "Settings", 9 },
|
||||
{ 10, "help", "/dashboard/help", null, "Help", 10 }
|
||||
});
|
||||
|
||||
migrationBuilder.UpdateData(
|
||||
table: "users",
|
||||
keyColumn: "UserId",
|
||||
keyValue: 1,
|
||||
column: "RoleId",
|
||||
value: null);
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "permissions",
|
||||
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "NAV:dashboard", 1, null },
|
||||
{ 2, "NAV:products", 2, null },
|
||||
{ 3, "NAV:vendors", 3, null },
|
||||
{ 4, "NAV:procurement", 4, null },
|
||||
{ 5, "NAV:receiving", 5, null },
|
||||
{ 6, "NAV:stock", 6, null },
|
||||
{ 7, "NAV:warehouses", 7, null },
|
||||
{ 8, "NAV:orders", 8, null },
|
||||
{ 9, "NAV:settings", 9, null },
|
||||
{ 10, "NAV:help", 10, null }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "sub_nav_items",
|
||||
columns: new[] { "SubNavItemId", "Code", "Href", "Icon", "Label", "NavItemId", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1, "products.item", "/dashboard/products", null, "Item", 2, 1 },
|
||||
{ 2, "products.category", "/dashboard/products/categories", null, "Category", 2, 2 },
|
||||
{ 3, "products.brand", "/dashboard/products/brands", null, "Brand", 2, 3 },
|
||||
{ 4, "products.item-type", "/dashboard/products/item-types", null, "Item Type", 2, 4 },
|
||||
{ 5, "products.uom", "/dashboard/products/uoms", null, "UOM", 2, 5 },
|
||||
{ 6, "products.configuration", "/dashboard/products/settings", null, "Configuration", 2, 6 },
|
||||
{ 7, "settings.roles", "/dashboard/settings/roles", null, "Roles", 9, 1 },
|
||||
{ 8, "settings.users", "/dashboard/settings/users", null, "Users", 9, 2 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
table: "permissions",
|
||||
columns: new[] { "PermissionId", "Code", "NavItemId", "SubNavItemId" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 11, "NAV:products.item", null, 1 },
|
||||
{ 12, "NAV:products.category", null, 2 },
|
||||
{ 13, "NAV:products.brand", null, 3 },
|
||||
{ 14, "NAV:products.item-type", null, 4 },
|
||||
{ 15, "NAV:products.uom", null, 5 },
|
||||
{ 16, "NAV:products.configuration", null, 6 },
|
||||
{ 17, "NAV:settings.roles", null, 7 },
|
||||
{ 18, "NAV:settings.users", null, 8 }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_users_RoleId",
|
||||
table: "users",
|
||||
column: "RoleId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_nav_items_Code",
|
||||
table: "nav_items",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_permissions_Code",
|
||||
table: "permissions",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_permissions_NavItemId",
|
||||
table: "permissions",
|
||||
column: "NavItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_permissions_SubNavItemId",
|
||||
table: "permissions",
|
||||
column: "SubNavItemId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_role_permissions_PermissionId",
|
||||
table: "role_permissions",
|
||||
column: "PermissionId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_roles_auth_role_id",
|
||||
table: "roles",
|
||||
column: "auth_role_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_roles_Code",
|
||||
table: "roles",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_sub_nav_items_Code",
|
||||
table: "sub_nav_items",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_sub_nav_items_NavItemId",
|
||||
table: "sub_nav_items",
|
||||
column: "NavItemId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_users_roles_RoleId",
|
||||
table: "users",
|
||||
column: "RoleId",
|
||||
principalTable: "roles",
|
||||
principalColumn: "RoleId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_users_roles_RoleId",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "role_permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "permissions");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "roles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "sub_nav_items");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "nav_items");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_users_RoleId",
|
||||
table: "users");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RoleId",
|
||||
table: "users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -524,6 +524,142 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("journal_entry_stubs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b =>
|
||||
{
|
||||
b.Property<int>("NavItemId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("NavItemId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Href")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.HasKey("NavItemId");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("nav_items", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
NavItemId = 1,
|
||||
Code = "dashboard",
|
||||
Href = "/dashboard",
|
||||
Label = "Dashboard",
|
||||
SortOrder = 1,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 2,
|
||||
Code = "products",
|
||||
Href = "/dashboard/products",
|
||||
Label = "Products",
|
||||
SortOrder = 2,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 3,
|
||||
Code = "vendors",
|
||||
Href = "/dashboard/vendors",
|
||||
Label = "Vendors",
|
||||
SortOrder = 3,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 4,
|
||||
Code = "procurement",
|
||||
Href = "/dashboard/procurement",
|
||||
Label = "Procurement",
|
||||
SortOrder = 4,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 5,
|
||||
Code = "receiving",
|
||||
Href = "/dashboard/receiving/grn",
|
||||
Label = "Receiving",
|
||||
SortOrder = 5,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 6,
|
||||
Code = "stock",
|
||||
Href = "/dashboard/stock",
|
||||
Label = "Stock",
|
||||
SortOrder = 6,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 7,
|
||||
Code = "warehouses",
|
||||
Href = "/dashboard/warehouse",
|
||||
Label = "Warehouses",
|
||||
SortOrder = 7,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 8,
|
||||
Code = "orders",
|
||||
Href = "/dashboard/orders",
|
||||
Label = "Orders",
|
||||
SortOrder = 8,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 9,
|
||||
Code = "settings",
|
||||
Href = "/dashboard/settings",
|
||||
Label = "Settings",
|
||||
SortOrder = 9,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
NavItemId = 10,
|
||||
Code = "help",
|
||||
Href = "/dashboard/help",
|
||||
Label = "Help",
|
||||
SortOrder = 10,
|
||||
Status = "Active"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b =>
|
||||
{
|
||||
b.Property<int>("SequenceId")
|
||||
@@ -554,6 +690,147 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("number_sequences", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b =>
|
||||
{
|
||||
b.Property<int>("PermissionId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("PermissionId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(80)
|
||||
.HasColumnType("character varying(80)");
|
||||
|
||||
b.Property<int?>("NavItemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int?>("SubNavItemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("PermissionId");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("NavItemId");
|
||||
|
||||
b.HasIndex("SubNavItemId");
|
||||
|
||||
b.ToTable("permissions", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
PermissionId = 1,
|
||||
Code = "NAV:dashboard",
|
||||
NavItemId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 2,
|
||||
Code = "NAV:products",
|
||||
NavItemId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 3,
|
||||
Code = "NAV:vendors",
|
||||
NavItemId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 4,
|
||||
Code = "NAV:procurement",
|
||||
NavItemId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 5,
|
||||
Code = "NAV:receiving",
|
||||
NavItemId = 5
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 6,
|
||||
Code = "NAV:stock",
|
||||
NavItemId = 6
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 7,
|
||||
Code = "NAV:warehouses",
|
||||
NavItemId = 7
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 8,
|
||||
Code = "NAV:orders",
|
||||
NavItemId = 8
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 9,
|
||||
Code = "NAV:settings",
|
||||
NavItemId = 9
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 10,
|
||||
Code = "NAV:help",
|
||||
NavItemId = 10
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 11,
|
||||
Code = "NAV:products.item",
|
||||
SubNavItemId = 1
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 12,
|
||||
Code = "NAV:products.category",
|
||||
SubNavItemId = 2
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 13,
|
||||
Code = "NAV:products.brand",
|
||||
SubNavItemId = 3
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 14,
|
||||
Code = "NAV:products.item-type",
|
||||
SubNavItemId = 4
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 15,
|
||||
Code = "NAV:products.uom",
|
||||
SubNavItemId = 5
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 16,
|
||||
Code = "NAV:products.configuration",
|
||||
SubNavItemId = 6
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 17,
|
||||
Code = "NAV:settings.roles",
|
||||
SubNavItemId = 7
|
||||
},
|
||||
new
|
||||
{
|
||||
PermissionId = 18,
|
||||
Code = "NAV:settings.users",
|
||||
SubNavItemId = 8
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b =>
|
||||
{
|
||||
b.Property<int>("PoLineId")
|
||||
@@ -942,6 +1219,78 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("rfq_lines", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Role", b =>
|
||||
{
|
||||
b.Property<int>("RoleId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("RoleId"));
|
||||
|
||||
b.Property<Guid>("AuthRoleId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("auth_role_id");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsSystemRole")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<uint>("RowVersion")
|
||||
.IsConcurrencyToken()
|
||||
.ValueGeneratedOnAddOrUpdate()
|
||||
.HasColumnType("xid")
|
||||
.HasColumnName("xmin");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("RoleId");
|
||||
|
||||
b.HasIndex("AuthRoleId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("roles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b =>
|
||||
{
|
||||
b.Property<int>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("PermissionId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("RoleId", "PermissionId");
|
||||
|
||||
b.HasIndex("PermissionId");
|
||||
|
||||
b.ToTable("role_permissions", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b =>
|
||||
{
|
||||
b.Property<int>("SerialId")
|
||||
@@ -1439,6 +1788,137 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.ToTable("subcategories", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b =>
|
||||
{
|
||||
b.Property<int>("SubNavItemId")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("SubNavItemId"));
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Href")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("Icon")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<int>("NavItemId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)")
|
||||
.HasDefaultValue("Active");
|
||||
|
||||
b.HasKey("SubNavItemId");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("NavItemId");
|
||||
|
||||
b.ToTable("sub_nav_items", (string)null);
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
SubNavItemId = 1,
|
||||
Code = "products.item",
|
||||
Href = "/dashboard/products",
|
||||
Label = "Item",
|
||||
NavItemId = 2,
|
||||
SortOrder = 1,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 2,
|
||||
Code = "products.category",
|
||||
Href = "/dashboard/products/categories",
|
||||
Label = "Category",
|
||||
NavItemId = 2,
|
||||
SortOrder = 2,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 3,
|
||||
Code = "products.brand",
|
||||
Href = "/dashboard/products/brands",
|
||||
Label = "Brand",
|
||||
NavItemId = 2,
|
||||
SortOrder = 3,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 4,
|
||||
Code = "products.item-type",
|
||||
Href = "/dashboard/products/item-types",
|
||||
Label = "Item Type",
|
||||
NavItemId = 2,
|
||||
SortOrder = 4,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 5,
|
||||
Code = "products.uom",
|
||||
Href = "/dashboard/products/uoms",
|
||||
Label = "UOM",
|
||||
NavItemId = 2,
|
||||
SortOrder = 5,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 6,
|
||||
Code = "products.configuration",
|
||||
Href = "/dashboard/products/settings",
|
||||
Label = "Configuration",
|
||||
NavItemId = 2,
|
||||
SortOrder = 6,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 7,
|
||||
Code = "settings.roles",
|
||||
Href = "/dashboard/settings/roles",
|
||||
Label = "Roles",
|
||||
NavItemId = 9,
|
||||
SortOrder = 1,
|
||||
Status = "Active"
|
||||
},
|
||||
new
|
||||
{
|
||||
SubNavItemId = 8,
|
||||
Code = "settings.users",
|
||||
Href = "/dashboard/settings/users",
|
||||
Label = "Users",
|
||||
NavItemId = 9,
|
||||
SortOrder = 2,
|
||||
Status = "Active"
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b =>
|
||||
{
|
||||
b.Property<int>("UomId")
|
||||
@@ -1510,6 +1990,9 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<int?>("RoleId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
@@ -1525,6 +2008,8 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.HasIndex("AuthUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.HasIndex("Username")
|
||||
.IsUnique();
|
||||
|
||||
@@ -1857,6 +2342,23 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.Navigation("Warehouse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("NavItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.SubNavItem", "SubNavItem")
|
||||
.WithMany()
|
||||
.HasForeignKey("SubNavItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.Navigation("NavItem");
|
||||
|
||||
b.Navigation("SubNavItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
@@ -2049,6 +2551,25 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.Navigation("Rfq");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Permission", "Permission")
|
||||
.WithMany()
|
||||
.HasForeignKey("PermissionId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ERPCore.Domain.Entities.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Permission");
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Item", "Item")
|
||||
@@ -2317,6 +2838,17 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.Navigation("Category");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem")
|
||||
.WithMany("Children")
|
||||
.HasForeignKey("NavItemId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("NavItem");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.UomConversion", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Uom", "FromUom")
|
||||
@@ -2344,6 +2876,16 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.Navigation("ToUom");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.User", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Role", "Role")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
b.Navigation("Role");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b =>
|
||||
{
|
||||
b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq")
|
||||
@@ -2399,6 +2941,11 @@ namespace ERPCore.Infra.Persistence.Migrations
|
||||
b.Navigation("UomConversions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b =>
|
||||
{
|
||||
b.Navigation("Children");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b =>
|
||||
{
|
||||
b.Navigation("Lines");
|
||||
|
||||
@@ -68,6 +68,10 @@ builder.Services.AddScoped<IProductConfigService, ProductConfigService>();
|
||||
builder.Services.AddScoped<IVendorService, VendorService>();
|
||||
builder.Services.AddScoped<IWarehouseService, WarehouseService>();
|
||||
|
||||
// RBAC / sidebar: Role (shadow of AuthHex) + Permission assignment + user management
|
||||
builder.Services.AddScoped<IRoleService, RoleService>();
|
||||
builder.Services.AddScoped<IUserManagementService, UserManagementService>();
|
||||
|
||||
// Cross-cutting + procurement services (docs/11 §3)
|
||||
builder.Services.AddScoped<INumberSequenceService, NumberSequenceService>();
|
||||
builder.Services.AddScoped<IRequisitionService, RequisitionService>();
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Role CRUD + permission assignment. AuthHex is the source of truth for Role
|
||||
/// identity (docs/10 C.9 "shadow user" pattern, applied to Role): every write is
|
||||
/// forwarded to AuthHex first, then mirrored into the local shadow <c>Role</c> row.
|
||||
/// Permission assignment is purely local (ERPCore/UI concern, not an AuthHex one).
|
||||
/// </summary>
|
||||
public interface IRoleService
|
||||
{
|
||||
Task<PagedResponse<RoleDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default);
|
||||
Task<ETagged<RoleDto>?> GetAsync(int roleId, CancellationToken ct = default);
|
||||
Task<ETagged<RoleDto>> CreateAsync(CreateRoleRequest request, CancellationToken ct = default);
|
||||
Task<ETagged<RoleDto>> UpdateAsync(int roleId, UpdateRoleRequest request, uint expectedRowVersion, CancellationToken ct = default);
|
||||
Task SetStatusAsync(int roleId, EntityStatus status, CancellationToken ct = default);
|
||||
Task DeleteAsync(int roleId, CancellationToken ct = default);
|
||||
|
||||
Task<RolePermissionsDto> GetPermissionsAsync(int roleId, CancellationToken ct = default);
|
||||
Task<RolePermissionsDto> AssignPermissionsAsync(int roleId, AssignRolePermissionsRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Resolves the nav codes a role (by AuthHex `RoleCode` claim) may see. Used by `GET /auth/me`.</summary>
|
||||
Task<MeResponseDto> GetMeAsync(string? roleCode, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Users;
|
||||
|
||||
namespace ERPCore.Services.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Admin-facing user management: list/create/reassign-role against the local
|
||||
/// shadow `User` table, orchestrating account creation in AuthHex too (see
|
||||
/// <see cref="CreateUserRequest"/>).
|
||||
/// </summary>
|
||||
public interface IUserManagementService
|
||||
{
|
||||
Task<PagedResponse<ManagedUserDto>> ListAsync(PageQuery query, CancellationToken ct = default);
|
||||
Task<ManagedUserDto?> GetAsync(int userId, CancellationToken ct = default);
|
||||
Task<ManagedUserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default);
|
||||
Task<ManagedUserDto> UpdateRoleAsync(int userId, UpdateUserRoleRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>AuthHex UserType options for the create-user form's select.</summary>
|
||||
Task<List<UserTypeOptionDto>> ListUserTypesAsync(CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using ERPCore.Common.Http;
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Rbac;
|
||||
using ERPCore.Infra.Auth.AuthHex;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class RoleService : IRoleService
|
||||
{
|
||||
private readonly IRepository<Role> _roles;
|
||||
private readonly IRepository<RolePermission> _rolePermissions;
|
||||
private readonly IRepository<Permission> _permissions;
|
||||
private readonly IAuthHexClient _authHex;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public RoleService(
|
||||
IRepository<Role> roles, IRepository<RolePermission> rolePermissions, IRepository<Permission> permissions,
|
||||
IAuthHexClient authHex, IUnitOfWork uow)
|
||||
{
|
||||
_roles = roles;
|
||||
_rolePermissions = rolePermissions;
|
||||
_permissions = permissions;
|
||||
_authHex = authHex;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<RoleDto>> ListAsync(PageQuery query, EntityStatus? status, CancellationToken ct = default)
|
||||
{
|
||||
var q = _roles.Query().AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(r => EF.Functions.ILike(r.Code, $"%{term}%") || EF.Functions.ILike(r.Name, $"%{term}%"));
|
||||
}
|
||||
if (status is not null) q = q.Where(r => r.Status == status);
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(r => r.Code)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<RoleDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ETagged<RoleDto>?> GetAsync(int roleId, CancellationToken ct = default)
|
||||
{
|
||||
var role = await _roles.Query().AsNoTracking().FirstOrDefaultAsync(r => r.RoleId == roleId, ct);
|
||||
return role is null ? null : new ETagged<RoleDto>(Map(role), role.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<RoleDto>> CreateAsync(CreateRoleRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var code = request.Code.Trim();
|
||||
if (await _roles.Query().AnyAsync(r => r.Code == code, ct))
|
||||
throw new ConflictException($"A role with code '{code}' already exists.");
|
||||
|
||||
var authRole = await _authHex.CreateRoleAsync(
|
||||
new CreateAuthHexRoleRequest { Code = code, Name = request.Name.Trim() }, ct);
|
||||
|
||||
var role = new Role
|
||||
{
|
||||
AuthRoleId = authRole.RoleId,
|
||||
Code = code,
|
||||
Name = request.Name.Trim(),
|
||||
IsSystemRole = authRole.IsSystemRole ?? false,
|
||||
Status = EntityStatus.Active,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _roles.AddAsync(role, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return new ETagged<RoleDto>(Map(role), role.RowVersion);
|
||||
}
|
||||
|
||||
public async Task<ETagged<RoleDto>> UpdateAsync(
|
||||
int roleId, UpdateRoleRequest request, uint expectedRowVersion, CancellationToken ct = default)
|
||||
{
|
||||
var role = await _roles.GetByIdAsync(roleId, ct)
|
||||
?? throw new NotFoundException($"Role {roleId} was not found.");
|
||||
|
||||
if (role.RowVersion != expectedRowVersion)
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The role was modified by another request.", 412);
|
||||
|
||||
var code = request.Code.Trim();
|
||||
if (!string.Equals(role.Code, code, StringComparison.Ordinal)
|
||||
&& await _roles.Query().AnyAsync(r => r.Code == code && r.RoleId != roleId, ct))
|
||||
throw new ConflictException($"A role with code '{code}' already exists.");
|
||||
|
||||
await _authHex.UpdateRoleAsync(
|
||||
new UpdateAuthHexRoleRequest { RoleId = role.AuthRoleId, Code = code, Name = request.Name.Trim() }, ct);
|
||||
|
||||
role.Code = code;
|
||||
role.Name = request.Name.Trim();
|
||||
role.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
try
|
||||
{
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
throw new DomainException(ErrorCodes.ConcurrencyConflict, "The role was modified by another request.", 412);
|
||||
}
|
||||
|
||||
return new ETagged<RoleDto>(Map(role), role.RowVersion);
|
||||
}
|
||||
|
||||
public async Task SetStatusAsync(int roleId, EntityStatus status, CancellationToken ct = default)
|
||||
{
|
||||
var role = await _roles.GetByIdAsync(roleId, ct)
|
||||
?? throw new NotFoundException($"Role {roleId} was not found.");
|
||||
|
||||
role.Status = status;
|
||||
role.UpdatedAt = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(int roleId, CancellationToken ct = default)
|
||||
{
|
||||
var role = await _roles.GetByIdAsync(roleId, ct)
|
||||
?? throw new NotFoundException($"Role {roleId} was not found.");
|
||||
|
||||
try
|
||||
{
|
||||
await _authHex.DeleteRoleAsync(role.AuthRoleId, ct);
|
||||
}
|
||||
catch (DomainException ex) when (ex.Message.Contains("ROLE_IN_USE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new DomainException(ErrorCodes.RoleInUse, "This role is assigned to one or more users.", 409);
|
||||
}
|
||||
|
||||
_roles.Remove(role);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
}
|
||||
|
||||
public async Task<RolePermissionsDto> GetPermissionsAsync(int roleId, CancellationToken ct = default)
|
||||
{
|
||||
_ = await _roles.GetByIdAsync(roleId, ct) ?? throw new NotFoundException($"Role {roleId} was not found.");
|
||||
|
||||
var granted = await _rolePermissions.Query().AsNoTracking()
|
||||
.Where(rp => rp.RoleId == roleId)
|
||||
.Include(rp => rp.Permission)
|
||||
.Select(rp => rp.Permission!)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return new RolePermissionsDto(
|
||||
roleId,
|
||||
granted.Where(p => p.NavItemId is not null).Select(p => p.NavItemId!.Value).ToList(),
|
||||
granted.Where(p => p.SubNavItemId is not null).Select(p => p.SubNavItemId!.Value).ToList());
|
||||
}
|
||||
|
||||
public async Task<RolePermissionsDto> AssignPermissionsAsync(int roleId, AssignRolePermissionsRequest request, CancellationToken ct = default)
|
||||
{
|
||||
_ = await _roles.GetByIdAsync(roleId, ct) ?? throw new NotFoundException($"Role {roleId} was not found.");
|
||||
|
||||
var existing = await _rolePermissions.Query().Where(rp => rp.RoleId == roleId).ToListAsync(ct);
|
||||
foreach (var rp in existing) _rolePermissions.Remove(rp);
|
||||
|
||||
var navIds = request.NavItemIds.Distinct().ToList();
|
||||
var subNavIds = request.SubNavItemIds.Distinct().ToList();
|
||||
|
||||
var permissionIds = await _permissions.Query().AsNoTracking()
|
||||
.Where(p => (p.NavItemId != null && navIds.Contains(p.NavItemId.Value))
|
||||
|| (p.SubNavItemId != null && subNavIds.Contains(p.SubNavItemId.Value)))
|
||||
.Select(p => p.PermissionId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
foreach (var permissionId in permissionIds)
|
||||
await _rolePermissions.AddAsync(new RolePermission { RoleId = roleId, PermissionId = permissionId }, ct);
|
||||
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
return await GetPermissionsAsync(roleId, ct);
|
||||
}
|
||||
|
||||
public async Task<MeResponseDto> GetMeAsync(string? roleCode, CancellationToken ct = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(roleCode))
|
||||
return new MeResponseDto(null, null, Array.Empty<string>());
|
||||
|
||||
var role = await _roles.Query().AsNoTracking()
|
||||
.FirstOrDefaultAsync(r => r.Code == roleCode, ct);
|
||||
if (role is null)
|
||||
return new MeResponseDto(roleCode, null, Array.Empty<string>());
|
||||
|
||||
var permissions = await _rolePermissions.Query().AsNoTracking()
|
||||
.Where(rp => rp.RoleId == role.RoleId)
|
||||
.Include(rp => rp.Permission!).ThenInclude(p => p.NavItem)
|
||||
.Include(rp => rp.Permission!).ThenInclude(p => p.SubNavItem)
|
||||
.Select(rp => rp.Permission!)
|
||||
.ToListAsync(ct);
|
||||
|
||||
var navCodes = permissions
|
||||
.Select(p => p.NavItem?.Code ?? p.SubNavItem?.Code)
|
||||
.Where(code => code is not null)
|
||||
.Select(code => code!)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
return new MeResponseDto(role.Code, role.Name, navCodes);
|
||||
}
|
||||
|
||||
private static RoleDto Map(Role r) => new(
|
||||
r.RoleId, r.Code, r.Name, r.IsSystemRole, r.Status, r.CreatedAt, r.UpdatedAt);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
using ERPCore.Domain.Entities;
|
||||
using ERPCore.Domain.Enums;
|
||||
using ERPCore.Dtos.Auth;
|
||||
using ERPCore.Dtos.Common;
|
||||
using ERPCore.Dtos.Users;
|
||||
using ERPCore.Infra.Auth.AuthHex;
|
||||
using ERPCore.Infra.UoW;
|
||||
using ERPCore.Repositories.Interfaces;
|
||||
using ERPCore.Services.Interfaces;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ERPCore.Services;
|
||||
|
||||
public sealed class UserManagementService : IUserManagementService
|
||||
{
|
||||
private readonly IRepository<User> _users;
|
||||
private readonly IRepository<Role> _roles;
|
||||
private readonly IAuthUserService _authUsers;
|
||||
private readonly IAuthHexClient _authHex;
|
||||
private readonly IUnitOfWork _uow;
|
||||
|
||||
public UserManagementService(
|
||||
IRepository<User> users, IRepository<Role> roles, IAuthUserService authUsers, IAuthHexClient authHex, IUnitOfWork uow)
|
||||
{
|
||||
_users = users;
|
||||
_roles = roles;
|
||||
_authUsers = authUsers;
|
||||
_authHex = authHex;
|
||||
_uow = uow;
|
||||
}
|
||||
|
||||
public async Task<PagedResponse<ManagedUserDto>> ListAsync(PageQuery query, CancellationToken ct = default)
|
||||
{
|
||||
IQueryable<User> q = _users.Query().AsNoTracking().Include(u => u.Role);
|
||||
if (!string.IsNullOrWhiteSpace(query.Q))
|
||||
{
|
||||
var term = query.Q.Trim();
|
||||
q = q.Where(u => EF.Functions.ILike(u.Username, $"%{term}%") || EF.Functions.ILike(u.DisplayName, $"%{term}%"));
|
||||
}
|
||||
|
||||
var total = await q.CountAsync(ct);
|
||||
var rows = await q.OrderBy(u => u.Username)
|
||||
.Skip(query.Skip).Take(query.PageSize)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return PagedResponse<ManagedUserDto>.Create(rows.Select(Map).ToList(), query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
public async Task<ManagedUserDto?> GetAsync(int userId, CancellationToken ct = default)
|
||||
{
|
||||
var user = await _users.Query().AsNoTracking().Include(u => u.Role)
|
||||
.FirstOrDefaultAsync(u => u.UserId == userId, ct);
|
||||
return user is null ? null : Map(user);
|
||||
}
|
||||
|
||||
public async Task<ManagedUserDto> CreateAsync(CreateUserRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var role = await _roles.GetByIdAsync(request.RoleId, ct)
|
||||
?? throw new NotFoundException($"Role {request.RoleId} was not found.");
|
||||
|
||||
var username = request.Username.Trim();
|
||||
if (await _users.Query().AnyAsync(u => u.Username == username, ct))
|
||||
throw new ConflictException($"A user with username '{username}' already exists.");
|
||||
|
||||
var authUserId = Guid.NewGuid();
|
||||
|
||||
// Source of truth: AuthHex creates the credential + emails it (registerUser,
|
||||
// ERP_Auth_Service/Services/UserManager/UserManagerService.cs).
|
||||
await _authUsers.RegisterAsync(new RegisterRequest
|
||||
{
|
||||
UserId = authUserId,
|
||||
RoleId = role.AuthRoleId,
|
||||
UserTypeId = request.UserTypeId,
|
||||
Fullname = request.FullName.Trim(),
|
||||
UserName = username,
|
||||
Nic = request.Nic,
|
||||
Email = request.Email.Trim(),
|
||||
MobileNumber = request.MobileNumber,
|
||||
Password = request.Password,
|
||||
ChkUser = true
|
||||
}, ct);
|
||||
|
||||
// Mirror into the local shadow User row immediately, rather than waiting
|
||||
// for ShadowUserClaimsTransformation's next-login JIT provisioning.
|
||||
var user = new User
|
||||
{
|
||||
AuthUserId = authUserId,
|
||||
Username = username,
|
||||
DisplayName = request.FullName.Trim(),
|
||||
RoleId = role.RoleId,
|
||||
Status = EntityStatus.Active
|
||||
};
|
||||
|
||||
await _users.AddAsync(user, ct);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
user.Role = role;
|
||||
return Map(user);
|
||||
}
|
||||
|
||||
public async Task<ManagedUserDto> UpdateRoleAsync(int userId, UpdateUserRoleRequest request, CancellationToken ct = default)
|
||||
{
|
||||
var user = await _users.GetByIdAsync(userId, ct)
|
||||
?? throw new NotFoundException($"User {userId} was not found.");
|
||||
var role = await _roles.GetByIdAsync(request.RoleId, ct)
|
||||
?? throw new NotFoundException($"Role {request.RoleId} was not found.");
|
||||
|
||||
user.RoleId = role.RoleId;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
user.Role = role;
|
||||
return Map(user);
|
||||
}
|
||||
|
||||
public async Task<List<UserTypeOptionDto>> ListUserTypesAsync(CancellationToken ct = default)
|
||||
{
|
||||
var userTypes = await _authHex.ListUserTypesAsync(ct);
|
||||
return userTypes.Select(t => new UserTypeOptionDto(t.UserTypeId, t.Code, t.Description)).ToList();
|
||||
}
|
||||
|
||||
private static ManagedUserDto Map(User u) => new(
|
||||
u.UserId, u.Username, u.DisplayName, u.Status, u.RoleId, u.Role?.Code, u.Role?.Name);
|
||||
}
|
||||
@@ -25,6 +25,7 @@ public static class ErrorCodes
|
||||
public const string ConcurrencyConflict = "CONCURRENCY_CONFLICT";
|
||||
public const string IdempotencyReplay = "IDEMPOTENCY_REPLAY";
|
||||
public const string ConfigDisabled = "CONFIG_DISABLED";
|
||||
public const string RoleInUse = "ROLE_IN_USE";
|
||||
|
||||
// Auth proxy (AuthController → AuthHex, docs/11 §2.0)
|
||||
public const string AuthUpstreamError = "AUTH_UPSTREAM_ERROR";
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=CHANGE_ME;Password=CHANGE_ME"
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=root;Password=root"
|
||||
},
|
||||
"Auth": {
|
||||
"Issuer": "AuthHex",
|
||||
@@ -16,7 +16,7 @@
|
||||
"RequiredRoleCode": ""
|
||||
},
|
||||
"AuthHex": {
|
||||
"BaseUrl": "CHANGE_ME"
|
||||
"BaseUrl": "http://localhost:5011"
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user