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:
@@ -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>`:
|
||||
|
||||
Reference in New Issue
Block a user