ath fix
This commit is contained in:
@@ -59,6 +59,7 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
|
||||
- [x] Audit log on every mutation (who/when/old→new) — `AuditLog` (jsonb `changeSet`), written by an `ErpDbContext.SaveChanges` override (`AuditScribe`): Create captures the field set, Update captures **only changed fields as {old,new}**, Delete captures the prior row; PK/RowVersion excluded; ledger/layer/seq/self/journal excluded. Actor from `ICurrentUser` (system=1 until auth). Read via `GET /audit-logs`. **Verified** (Item create+update old→new; StockAdjustment create). This is the **AR-01 compensating control** (02-SECURITY B.3) — app-level append-only; DB-role UPDATE/DELETE revoke still deferred.
|
||||
- [x] Document numbering sequences (per type, per year) — `NumberSequence` + `NumberSequenceService` (atomic `INSERT … ON CONFLICT … RETURNING` inside the doc's UoW txn; gap-controlled). Verified issuing + incrementing PR/RFQ/PO.
|
||||
- [x] Auth: **external AuthHex IdP integration** (2026-07-14) — ERPCore is a resource server. `JwtAuthExtensions` validates **RS256** against AuthHex's RSA **public** key (config `Auth:RsaPublicKeyXml` → `RsaSecurityKey`; `MapInboundClaims=false`), issuer `AuthHex`, audience `AuthHexClient` (no JWKS → static key). `[Authorize(ErpAccess)]` on `ApiControllerBase` gates every v1 endpoint; the `ErpAccess` policy `RequireAuthenticatedUser` + optional `RequireClaim(UserTypeCode/RoleCode)` from `Auth:RequiredUserTypeCode`/`RequiredRoleCode` (empty ⇒ any valid ERP token — AuthHex is ERP-dedicated). **Shadow-user JIT provisioning:** `ShadowUserClaimsTransformation` (`IClaimsTransformation`) maps the token's `UserId` **GUID** → a local `users` row (`auth_user_id` unique; Username/DisplayName = `NIC`), idempotent, and injects the local `int` id as `nameid` so `ICurrentUser.AuditUserId` resolves the real actor. Migration `AddAuthUserId`. **Verified:** no token→401; `/health`,`/api/meta`,Swagger anonymous; valid token→200; shadow user provisioned (User 2, Username=NIC, AuthUserId=GUID); item Create **audited as the shadow user (id 2, not system)**; re-request reuses the same user; door gate → **403** on UserType mismatch, **200** on match.
|
||||
- [x] Auth proxy: **`AuthController` fronting AuthHex** (2026-07-16) — the frontend no longer calls AuthHex directly; `Controllers/AuthController.cs` + `Services/Auth/{AuthUserService,AuthRecoveryService,AuthAltService}` proxy all 24 AuthHex functions (register/login/OTP-login/refresh/sessions/status/lock/change-password/verify-password/logout/update/2FA×5/recovery×4/alt×3) via `Infra/Auth/AuthHex/{IAuthHexClient,AuthHexClient}` (`AuthHex:BaseUrl` config). Sessions delivered as httpOnly Secure `erp_at`/`erp_rt` cookies + `XSRF-TOKEN` double-submit cookie (`Infra/Auth/AuthCookieWriter.cs`, 02-SECURITY §B.2); `ValidateCsrfAttribute` guards every mutating action; the JWT bearer handler now also accepts `erp_at` as a fallback (`JwtAuthExtensions`'s `OnMessageReceived`) so every other v1 controller keeps working unchanged. See `docs/11-BACKEND-PHASE1.md §2.0` for the full route table and `docs/02-SECURITY.md` AR-07/AR-08 for the two carried-over exposures (anonymous `getUserDetails`/`LogoutUser`, no rate limiting yet). **Not done this pass:** CORS (needed once a browser frontend calls these endpoints cross-origin), rate limiting, and the frontend wiring itself (`lib/api/auth.ts` + login/OTP/reset pages) — all deliberately deferred follow-ups.
|
||||
- [x] JournalEntryStub emitted per stock movement (data only) — `JournalEntryStub` written in `FifoCostingService.PostLedgerAsync` for every ledger entry (In → Dr Inventory `1300` / Cr Clearing `2100`; Out reverses; amount = movement value). Placeholder accounts until a chart of accounts exists. Read via `GET /journal-entries`. **Verified** (GRN In 700, ADJ Out 70).
|
||||
- [x] Negative-stock policy enforcement (default block) — enforced in `FifoCostingService.ConsumeAsync` → `409 STOCK_NEGATIVE_BLOCKED` (verified). Per-item override still a config stub.
|
||||
- [~] FEFO picking for perishables; block expired / on-hold issue — **issue-block done + verified** (`ONHOLD_NOT_ISSUABLE`, `EXPIRED_BATCH_BLOCKED`; on-hold/expired layers excluded from consume). FEFO *pick ordering* (oldest-expiry first) not yet built.
|
||||
@@ -133,3 +134,16 @@ Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit**
|
||||
- **Shadow-user provisioning:** `ShadowUserClaimsTransformation` (`IClaimsTransformation`, scoped) maps token `UserId` GUID → local `users` row (`auth_user_id` unique, Username/DisplayName=`NIC`), idempotent w/ race-safe re-read, injects local `int` id as `nameid`. `User.AuthUserId` (Guid?) + `AuthHexClaims` consts + migration `AddAuthUserId`.
|
||||
- **Verified (minted AuthHex-shaped RS256 token, signed with AuthHex's real private key):** no token→401; `/health`,`/api/meta`,Swagger→200 anon; valid token→200; POST item→201 **audited as shadow user id 2** (Username=NIC, AuthUserId=GUID), not system; repeat request reuses user (1 provision); door gate `RequiredUserTypeCode=WAREHOUSE` → ERP-type token **403**, WAREHOUSE-type token **200**. Build clean; migration applied.
|
||||
- **§6 COMPLETE.** Only intentional deferral left in Phase 1: FEFO pick-ordering (§6, `[~]`). Follow-ups: set the real ERP `Auth:RequiredUserTypeCode`/`RoleCode` for production; secure the RSA key rotation process.
|
||||
|
||||
### 2026-07-16 — Auth proxy: `AuthController` fronting AuthHex (frontend no longer calls AuthHex directly)
|
||||
- **Architecture reversal:** the 2026-07-14 "resource-server-only, no login proxy" decision (docs/10, docs/11 §2.0) is reversed — the frontend was found to have **zero** existing AuthHex integration (login/OTP/reset screens were UI-only mocks with no network calls), so this was greenfield backend work, not a migration. `docs/10-BACKEND-PHASE1.md` (header, A.4, NFR-03) and `docs/11-BACKEND-PHASE1.md §2.0` updated in place; `docs/02-SECURITY.md` gained AR-07/AR-08 and ticked 3 of 5 B.2 boxes.
|
||||
- **`Infra/Auth/AuthHex/`** — `IAuthHexClient`/`AuthHexClient` (typed `HttpClient`, `AuthHex:BaseUrl` config = `http://localhost:5011` dev), one C# method per AuthHex `functionName`, hides the `{functionName,payload,reference}`/`{statusCode,success,message,data}` dispatcher envelope entirely; upstream failures → `DomainException` (`AUTH_UPSTREAM_ERROR`/`AUTH_SERVICE_UNAVAILABLE`).
|
||||
- **`Dtos/Auth/*`** — REST-shaped request/response DTOs per function (not a functionName-dispatcher passthrough), matching ERPCore's existing DTO-at-boundary convention. Session-issuing responses (`AuthSessionResponse`, `OtpLoginVerifiedResponse`) deliberately omit tokens.
|
||||
- **`Services/Auth/{AuthUserService,AuthRecoveryService,AuthAltService}`** — orchestrate `IAuthHexClient` calls; `AuthSessionResult`/`OtpAuthSessionResult` (`Services/Auth/AuthSessionResult.cs`) carry tokens from service → controller only, never serialized.
|
||||
- **`Controllers/AuthController.cs`** — `api/v1/auth/*`, 24 actions (see `docs/11 §2.0` table); inherits `ControllerBase` directly (not `ApiControllerBase`) since most actions need `[AllowAnonymous]` and its ETag/If-Match handling doesn't apply here.
|
||||
- **Cookie/CSRF (`Infra/Auth/AuthCookieWriter.cs`, `ValidateCsrfAttribute.cs`, `JwtAuthExtensions.cs`):** `erp_at` (Path `/`), `erp_rt` (Path `/api/v1/auth/refresh-token`, scoped so it's only sent to the refresh call), `XSRF-TOKEN` (non-httpOnly) — all `HttpOnly`(except CSRF)/`Secure`/`SameSite=Strict`. `ValidateCsrfAttribute` double-submit-checks `X-XSRF-TOKEN` against the cookie on every mutating action, exempting Bearer-header callers. `JwtAuthExtensions`'s `OnMessageReceived` falls back to the `erp_at` cookie when no `Authorization` header is present — every existing v1 controller keeps working unchanged under either auth mode.
|
||||
- **Verified:** `dotnet build` clean (0 warn/0 err) after two passes — first pass hit `CS0051` (a public interface/constructor can't expose an `internal` parameter type) on `IAuthHexClient` and its supporting `AuthHex*` wire types, fixed by making them `public`; second pass caught a cookie-path bug (`erp_rt`'s `Path` was written as `/api/auth/refresh-token`, not matching the actual `/api/v1/auth/refresh-token` route — the browser would never have sent the cookie back on refresh) before it shipped.
|
||||
- **Not done this pass (tracked as follow-ups, not silently skipped):** CORS (needed once a browser frontend calls cross-origin — `docs/02-SECURITY.md §B.2` left unticked), rate limiting on the anonymous endpoints (`docs/02-SECURITY.md` AR-08), and the frontend wiring itself (`lib/api/auth.ts` + wiring `app/login/**`'s mock pages to these endpoints) — deliberately out of scope per user decision.
|
||||
- **Live-verified against the running AuthHex instance (`:5011`) and ERPCore (`:5224`, dev):** `register` → `200` with `Set-Cookie: erp_at`(httpOnly/Secure/Strict/maxAge=3600) + `erp_rt`(httpOnly/Secure/Strict/Path=`/api/v1/auth/refresh-token`/30d) + `XSRF-TOKEN`(Secure/Strict, JS-readable), body carries `user`+`expiresIn` only, **no tokens**; the `erp_at` cookie alone (zero `Authorization` header) authenticated `GET /api/v1/items` — confirms the `OnMessageReceived` cookie fallback works for every existing v1 controller unchanged; `GET /api/v1/auth/sessions` (protected, cookie-authenticated) → `200`; mutating `POST /api/v1/auth/change-password` without `X-XSRF-TOKEN` → `403 CSRF_TOKEN_MISMATCH`, with the matching header → `204` + all three cookies cleared, exactly as designed.
|
||||
- **Found + fixed a real bug during live testing:** `AuthHexClient` trusted the envelope's `success` flag alone; AuthHex was observed returning **`HTTP 500` with `"success": true, "data": null`** on a business failure (invalid-credentials login), which slipped past the `!envelope.Success` check and null-derefed inside `AuthUserService.ToSessionResult` (`NullReferenceException` → bare unhandled `500`, no `code`). Fixed `AuthHexClient.CallAsync` to also fail on `!httpResponse.IsSuccessStatusCode` regardless of `envelope.Success`, plus added `result is null` guards in `ToSessionResult`/`ToOtpSessionResult`/`AuthAltService.VerifyOtpAsync` as defense-in-depth. Re-verified: the same invalid-credentials case now returns a clean `500 AUTH_UPSTREAM_ERROR` ProblemDetails instead of crashing.
|
||||
- **Login/refresh/logout left unverified live** — `loginUser` currently fails with `{"statusCode":500,"success":true,"message":"Invalid credentials","data":null}` on AuthHex **even immediately after a fresh registration with the exact same credentials**, confirmed by calling AuthHex's `/api/user` directly with the identical payload (bypassing ERPCore entirely) — this is a bug in AuthHex's own `loginUser`/password-verification path, not in this proxy. Blocked on an AuthHex-side fix; re-run the register→login→cookie→refresh→logout pass once that's resolved.
|
||||
|
||||
Reference in New Issue
Block a user