feat: add user and role view sheets with detailed information display

- Created UserViewSheet component to display user details including username, full name, role, status, and timestamps.
- Created RoleViewSheet component to display role details including name, level, status, creation date, and associated permissions.
- Introduced permissions configuration in a new permissions.ts file, defining categories and permission codes for better management.
- Implemented middleware for authentication, ensuring access control for protected routes.
This commit is contained in:
Kalana B. Thilakarathna
2026-10-18 23:11:01 +05:30
parent 558afc6f9d
commit 0052725b88
86 changed files with 41117 additions and 939 deletions
+108 -94
View File
@@ -1,102 +1,116 @@
# Authentication — Own-User Auth via External Identity Service (AuthHex)
# Authentication & Authorization Layer
**Status**: Done
**Depends On**: — (cross-cutting infrastructure, not a numbered feature step)
## External Auth Service (AuthHex)
- Runs on `http://localhost:6703`
- Owns credentials — this app never stores passwords
- Issues RS256-signed JWTs with claims: `sub`, `role_id`, `role_level`, `username`, `role_code`
---
## Goal
Own-user (staff/API) authentication is delegated to an external identity service ("AuthHex"),
which owns credentials and signs RS256 JWTs. This app validates those tokens and maps them onto
its own `User`/`Role` tables so the existing permission-policy authorization stays unchanged.
---
## Background / Context
Previously this app verified passwords and minted its own HMAC-signed JWT locally. That's been
replaced — AuthHex now owns credentials and signing; this app only holds AuthHex's **public** key
to validate tokens, and never sees a plaintext password for this flow.
Authorization is untouched: permission policies, role levels, scope checks, and the seeded role
set all still work exactly as before, because every validated token is mapped back onto a local
`User`/`Role` row before any authorization check runs.
---
## Configuration Changes
```jsonc
"Jwt": {
"PublicKey": "<RSAKeyValue>...</RSAKeyValue>",
"Audience": "AuthHexClient",
"Issuer": "AuthHex"
},
"AuthHex": { "BaseUrl": "http://localhost:6102" }
```
`Jwt:Key` (old symmetric secret) is removed. `Jwt:PublicKey` is AuthHex's RSA public key in XML
format (not PEM).
---
## API Endpoints
| Method | Route | Notes |
|---|---|---|
| POST | /api/auth/login | Proxies to AuthHex `loginUser` |
| POST | /api/auth/register | Proxies to AuthHex `registerUser` |
| GET | /api/auth/me | `[Authorize]`, proxies to AuthHex `getUserDetails` |
| POST | /api/auth/logout | `[Authorize]`, proxies to AuthHex `LogoutUser` |
All four just forward to `POST {AuthHex:BaseUrl}/api/user` and pass AuthHex's response back
as-is. No cookie — token returned in the response body, sent back as `Authorization: Bearer`.
---
## Business Rules
1. No local password check or token issuance — both delegated to AuthHex.
2. `AddJwtBearer` validates issuer/audience/signature/lifetime against AuthHex's public key.
3. `OnTokenValidated` maps the token's `sub` claim to a local `User` (`IUserRepository`); fails
the token if no matching active user exists.
4. On success, `username`/`role_id`/`role_level` claims are attached from the **local** `User`/
`Role` row — authorization checks downstream are unaffected by anything AuthHex's own token
claims contain.
5. Only one validation mechanism runs (`AddJwtBearer`) — two ported alternatives exist but are not
registered (see Implementation Notes).
6. No refresh tokens — once the AuthHex-issued token expires, the client must log in again.
---
## Permissions to Add to Seed
None — this only changes authentication. Permission policies and role seed data are untouched.
---
## Open Questions
- Add a refresh-token flow if AuthHex doesn't offer one and re-login UX proves disruptive.
- Keep or delete the ported `AuthMiddleware`/`JwtMiddleware` if never actually needed.
- No retry/circuit-breaker if AuthHex is unreachable.
---
## Implementation Notes
## Directory Structure
```
Features/Auth/ — AuthController, AuthService, AuthDTOs (thin AuthHex proxies)
Infra/Authorization/
AuthHex/ — IAuthHexClient, AuthHexClient, AuthHexResponse
Jwt/RsaXmlKeyReader.cs — portable XML→RSA key parser (RSA.FromXmlString is Windows-only)
Middleware/ — AuthMiddleware, JwtMiddleware, JwtOptions, ApiResponse
(ported from OSMCore for reference — NOT registered)
Program.cs — AddHttpClient<IAuthHexClient>, AddJwtBearer + OnTokenValidated
(maps token → local User/Role → role_id/role_level claims)
Infra/
├── Auth/ External auth service integration
│ ├── AuthHexClient.cs HTTP proxy to AuthHex (/api/user)
│ ├── IAuthHexClient.cs
│ └── AuthHexResponse.cs Raw response shape from AuthHex
│
└── Authorization/ This app's permission enforcement
├── PermissionRequirement.cs ASP.NET Core requirement (wraps permission code string)
├── PermissionPolicyProvider.cs Builds policy dynamically for any "x.y" string
├── PermissionAuthorizationHandler.cs Evaluates requirement against DB
├── AuthorizationController.cs Exposes permission/scope check endpoints
├── CurrentUserService.cs Extracts typed claims from HttpContext.User
├── ICurrentUserService.cs
├── AuthorizationDTOs.cs
├── Permission/
│ ├── PermissionRepository.cs Queries RolePermissions + Permissions tables
│ ├── PermissionService.cs
│ ├── IPermissionRepository.cs
│ └── IPermissionService.cs
└── Scope/
├── ScopeRepository.cs Queries UserStoreScopes / UserRouteScopes / UserRegionScopes
├── ScopeAccessService.cs Resolves accessible stores via store → route → region hierarchy
├── IScopeRepository.cs
└── IScopeAccessService.cs
```
**Gotcha:** don't run more than one of `AddJwtBearer` / `AuthMiddleware` /
`JwtMiddleware.CustomMiddlewareAttribute` at once — each re-validates the same token
independently.
---
## Login Flow
```
POST /api/auth/login
→ AuthController
→ AuthService.LoginAsync
→ AuthHexClient.CallAsync("loginUser", ...)
→ POST http://localhost:6703/api/user
← RS256-signed JWT returned to client
```
---
## Request Flow
```
Incoming request with Authorization: Bearer <jwt>
│
▼
AuthMiddleware (Middleware/AuthMiddleware.cs)
- Reads Jwt:PublicKey from appsettings.json
- RSA.Create().FromXmlString(publicKeyXml)
- Validates JWT signature (RS256), issuer, audience, lifetime
- Sets HttpContext.User = principal (claims: sub, role_id, role_level, username)
│
▼
app.UseAuthorization()
- Global AuthorizeFilter: requires IsAuthenticated on every endpoint
- [Authorize("brand.view")] triggers PermissionPolicyProvider
→ builds AuthorizationPolicy with PermissionRequirement("brand.view")
→ PermissionAuthorizationHandler reads role_id claim
→ PermissionService.HasPermissionAsync(roleId, "brand.view")
→ DB: RolePermissions JOIN Permissions WHERE code = "brand.view"
│
▼
Controller
- ICurrentUserService.UserId / RoleId / RoleLevel extracted from HttpContext.User
- Scope guards: HasStoreScopeAsync / HasRouteScopeAsync / HasRegionScopeAsync
```
---
## Scope System
Scope controls which **records** a user can access (not just which actions).
| Assignment table | Access granted to |
|---|---|
| `UserStoreScopes` | Specific stores directly |
| `UserRouteScopes` | All stores on that route |
| `UserRegionScopes` | All routes → all stores in that region |
`ScopeAccessService.GetAccessibleStoreIdsAsync` merges all three levels and deduplicates.
---
## Pipeline Order (Program.cs)
```csharp
app.UseCors();
app.UseMiddleware<AuthMiddleware>(); // 1. populate HttpContext.User
app.UseAuthorization(); // 2. evaluate [Authorize] attributes
```
`UseAuthentication()` is intentionally absent — JWT validation is done manually in `AuthMiddleware`, not via ASP.NET Core's built-in bearer scheme.
---
## appsettings.json Keys
| Key | Purpose |
|---|---|
| `Jwt:PublicKey` | RSA XML public key from AuthHex (used by AuthMiddleware) |
| `Jwt:Issuer` | `AuthHex` |
| `Jwt:Audience` | `AuthHexClient` |
| `AuthHex:BaseUrl` | `http://localhost:6703` |
@@ -1,6 +1,6 @@
# Frontend API Reference — ERP Distribution (Post-Refactor)
> Last updated: 2026-09-17
> Last updated: 2026-10-13
> Base URL: `https://<host>/api`
> All responses are JSON. Enum fields are serialized as **strings** (e.g. `"Pending"`, `"ModeB"`).
> All `Guid` fields are UUID v4 strings. All `DateTime` fields are ISO 8601.
@@ -9,7 +9,8 @@
## Table of Contents
1. [Pagination](#pagination)
1. [Authentication](#authentication)
2. [Pagination](#pagination)
2. [Enums Reference](#enums-reference)
3. [ShopPO](#shoppo)
4. [Invoice](#invoice)
@@ -24,6 +25,146 @@
---
## Authentication
### Login
```http
POST /api/auth/login
Content-Type: application/json
{
"identifier": "string",
"password": "string",
"userTypeId": "uuid | null",
"deviceName": "string"
}
```
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `identifier` | `string` | Yes | Username or email |
| `password` | `string` | Yes | |
| `userTypeId` | `Guid?` | No | Scopes login to a specific user type |
| `deviceName` | `string` | No | Defaults to `"Unknown Device"` |
**Response:** AuthHex response. On success, the server also sets an **HttpOnly** cookie `access-token` containing the JWT (expiry driven by the `expiresIn` field, defaulting to 1 hour).
```json
{
"statusCode": 200,
"success": true,
"message": null,
"data": {
"accessToken": "<jwt>",
"expiresIn": 3600
}
}
```
### Using the Token
The server sets the JWT as an `HttpOnly` cookie (`access-token`) on login. For browser clients the cookie is sent automatically on every same-origin request — no manual token handling is needed.
For non-browser clients (or cross-origin requests with `withCredentials: true`), include the JWT explicitly:
```http
Authorization: Bearer <jwt>
```
The middleware accepts either the `Authorization: Bearer` header **or** the `access-token` cookie, preferring the header.
### Register
```http
POST /api/auth/register
Content-Type: application/json
{
"username": "string",
"password": "string"
}
```
### Get Current User
```http
GET /api/auth/me
Authorization: Bearer <jwt>
```
**Response:**
```json
{
"userId": "uuid",
"username": "string",
"roleId": "uuid",
"roleCode": "string",
"roleLevel": 0
}
```
### Logout
```http
POST /api/auth/logout
```
Deletes the `access-token` cookie server-side. No request body required.
**Response:**
```json
{ "success": true }
```
> **Note:** Logout only clears the cookie on this server. The JWT itself is not invalidated at AuthHex — it remains valid until it expires.
### Error Responses
| Status | Meaning |
|--------|---------|
| `401` | No token supplied, token invalid, or token expired |
| `403` | Token is valid but the user's role does not have the required permission |
### Permission Check (optional)
The frontend can check whether the current user holds a specific permission before rendering UI elements:
```http
GET /api/authorization/permissions/{permissionCode}
Authorization: Bearer <jwt>
```
**Response:**
```json
{ "hasPermission": true }
```
Common permission codes: `brand.view`, `stock.manage`, `ride.view`, `loading-sheet.manage`, etc.
### Scope Check (optional)
Check whether the user has data-access scope over a specific resource:
```http
GET /api/authorization/scopes/stores/{storeId}
GET /api/authorization/scopes/routes/{routeId}
GET /api/authorization/scopes/regions/{regionId}
GET /api/authorization/scopes/stores
```
Single-resource checks (`/stores/{id}`, `/routes/{id}`, `/regions/{id}`) return:
```json
{ "hasScope": true }
```
`GET /api/authorization/scopes/stores` returns all store IDs the current user can access:
```json
{ "storeIds": ["uuid", "uuid"] }
```
---
## Pagination
All list endpoints return a `PagedResponse<T>`: