docs: finalize BACKEND.md as a self-contained implementation prompt
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Goal: this document alone, pasted into a fresh Claude session, should be enough for a backend dev to implement against without getting stuck or inventing conventions the frontend doesn't actually need. Fixed a real bug: ~65 cross-references throughout the document pointed to docs/AUTHENTICATION.md, docs/ERROR_CONTRACT.md, docs/MAINTENANCE_MODE.md - three sibling docs that were deleted and fully merged into this document's own §4/§6/§10 during the earlier doc-consolidation pass, but the in-text references were never updated. A fresh agent following those links would hit dead ends repeatedly. Bulk-replaced with in- document section references; hand-fixed ~4 sentences that framed §6/ §10 as "sibling task, in progress" (stale - both are complete, this document's §6/§10 already are the settled contract, nothing to wait on). Added "Recommended default" to every item across the document's 5 consolidated "Requires backend decision" registers (§2.12 framework, §3.21 CRUD cross-cutting - 18 items, §4 §12 auth open items - 9 items, §6 error-model summary - 9 items, §10 maintenance-mode list - 5 items). Each default is derived from what the frontend already implies or standard REST/security convention - no invented APIs or business rules. ~6 items are explicitly flagged as real business/security decisions instead (order state-machine rules, Ed25519 cutover strategy, refresh-token reuse-detection posture) since those carry consequences no amount of frontend-code-reading can resolve. Added a "How to use this document" preamble up front: work in §9's dependency order not document order, apply recommended defaults and keep moving, only stop for the explicitly-flagged business/security items, don't invent beyond what's written or directly implied. Frontend side: confirmed nothing else is left. docs/TODO.md already has zero blockers; docs/KNOWN-ISSUES.md's one open item (unreachable Ed25519 error-code UI) is correctly left open and documented rather than faked closed - fixing it needs a real backend emitting real distinguishable error codes, which doesn't exist yet and can't be fabricated without inventing an API contract. No frontend code touched. No architecture changed. No APIs invented.
This commit is contained in:
325
docs/BACKEND.md
325
docs/BACKEND.md
@@ -1,11 +1,52 @@
|
||||
# Backend — Canonical Specification
|
||||
|
||||
**This is the ONE document a backend engineer needs.** It is the single source of truth for backend implementation — architecture, bootstrap, authentication, JWT, Ed25519 public-key login, permissions, maintenance mode, error contract, every endpoint, DTOs, request/response schemas, uploads, pagination, filters, sorting, publish workflow, media, builder, examples, and a top-to-bottom implementation checklist. It supersedes and fully merges `AUTHENTICATION.md`, `ERROR_CONTRACT.md`, `MAINTENANCE_MODE.md`, and (already archived, content re-derived from current source) `docs/archive/BACKEND_API.md` and `docs/archive/BACKEND_API_REMAINING_WORK.md`. Those standalone files no longer exist — everything they contained lives here.
|
||||
**This is the ONE document a backend engineer needs.** It is the single source of truth for backend implementation — architecture, bootstrap, authentication, JWT, Ed25519 public-key login, permissions, maintenance mode, error contract, every endpoint, DTOs, request/response schemas, uploads, pagination, filters, sorting, publish workflow, media, builder, examples, and a top-to-bottom implementation checklist. Authentication (§4), the Error Model (§6), and Maintenance Mode (§10) are sections of this same document, not separate files — earlier drafts existed as standalone `AUTHENTICATION.md`/`ERROR_CONTRACT.md`/`MAINTENANCE_MODE.md` files and were fully merged in; those files no longer exist. This document also supersedes (already archived, content re-derived from current source) `docs/archive/BACKEND_API.md` and `docs/archive/BACKEND_API_REMAINING_WORK.md`.
|
||||
|
||||
Everything here is derived from the actual current frontend source code (branch `B2B`), not from prior/stale documentation. Primary input: `docs/context/BACKEND-AUDIT.md` (exhaustive audit of every HTTP call, gateway, facade, and model in the frontend).
|
||||
|
||||
**Convention used throughout:** where the frontend already implies a concrete behavior, it's documented as-is. Where the frontend has no opinion and a real backend needs one, it's marked **"Requires backend decision"** — nothing beyond what the frontend requires is invented.
|
||||
|
||||
## How to use this document (read this first if you're implementing from it)
|
||||
|
||||
You are building a real backend for an existing, feature-complete Angular
|
||||
frontend (branch `B2B`) that currently runs entirely on mock/local data. This
|
||||
document is the complete contract — every endpoint, DTO, status code, and
|
||||
convention the frontend already expects. Treat it as your spec, not as
|
||||
background reading.
|
||||
|
||||
1. **Work in dependency order**, not top-to-bottom document order: §9 (Backend
|
||||
Checklist) gives the actual build sequence in 6 phases (foundation → auth/
|
||||
tenant/bootstrap → read-heavy catalog → write-heavy customer domains →
|
||||
admin domains → builder/CMS → hardening). Start there.
|
||||
2. **Every "Requires backend decision" marker has a Recommended default**
|
||||
attached to it (in §2.12, §3.21, §4 §12, §6's summary, and §10's list —
|
||||
the five per-section consolidated registers). Apply the recommended
|
||||
default and keep moving. Do not stop and wait for human input on these —
|
||||
they exist so you don't have to invent a convention from nothing, and
|
||||
they were chosen to match what the frontend already assumes.
|
||||
3. **A small number of items are explicitly marked as business/security
|
||||
decisions, not technical ones** (e.g. order status-transition rules, the
|
||||
Ed25519 cutover strategy, refresh-token reuse detection posture). These
|
||||
are the only points where you should actually stop and ask a human,
|
||||
because they carry money, legal, or security consequences the frontend
|
||||
code can't tell you the answer to.
|
||||
4. **Do not invent new endpoints, fields, or business rules beyond what's
|
||||
written here or directly implied by an existing frontend call.** If you
|
||||
find yourself needing something this document doesn't cover and no
|
||||
recommended default applies, that's a real gap — flag it explicitly
|
||||
rather than guessing silently.
|
||||
5. **§8 (Real Backend Implementation Guide)** has the frontend-side
|
||||
mechanics: which DI-token gateways to swap, which facades change, worked
|
||||
DTO-mapping examples. Read it before touching any single domain's
|
||||
endpoints in §3 — it explains the pattern every domain repeats.
|
||||
6. **Verify against the live frontend where you can.** Every literal
|
||||
(non-PROPOSED) path, DTO, and example in this document was checked
|
||||
against actual source on `B2B` — but the frontend evolves. If your
|
||||
implementation and this document ever disagree with what the running
|
||||
frontend actually sends/expects, the frontend's actual behavior wins;
|
||||
flag the doc as stale rather than building against a description that
|
||||
no longer matches reality.
|
||||
|
||||
## Table of contents
|
||||
|
||||
1. [Bootstrap](#1-bootstrap)
|
||||
@@ -172,7 +213,7 @@ Each nested field, with its source model file under
|
||||
- **`permissions`** (`PermissionsConfig`, `permissions.model.ts`) —
|
||||
`{ definitions: [{ key, description? }], roles: [{ role, permissions[] }] }`.
|
||||
Bootstrap-level RBAC catalog. (Distinct from the Ed25519 JWT `AdminRole`
|
||||
union — see `docs/AUTHENTICATION.md` §9 for the flagged `AdminRole` naming
|
||||
union — see `§4 (Authentication, this document)` §9 for the flagged `AdminRole` naming
|
||||
collision.)
|
||||
- **`header`** (`HeaderConfig`, optional) — header layout config.
|
||||
- **`catalog`** (`CatalogConfig`, `catalog-config.model.ts`, optional) — the
|
||||
@@ -392,7 +433,7 @@ trimmed for length; full versions in that file):
|
||||
|
||||
- There is **no `APP_INITIALIZER`** wiring bootstrap fetch as a hard app
|
||||
precondition. `main.ts` calls `bootstrapApplication(App, appConfig)` with no
|
||||
initializer that blocks on config. (`docs/AUTHENTICATION.md` §5.2 separately
|
||||
initializer that blocks on config. (`§4 (Authentication, this document)` §5.2 separately
|
||||
notes the Ed25519 `restoreSession()` initializer is also not wired.)
|
||||
- Bootstrap is instead loaded lazily-but-eagerly by the first consumer to need
|
||||
it. Multiple root-level services subscribe to `ConfigService.loadBootstrap()`
|
||||
@@ -441,7 +482,7 @@ strategy and the frontend handling for it would be new work.
|
||||
### 1.9 Tenant resolution
|
||||
|
||||
Tenant is resolved by **subdomain**, not by header or path (full detail in
|
||||
`docs/AUTHENTICATION.md` §10; source
|
||||
`§4 (Authentication, this document)` §10; source
|
||||
`src/app/core/config/tenant-resolver.service.ts`):
|
||||
|
||||
- `TenantResolverService.getTenantKey()`: localhost →
|
||||
@@ -498,7 +539,7 @@ there is no separate "draft bootstrap" vs "live bootstrap" endpoint today
|
||||
error.
|
||||
- Feature-specific consumers that require bootstrap data render their own
|
||||
generic empty/error states (the same `common.errorTitle` pattern described in
|
||||
`docs/ERROR_CONTRACT.md`), not a bootstrap-specific one.
|
||||
`§6 (Error Model, this document)`), not a bootstrap-specific one.
|
||||
|
||||
> **Requires backend decision / frontend follow-up:** whether a hard bootstrap
|
||||
> failure should present a dedicated tenant-level error screen. Today the
|
||||
@@ -513,13 +554,13 @@ The frontend does not parse a bootstrap-specific error body — a failed
|
||||
the standard error envelope every endpoint (including this one) should return
|
||||
on non-2xx, and the per-status semantics (401/403/404/409/422/429/500/503,
|
||||
plus `TENANT_DISABLED` and `MAINTENANCE_MODE`), see
|
||||
**`docs/ERROR_CONTRACT.md`** — that document owns the error envelope and this
|
||||
**§6 (Error Model)** — that section owns the error envelope and this
|
||||
section does not redefine it. Two of its rows are especially relevant to
|
||||
bootstrap:
|
||||
|
||||
- **`TENANT_DISABLED`** (HTTP 403, `error.code: "TENANT_DISABLED"`) — the
|
||||
natural failure for `GET /bootstrap` against a known-but-inactive tenant.
|
||||
`ERROR_CONTRACT.md` marks the frontend handling for this as "Requires backend
|
||||
`§6` marks the frontend handling for this as "Requires backend
|
||||
decision" (no code path handles it today).
|
||||
- **`MAINTENANCE_MODE`** (HTTP 503, `error.code: "MAINTENANCE_MODE"`) — if a
|
||||
tenant is down for maintenance, bootstrap is where the frontend would first
|
||||
@@ -681,7 +722,7 @@ defaults defensively:
|
||||
|
||||
### 2.7 JWT / authentication header format
|
||||
|
||||
Two coexisting mechanisms; full spec in **`docs/AUTHENTICATION.md`** (this
|
||||
Two coexisting mechanisms; full spec in **`§4 (Authentication, this document)`** (this
|
||||
section only references it, does not redefine):
|
||||
|
||||
- **Telegram session auth (LIVE)** — no `Authorization: Bearer`. Identity is a
|
||||
@@ -693,12 +734,12 @@ section only references it, does not redefine):
|
||||
- **Ed25519 admin auth (wired, backend absent)** — issues a JWT
|
||||
`AuthTokenPair { token, refreshToken }`; the intended header is standard
|
||||
`Authorization: Bearer <token>`, but its `authInterceptor` is **not
|
||||
registered** today (AUTHENTICATION.md §2.6), so no request auto-attaches the
|
||||
registered** today (§4 §2.6), so no request auto-attaches the
|
||||
bearer token yet. `adminAuthHeadersInterceptor` *does* set `Authorization:
|
||||
Bearer <token>` if an admin token happens to be stored, but nothing stores one
|
||||
in the live flow.
|
||||
|
||||
Refer to `docs/AUTHENTICATION.md` for token structure (`JwtClaims`), refresh,
|
||||
Refer to `§4 (Authentication, this document)` for token structure (`JwtClaims`), refresh,
|
||||
rotation, expiry, role hierarchy, and tenant-scoping open items.
|
||||
|
||||
### 2.8 Live endpoint domains (real HTTP, already implemented)
|
||||
@@ -706,13 +747,13 @@ rotation, expiry, role hierarchy, and tenant-scoping open items.
|
||||
The following domains have **real `HttpClient` calls in code today** (not
|
||||
proposals). Documented per-endpoint below. (The Ed25519 admin-auth API is wired
|
||||
to real HTTP but the backend does not implement it yet — its full contract is
|
||||
in `docs/AUTHENTICATION.md` §2, not repeated here.)
|
||||
in `§4 (Authentication, this document)` §2, not repeated here.)
|
||||
|
||||
#### 2.8.1 Session auth API (LIVE)
|
||||
|
||||
Base: `environment.authApiUrl` (= `https://api.dexarmarket.ru:445`). Source:
|
||||
`src/app/services/telegram-session-api.service.ts`. Narrative flow in
|
||||
`docs/AUTHENTICATION.md` §1.
|
||||
`§4 (Authentication, this document)` §1.
|
||||
|
||||
| Endpoint | Method | Auth | Body / Headers | Response |
|
||||
|---|---|---|---|---|
|
||||
@@ -771,7 +812,7 @@ GET https://api.dexarmarket.ru:445/users/sessions/3f1c2a0e-4e21-4d3a-9e77-1e8f6a
|
||||
```
|
||||
|
||||
The frontend reads fields **field-tolerantly** (accepts many casings/aliases —
|
||||
see `normalizeWebSession`, and `docs/AUTHENTICATION.md` §1.3 for the full alias
|
||||
see `normalizeWebSession`, and `§4 (Authentication, this document)` §1.3 for the full alias
|
||||
priority lists). A backend should send a real `expiresAt`/`expires` (else the
|
||||
frontend fabricates `now + 3600s`).
|
||||
|
||||
@@ -1047,10 +1088,10 @@ against `docs/context/BACKEND-AUDIT.md` (the this-session audit — the ground
|
||||
truth for what code actually does), the prior `docs/archive/BACKEND_API.md`, and the two
|
||||
sibling specs written this session:
|
||||
|
||||
- **Auth / headers / JWT** — see `docs/AUTHENTICATION.md`. This section never
|
||||
- **Auth / headers / JWT** — see `§4 (Authentication, this document)`. This section never
|
||||
re-defines the auth header format; it references it.
|
||||
- **Error response envelope + per-status semantics** — see
|
||||
`docs/ERROR_CONTRACT.md`. This section names *which* statuses apply per
|
||||
`§6 (Error Model, this document)`. This section names *which* statuses apply per
|
||||
endpoint and *why*, but the wire shape of the error body is owned by that doc.
|
||||
|
||||
### 3.0 Conventions used in this section
|
||||
@@ -1061,12 +1102,12 @@ sibling specs written this session:
|
||||
(`src/app/core/config/api-config.service.ts`). Localhost → `/api`; otherwise a
|
||||
per-tenant origin (default `https://api.dexarmarket.ru:445`). Tenant isolation
|
||||
is **by subdomain/base-URL only** — no `X-Tenant` header, no `/tenant/{id}`
|
||||
prefix (`AUTHENTICATION.md` §10). All storefront reads and all admin CRUD go
|
||||
prefix (`§4` §10). All storefront reads and all admin CRUD go
|
||||
here.
|
||||
- **Payment / QR API** — `environment.qrApiUrl` (`https://qr.vitanova.network/api`).
|
||||
Only cart/order payment + status polling (§3.3).
|
||||
- **Session-auth API** — `environment.authApiUrl`. Login/session only, covered by
|
||||
`docs/AUTHENTICATION.md`, not repeated here.
|
||||
`§4 (Authentication, this document)`, not repeated here.
|
||||
|
||||
**LIVE vs PROPOSED paths.** Only the storefront catalog/cart/engagement reads and
|
||||
the single `AdminCategoriesApiGateway` have **literal HTTP paths in code**. Every
|
||||
@@ -1102,34 +1143,34 @@ FUTURE** throughout.
|
||||
`/admin/`, `/backoffice/`, `/builder/`, or `/media/` **additionally** get
|
||||
`AdminWebSessionID` and, if an admin token is stored, `Authorization: Bearer
|
||||
<token>` (`adminAuthHeadersInterceptor`). These header names/values are owned by
|
||||
`AUTHENTICATION.md` — assume them on every admin endpoint below unless stated.
|
||||
`§4` — assume them on every admin endpoint below unless stated.
|
||||
|
||||
**JWT requirement.** No endpoint in the app requires a verified JWT **today**.
|
||||
Mechanism A (Telegram session) authenticates admins with an opaque
|
||||
`AdminWebSessionID`, not a JWT; the Ed25519 JWT flow (Mechanism B) is fully wired
|
||||
but dormant (`AUTHENTICATION.md` §2). For every admin endpoint below, "JWT
|
||||
but dormant (`§4` §2). For every admin endpoint below, "JWT
|
||||
requirement" is therefore stated as: **Today: `AdminWebSessionID` session header
|
||||
(Mechanism A). Target: `Authorization: Bearer <Ed25519 JWT>` once Mechanism B is
|
||||
cut over.** Server-side authorization must be enforced regardless of any
|
||||
client-side guard (`AUTHENTICATION.md` §11.6).
|
||||
client-side guard (`§4` §11.6).
|
||||
|
||||
**Required permission / role.** The frontend's only permission model is the
|
||||
coarse `ROLE_PERMISSIONS` table (`AUTHENTICATION.md` §9.1): `backoffice.read`,
|
||||
coarse `ROLE_PERMISSIONS` table (`§4` §9.1): `backoffice.read`,
|
||||
`backoffice.write`, `builder.read`, `builder.write`, `users.manage`,
|
||||
`settings.manage`. The frontend does **not** enforce per-domain permissions on
|
||||
admin CRUD today (the live `adminAuthGuard` only checks "is there an admin
|
||||
session at all"). Where a specific permission is the obvious fit it is named as
|
||||
**Proposed**; any finer per-role assignment is **Requires backend decision**
|
||||
(`AUTHENTICATION.md` §9.1 already flags fine-grained permissions as out of scope).
|
||||
(`§4` §9.1 already flags fine-grained permissions as out of scope).
|
||||
|
||||
**Error responses.** Unless a domain-specific note says otherwise, every endpoint
|
||||
below can return the envelope from `ERROR_CONTRACT.md` with these statuses:
|
||||
below can return the envelope from `§6` with these statuses:
|
||||
`401 UNAUTHENTICATED` (no/expired session), `403 FORBIDDEN` (wrong role/tenant),
|
||||
`500 INTERNAL_ERROR`, `503 SERVICE_UNAVAILABLE`/`MAINTENANCE_MODE`. Per-endpoint
|
||||
notes below add `404`, `409`, `422`, `429` only where they are meaningful. **The
|
||||
frontend does not parse the error body today** (`ERROR_CONTRACT.md` "Finding") —
|
||||
frontend does not parse the error body today** (`§6` "Finding") —
|
||||
admin list pages collapse every non-2xx into one generic "retry" state
|
||||
(`ERROR_CONTRACT.md` "Generic list-page error UI"). So all per-status handling
|
||||
(`§6` "Generic list-page error UI"). So all per-status handling
|
||||
below is the *contract the backend should honor*, not behavior the current UI
|
||||
distinguishes.
|
||||
|
||||
@@ -1186,7 +1227,7 @@ should treat it as authoritative for accepted field name variants; see
|
||||
is the only one returning a `total` for page math.
|
||||
- **Error responses:** `404` when `itemID` doesn't exist (P1) — but note the
|
||||
frontend does **not** distinguish 404 from any other error today
|
||||
(`ERROR_CONTRACT.md` §404); a deleted product and a 500 render the same generic
|
||||
(`§6` §404); a deleted product and a 500 render the same generic
|
||||
empty-state. `500/503` generic. Calls retry x2 with exponential backoff
|
||||
(`ApiService.retryConfig`).
|
||||
- **Validation:** none (read).
|
||||
@@ -1325,10 +1366,10 @@ DTO: `AdminCategory` (`src/app/features/admin/categories/models/admin-category.m
|
||||
- **AC7:** proactive slug-uniqueness pre-check the frontend calls *before*
|
||||
submit (`excludingId` omitted on create, set to the editing id on update). This
|
||||
is the only proactive conflict check in the whole admin surface. On network
|
||||
error it returns `false` (best-effort, TOCTOU-prone — `ERROR_CONTRACT.md` §409).
|
||||
error it returns `false` (best-effort, TOCTOU-prone — `§6` §409).
|
||||
- **Validation:** slug uniqueness via AC7; everything else none client-side.
|
||||
`title`, `slug` are non-nullable in the model. Recommended `409 CONFLICT` on
|
||||
AC3/AC4 slug collision as a race backstop (`ERROR_CONTRACT.md` §409), and
|
||||
AC3/AC4 slug collision as a race backstop (`§6` §409), and
|
||||
`422` for field errors.
|
||||
- **Error responses:** AC2/AC6 swallow errors → `null` (so a `404` there renders
|
||||
as "not found"/empty, not an error screen). AC5 hard `404` if unknown. `409`
|
||||
@@ -1384,7 +1425,7 @@ these live calls (`BACKEND-AUDIT.md` §10). Payment DTOs are inline in
|
||||
any of the documented aliases.
|
||||
- **Error responses:** O5 is fire-and-forget — a failure must **never** block the
|
||||
payment-confirmed flow (source comment, line 635). `401` on a customer-facing
|
||||
`/cart`/`/orders` has **no unified re-auth UX** today (`ERROR_CONTRACT.md` §401,
|
||||
`/cart`/`/orders` has **no unified re-auth UX** today (`§6` §401,
|
||||
Requires backend decision). Payment status polling handles its own timeouts.
|
||||
- **Validation:** none client-side. Amounts/quantities ≥ 0, valid email/phone =
|
||||
Requires backend decision.
|
||||
@@ -1502,7 +1543,7 @@ Contract: `AdminUsersGateway`
|
||||
- **Headers/JWT:** admin auth (§3.0).
|
||||
- **Permission (Proposed):** all user/role/invitation/session management →
|
||||
`users.manage` (the one permission specifically about this domain — see
|
||||
`AUTHENTICATION.md` §9.1). Read-only U1-U5 could be `backoffice.read` — Requires
|
||||
`§4` §9.1). Read-only U1-U5 could be `backoffice.read` — Requires
|
||||
backend decision.
|
||||
- **U6 body:** `{ roleId: string }`. **U7 body:** `{ status: AdminUserStatus }`
|
||||
where status ∈ `active|invited|suspended`. **U8 body:** `{ email, roleId,
|
||||
@@ -1511,7 +1552,7 @@ Contract: `AdminUsersGateway`
|
||||
— **a different type from the auth `AdminRole` string-union** used by the JWT
|
||||
`role` claim (`Owner|Administrator|Editor|Support|ReadOnly`,
|
||||
`src/app/core/auth/models/permission.model.ts`). **This is the flagged
|
||||
duplicate** (`BACKEND-AUDIT.md` §14, `AUTHENTICATION.md` §9). Reconciliation
|
||||
duplicate** (`BACKEND-AUDIT.md` §14, `§4` §9). Reconciliation
|
||||
needed: treat the auth string-union as the JWT/role-claim contract; treat this
|
||||
interface as an admin-users-management row. Recommend renaming the latter (e.g.
|
||||
`AdminUserRoleRecord`). **Requires backend decision / naming reconciliation.**
|
||||
@@ -1522,7 +1563,7 @@ Contract: `AdminUsersGateway`
|
||||
- **Sessions/audit:** `revokeSession` (U10) exists as a mock method; whether an
|
||||
Owner can remotely kill another admin's live session, and how that propagates to
|
||||
the already-logged-in client, is **Requires backend decision**
|
||||
(`AUTHENTICATION.md` §8 — no push/poll mechanism exists client-side).
|
||||
(`§4` §8 — no push/poll mechanism exists client-side).
|
||||
- **Validation:** none client-side. Email format, role existence, self-demotion
|
||||
guards = Requires backend decision.
|
||||
- **Error responses:** `404` (unknown user/session/invitation), `409` (e.g.
|
||||
@@ -1544,7 +1585,7 @@ reconciliation the backend must make (this is the audit's flagged item):
|
||||
- **`AdminRole` #1** — string union `'Owner'|'Administrator'|'Editor'|'Support'
|
||||
|'ReadOnly'` (`src/app/core/auth/models/permission.model.ts`). Used by the JWT
|
||||
`role` claim and `PermissionService`. Ordered high→low by convention only. Maps
|
||||
to permissions via `ROLE_PERMISSIONS` (`AUTHENTICATION.md` §9.1). **This is the
|
||||
to permissions via `ROLE_PERMISSIONS` (`§4` §9.1). **This is the
|
||||
role contract for authorization.**
|
||||
- **`AdminRole` #2** — interface `{ id, name, permissions: string[], builtIn }`
|
||||
(`src/app/features/admin/users/models/admin-user.model.ts`). A management row for
|
||||
@@ -1646,7 +1687,7 @@ view model: `ContentPage` (`src/app/features/content-management/models/content-p
|
||||
bootstrap is published).
|
||||
- **Validation:** the client-side `ContentPageService.validatePages()` +
|
||||
`ProjectValidator` convention exists (`ProjectValidationIssue { code, message,
|
||||
section, fieldKey, severity }`) and is the shape `ERROR_CONTRACT.md` §422
|
||||
section, fieldKey, severity }`) and is the shape `§6` §422
|
||||
recommends aligning backend `details[]` to. Actual required fields (slug
|
||||
uniqueness, non-empty title) = Requires backend decision.
|
||||
- **Error responses:** `404` (CM2/CM4/CM5), `409` (duplicate slug — proposed),
|
||||
@@ -1891,7 +1932,7 @@ sections already covered (§3.8-3.14) and `BootstrapConfig`'s remaining sub-conf
|
||||
`productPage`, `userExperience`, `company`, `apiEndpoints`, `permissions`). All of
|
||||
these are **served inside `GET /bootstrap`** and edited (where editable) via the
|
||||
project-editor in-memory (LOCAL-ONLY). The only permission that names settings is
|
||||
`settings.manage` (Owner-only, `AUTHENTICATION.md` §9.1).
|
||||
`settings.manage` (Owner-only, `§4` §9.1).
|
||||
|
||||
The unifying gap: **there is no client-side "publish/save bootstrap" HTTP call
|
||||
anywhere** (`BACKEND-AUDIT.md` §17, §25.5). Everything in §3.8-3.15 is persisted
|
||||
@@ -2129,46 +2170,106 @@ token-bound admin gateway. Proposed: `GET {base}/backoffice/dashboard/metrics`
|
||||
|
||||
### 3.21 Cross-cutting "Requires backend decision" register (this section)
|
||||
|
||||
Consolidated so the master doc can dedupe against the auth/error registers:
|
||||
Consolidated so the master doc can dedupe against the auth/error registers.
|
||||
Each item has a **Recommended default** — apply it unless you have a specific
|
||||
reason not to; only stop and ask a human where none is given or the item has
|
||||
real business/money/legal weight (marked accordingly).
|
||||
|
||||
1. **DI seam prerequisite** — orders, products, users, transactions, monitoring,
|
||||
moderation (+ derived customers/analytics) inject their `*LocalGateway`
|
||||
concretely; a backend requires introducing a DI token first (`BACKEND-AUDIT.md`
|
||||
§14). Only categories + dashboard-metrics are token-bound today.
|
||||
**Recommended default:** this is frontend work (adding the token seam), do
|
||||
it for every domain before wiring its real gateway — mechanical, no backend
|
||||
input needed. See §8.5 for the exact per-domain pattern.
|
||||
2. **All admin CRUD paths are PROPOSED** — only `/backoffice/categories*` and the
|
||||
storefront catalog/cart/engagement paths are literal. Adopt the
|
||||
`/backoffice/<domain>` convention (or reject it).
|
||||
**Recommended default:** adopt it as-is — it's already live for categories,
|
||||
changing it later is a bigger cost than any alternative naming would save.
|
||||
3. **Server-owned field lists** on create/update for products/orders/etc. (only
|
||||
categories defines one, by stripping `id/itemsCount/deletedAt/createdAt/updatedAt`).
|
||||
**Recommended default:** mirror the categories pattern exactly (server strips
|
||||
`id`/counts/timestamps from client payloads; client never sends them).
|
||||
4. **Field validation + error wording** — the frontend enforces essentially none
|
||||
on admin CRUD (no reactive `Validators`); all `422` field rules and messages are
|
||||
backend-owned.
|
||||
**Recommended default:** validate required/type/length server-side per the DTO
|
||||
shapes given in §3; return field errors in the §6 `422` shape (`fieldErrors`
|
||||
keyed by field path). Don't wait on frontend validation to be added first.
|
||||
5. **Product slug/sku uniqueness** — no proactive check exists (unlike categories'
|
||||
`isSlugTaken`); decide `409` behavior.
|
||||
**Recommended default:** mirror categories — add an `isSlugTaken`-equivalent
|
||||
check endpoint, and return `409` with the §6 conflict shape on collision.
|
||||
6. **Order status-transition legality** and refund eligibility rules.
|
||||
**Business decision, not technical** — the valid state machine and who can
|
||||
trigger which transition depends on the merchant's actual fulfillment
|
||||
process. Ask.
|
||||
7. **Customers as a first-class resource** vs. derived-from-orders aggregate.
|
||||
**Recommended default:** derived-from-orders aggregate (§3.4 as written) —
|
||||
it's what the frontend already assumes and needs no new storage model. Only
|
||||
promote to first-class if a real need appears (e.g. customer accounts,
|
||||
guest-order merging).
|
||||
8. **`AdminRole` duplication** — auth string-union vs. users-management interface
|
||||
(naming reconciliation; role CRUD / custom roles).
|
||||
**Recommended default:** keep the coarse `AdminRole` string union (§4 §9.1)
|
||||
as the enforced permission source; treat the users-management interface's
|
||||
richer shape as display-only until custom roles are actually requested by
|
||||
the product.
|
||||
9. **Remote session revocation propagation** (`revokeSession`) to a logged-in
|
||||
client (`AUTHENTICATION.md` §8 — no push/poll exists).
|
||||
client (`§4` §8 — no push/poll exists).
|
||||
**Recommended default:** accept the propagation delay (revoked session stays
|
||||
client-usable until its next natural refresh-interval tick, §4 §8) rather
|
||||
than building push/poll infrastructure for this alone — revisit only if a
|
||||
real incident makes the delay unacceptable.
|
||||
10. **Media**: allowed mime/size; whether in-use assets are delete-blocked.
|
||||
**Recommended default:** allow the mime types the client already filters
|
||||
for (`image/*`, `application/pdf`, `image/svg+xml` — §7.1 `MediaAssetKind`),
|
||||
10MB per file; block delete of in-use assets with a `409` listing the
|
||||
referencing entities (safer default than silent broken references).
|
||||
11. **CMS/homepage/widgets/nav/footer/branding/languages/settings are LOCAL-ONLY**
|
||||
— the single biggest decision is **whole-bootstrap publish (S2) vs. granular
|
||||
builder endpoints** (§3.15). No builder write call exists in code today.
|
||||
**Recommended default:** whole-bootstrap publish (a single `PUT`/`POST` that
|
||||
replaces the tenant's bootstrap document) — matches how the frontend already
|
||||
edits it as one in-memory draft object; granular per-section endpoints are
|
||||
strictly more backend work for no frontend benefit today.
|
||||
12. **Languages**: duplicate-add `409`, removing the default/last locale, BCP-47
|
||||
validation.
|
||||
**Recommended default:** `409` on duplicate-add; reject removing the last
|
||||
remaining locale or the current default (`422`); validate against BCP-47
|
||||
syntax, not a fixed enum (frontend already treats locale as an open string).
|
||||
13. **Transactions ↔ live payment records** are unconnected models; how they link.
|
||||
**Recommended default:** key `AdminTransaction` off the same order/payment id
|
||||
the live QR/card flow already returns (§3.3.a) rather than inventing a new
|
||||
identifier — one payment, one transaction record.
|
||||
14. **Reviews**: storefront `Review` vs. admin `AdminReview` linkage; how a
|
||||
submitted review enters the moderation queue.
|
||||
**Recommended default:** every storefront-submitted review starts in the
|
||||
moderation queue at `pending` status; `AdminReview` is the same underlying
|
||||
record with moderation fields attached, not a separate entity.
|
||||
15. **Reports**: no creation flow exists — who creates reports.
|
||||
**Business decision** — depends on whether "reports" means customer-flagged
|
||||
content, automated abuse detection, or admin-initiated audits. Ask.
|
||||
16. **Monitoring**: event windowing/pagination for a real stream; webhook
|
||||
*management* (vs. read-only delivery log) is out of scope.
|
||||
**Recommended default:** offset-paginated `GET` with a `since`/time-range
|
||||
filter (matches §2.2's standard pagination contract) — no need for a
|
||||
streaming protocol for an admin-facing log view.
|
||||
17. **Analytics**: `conversionRate`/`retentionPercent` must stay `null` when
|
||||
unknown; no traffic/visitor data source exists anywhere — real analytics is a
|
||||
from-scratch backend build.
|
||||
**Recommended default:** ship order/product/revenue aggregates first (real
|
||||
data, computable from orders/products already in scope); leave traffic-based
|
||||
metrics `null` until a real analytics/traffic pipeline exists — never
|
||||
fabricate a number, the frontend already renders `null` as "Unknown."
|
||||
18. **Variants/options** shape reconciliation between storefront `variantOptions`
|
||||
(grouped) and admin `variants` (flat priced rows); no dedicated variants
|
||||
endpoint (created within the product body).
|
||||
**Recommended default:** keep variants embedded in the product body (no
|
||||
separate variants endpoint) — matches how the admin editor already sends
|
||||
them; the backend computes/derives the storefront's grouped
|
||||
`variantOptions` shape from the same flat rows at read time.
|
||||
|
||||
---
|
||||
|
||||
@@ -2948,44 +3049,73 @@ of anything to the backend.
|
||||
### 12. Open items — "Requires backend decision"
|
||||
|
||||
Consolidated list of everything this document could not derive from
|
||||
existing frontend code and therefore does not prescribe:
|
||||
existing frontend code and therefore does not prescribe. Each has a
|
||||
**Recommended default**; apply it unless the item is flagged as a real
|
||||
business/security decision.
|
||||
|
||||
- **Public-key enrollment mechanism** (§2.1) — how an admin's Ed25519
|
||||
`publicKeyBase64` gets associated with an account/role server-side (admin
|
||||
tool? one-time enrollment link? manual DB entry?). Zero frontend code
|
||||
exists for this by design.
|
||||
**Recommended default:** an authenticated admin-management endpoint
|
||||
(`POST /backoffice/users/{id}/public-key`), callable only by an
|
||||
already-authenticated Owner/Admin — treat it as a users-management
|
||||
action (§3.5), not a public/unauthenticated enrollment flow.
|
||||
- **Refresh-token reuse/compromise detection** (§6) — whether presenting an
|
||||
already-rotated-out refresh token should revoke the whole token family.
|
||||
Not implied by any frontend behavior.
|
||||
**Recommended default:** yes — revoke the whole token family on reuse of
|
||||
a rotated-out refresh token (standard rotation-detection practice); this
|
||||
is a security posture, not just a technical convention, so treat as
|
||||
security-review-required rather than silently skippable.
|
||||
- **Session/token revocation propagation** (§8) — whether/how a
|
||||
remotely-revoked admin session (e.g. via the mock `AdminUsersGateway.
|
||||
revokeSession`) is communicated to an already-logged-in client before its
|
||||
next refresh cycle. No push/poll mechanism exists today.
|
||||
**Recommended default:** accept the propagation delay (§3.21 item 9) —
|
||||
don't build push infrastructure for this alone.
|
||||
- **Tenant scoping of auth requests** (§10) — whether login/challenge/verify
|
||||
need an explicit tenant identifier in the payload, versus relying on
|
||||
request origin. Not present in any current auth payload.
|
||||
**Recommended default:** rely on request origin/host (matches how
|
||||
`ApiConfigService.getBaseUrl()` already resolves tenant elsewhere, §1.9)
|
||||
rather than adding a redundant payload field.
|
||||
- **Relationship between the two mechanisms at cutover** — replace
|
||||
`adminAuthGuard` with `ed25519AuthGuard` outright, or run both and let
|
||||
role/tenant config decide? Explicitly called out in the prior
|
||||
`docs/AUTH.md` as "a product decision, not made here," and nothing has
|
||||
changed that.
|
||||
**Business decision, not technical** — ask. (This is about auth security
|
||||
posture and rollout risk, not a convention.)
|
||||
- **`AdminAuthService`'s reserved JWT-pair slots** (`adminToken`/
|
||||
`adminRefreshToken`, §4) — whether Mechanism A is ever meant to gain its
|
||||
own token pair (as the reserved-but-unused storage suggests) independent
|
||||
of the Ed25519 migration, or whether that code is dead and should be
|
||||
removed. Not resolved by current usage (nothing writes to it).
|
||||
**Recommended default:** treat as dead/reserved-but-unused; don't build
|
||||
backend support for it. Remove client-side only if a dedicated frontend
|
||||
cleanup pass confirms nothing depends on the reserved slots existing.
|
||||
- **`AdminRole` naming collision** (§9) — a reconciliation/rename decision
|
||||
between `core/auth/models/permission.model.ts`'s string union and
|
||||
`features/admin/users/models/admin-user.model.ts`'s interface; flagged,
|
||||
not resolved, by this document.
|
||||
**Recommended default:** same as §3.21 item 8 — keep the coarse string
|
||||
union as the enforced source, treat the richer interface as display-only.
|
||||
- **Fine-grained/per-domain permissions** (§9.1) — the current model is
|
||||
intentionally coarse; whether a richer permission model is ever needed is
|
||||
a backend/product decision.
|
||||
**Recommended default:** keep it coarse (`backoffice.read`/
|
||||
`backoffice.write`/`builder.read`/`builder.write`/`settings.manage`) —
|
||||
don't build fine-grained permissions speculatively; the frontend has no
|
||||
UI that would use finer granularity today.
|
||||
- **`APP_INITIALIZER` wiring for `AuthFacade.restoreSession()`** (§5.2) — the
|
||||
code comment says this should be wired in before the Ed25519 flow goes
|
||||
live, but it is not wired in today. This is frontend follow-up work, not a
|
||||
backend decision, but is listed here because it changes what "session
|
||||
restored on refresh" means in practice until it lands.
|
||||
**Recommended default:** frontend must wire this before Ed25519 goes live
|
||||
in production — flag as a blocking frontend task in the cutover plan, not
|
||||
something backend needs to wait on.
|
||||
|
||||
---
|
||||
|
||||
@@ -2993,9 +3123,9 @@ existing frontend code and therefore does not prescribe:
|
||||
|
||||
Every claim below is grounded in current source on branch `B2B`. Where the
|
||||
frontend implies nothing, the item is marked **Requires backend decision**.
|
||||
Auth and error contracts are owned by sibling docs — this section references
|
||||
`docs/AUTHENTICATION.md` and `docs/ERROR_CONTRACT.md` rather than restating
|
||||
them, and only covers the security-relevant angle.
|
||||
Auth and error contracts are owned by §4 and §6 respectively — this section
|
||||
references them rather than restating them, and only covers the
|
||||
security-relevant angle.
|
||||
|
||||
### 5.1 Origin
|
||||
|
||||
@@ -3012,7 +3142,7 @@ The frontend calls a small, fixed set of origins, all declared in
|
||||
| `http://ip-api.com/json/...` | literal in `LocationService` | external geo-IP autodetect (no key, plaintext HTTP) |
|
||||
|
||||
The tenant marketplace origin is chosen by **subdomain** (`TenantResolverService`,
|
||||
see `docs/AUTHENTICATION.md` §10) — there is no `X-Tenant` header or tenant
|
||||
see `§4 (Authentication, this document)` §10) — there is no `X-Tenant` header or tenant
|
||||
path prefix. **Backend note:** the browser will make cross-origin requests to
|
||||
`api.dexarmarket.ru:445` and `qr.vitanova.network` from whatever host the SPA
|
||||
is served on (e.g. `dexarmarket.ru`), so those origins must return correct
|
||||
@@ -3058,7 +3188,7 @@ mandatory. Requires backend decision only if that design change is made.
|
||||
|
||||
### 5.4 JWT
|
||||
|
||||
Cross-reference `docs/AUTHENTICATION.md` §3–§4. Security-relevant storage
|
||||
Cross-reference `§4 (Authentication, this document)` §3–§4. Security-relevant storage
|
||||
angle only:
|
||||
|
||||
- **Live today (Telegram session, Mechanism A):** no JWT at all — an opaque
|
||||
@@ -3081,16 +3211,16 @@ angle only:
|
||||
Refresh tokens are protected client-side only as well as `localStorage`
|
||||
protects them — i.e. not from XSS (§5.4). The frontend never decodes the
|
||||
refresh token (round-tripped opaque), stores it beside the access token, and
|
||||
replaces the pair on every `/verify` and `/refresh` (`docs/AUTHENTICATION.md`
|
||||
replaces the pair on every `/verify` and `/refresh` (`§4 (Authentication, this document)`
|
||||
§4, §6). Rotation is **expected** but only the backend can enforce it. There is
|
||||
no client-side reuse-detection. **Requires backend decision** (already flagged
|
||||
in `docs/AUTHENTICATION.md` §6): single-use refresh tokens + reuse/compromise
|
||||
in `§4 (Authentication, this document)` §6): single-use refresh tokens + reuse/compromise
|
||||
revocation cascade — nothing in the frontend implies or depends on it.
|
||||
|
||||
### 5.6 Rate limiting
|
||||
|
||||
There is **no client-side rate-limit handling** (grep: no `429` / "rate limit"
|
||||
reference in `src/`; confirmed by `docs/ERROR_CONTRACT.md` §429). The frontend
|
||||
reference in `src/`; confirmed by `§6 (Error Model, this document)` §429). The frontend
|
||||
does, however, apply **debounce** on user-driven request bursts, which implies
|
||||
the natural request cadence a backend limiter should tolerate rather than block:
|
||||
|
||||
@@ -3102,13 +3232,13 @@ the natural request cadence a backend limiter should tolerate rather than block:
|
||||
|
||||
**Requires backend decision** on the rate-limit contract end to end (limits,
|
||||
`Retry-After` header vs `retryAfterSeconds` body field, and whether the
|
||||
frontend should auto-retry). Per `docs/ERROR_CONTRACT.md` §429, no retry
|
||||
frontend should auto-retry). Per `§6 (Error Model, this document)` §429, no retry
|
||||
interceptor exists — honoring 429 is net-new frontend work, not a config
|
||||
change.
|
||||
|
||||
### 5.7 Replay protection
|
||||
|
||||
The Ed25519 signing flow (`docs/AUTHENTICATION.md` §2) signs the **raw
|
||||
The Ed25519 signing flow (`§4 (Authentication, this document)` §2) signs the **raw
|
||||
backend-issued nonce string exactly as received** — `Ed25519KeypairService.sign(nonce)`
|
||||
applies no client-side framing, prefix, hashing, timestamp, or
|
||||
counter (`src/app/core/auth/services/auth.service.ts` login flow;
|
||||
@@ -3290,7 +3420,7 @@ admin action the UI exposes; enforcement must be entirely server-side.
|
||||
|
||||
The **intended** matrix comes from the dormant Ed25519 flow's `ROLE_PERMISSIONS`
|
||||
table (`src/app/core/auth/models/permission.model.ts`, mirrored in
|
||||
`docs/AUTHENTICATION.md` §9.1). Roles are the string union
|
||||
`§4 (Authentication, this document)` §9.1). Roles are the string union
|
||||
`Owner | Administrator | Editor | Support | ReadOnly`; permission domains are
|
||||
`backoffice`, `builder`, `users`, `settings`:
|
||||
|
||||
@@ -3304,7 +3434,7 @@ table (`src/app/core/auth/models/permission.model.ts`, mirrored in
|
||||
|
||||
Guards that would eventually enforce this (`ed25519AuthGuard`,
|
||||
`permissionGuard(permission)`) exist but are **not referenced by any route**
|
||||
today (`docs/AUTHENTICATION.md` §11). The mapping is deliberately coarse —
|
||||
today (`§4 (Authentication, this document)` §11). The mapping is deliberately coarse —
|
||||
there is no per-domain granularity (e.g. "edit prices but not delete products")
|
||||
anywhere client-side.
|
||||
|
||||
@@ -3315,7 +3445,7 @@ anywhere client-side.
|
||||
needed (none implied client-side).
|
||||
- **Requires backend decision:** the `AdminRole` naming collision (auth string
|
||||
union vs the users-management `AdminRole` interface with `{id, name,
|
||||
permissions[], builtIn}`) — flagged in `docs/AUTHENTICATION.md` §9; the
|
||||
permissions[], builtIn}`) — flagged in `§4 (Authentication, this document)` §9; the
|
||||
users-management interface suggests custom/non-builtin roles with arbitrary
|
||||
permission-string sets, which the coarse 5-role union does not model. Reconcile
|
||||
before building the backend role table.
|
||||
@@ -3619,10 +3749,10 @@ maintenance window should send `"code": "MAINTENANCE_MODE"` (still with HTTP
|
||||
status 503, since it's a byte-identical "the service is not accepting
|
||||
requests" situation, but a different reason). This is the only way for the
|
||||
frontend to build a distinct maintenance-mode UX later, since status alone
|
||||
is not enough. `docs/MAINTENANCE_MODE.md` (sibling task, in progress) owns
|
||||
the UX/copy for the maintenance case — this document only fixes the wire
|
||||
signal it must key off (`error.code === "MAINTENANCE_MODE"`), so the two
|
||||
docs stay consistent without duplicating UX detail here.
|
||||
is not enough. §10 (Maintenance Mode) owns the UX/copy for the maintenance
|
||||
case — this section only fixes the wire signal it must key off
|
||||
(`error.code === "MAINTENANCE_MODE"`), consistent with §10 without
|
||||
duplicating UX detail here.
|
||||
|
||||
#### Maintenance mode
|
||||
|
||||
@@ -3647,7 +3777,7 @@ should be treated as advisory-only.
|
||||
|
||||
**Frontend reaction today:** none — no maintenance-mode concept exists in
|
||||
the frontend at all currently (confirmed: no matches for "maintenance" in
|
||||
`src/`). This entire row is new; the sibling `docs/MAINTENANCE_MODE.md` task
|
||||
`src/`). This entire row is new; the sibling `§10 (Maintenance Mode, this document)` task
|
||||
should treat it as building from scratch, not preserving anything.
|
||||
|
||||
#### Tenant disabled
|
||||
@@ -3728,11 +3858,9 @@ not something this contract can silently paper over.
|
||||
}
|
||||
```
|
||||
|
||||
Ties to the Ed25519 admin-auth flow documented in `AUTHENTICATION.md`
|
||||
(sibling task, in progress) — keep the `code` value (`INVALID_SIGNATURE`)
|
||||
consistent with whatever that doc names the failure mode, since this
|
||||
contract only defines the wire shape and that doc owns the auth-flow
|
||||
narrative.
|
||||
Ties to the Ed25519 admin-auth flow documented in §4 — keep the `code`
|
||||
value (`INVALID_SIGNATURE`) consistent with what §4 names the failure mode;
|
||||
this contract only defines the wire shape, §4 owns the auth-flow narrative.
|
||||
|
||||
**Frontend reaction today:** same gap as "Expired token" above.
|
||||
`AuthService.login()` passes `fallbackCode: 'invalid-signature'` into
|
||||
@@ -3789,25 +3917,56 @@ per-status handling, except in the Ed25519 admin-auth module.
|
||||
|
||||
### Summary: "Requires backend decision" items
|
||||
|
||||
Each has a **Recommended default** — apply it unless flagged otherwise.
|
||||
|
||||
- **Envelope adoption** — the `{ error: { code, message, status, requestId,
|
||||
details? } }` shape itself; no frontend code parses any envelope today.
|
||||
**Recommended default:** adopt it as specified — every status-by-status
|
||||
example in this section already uses it; it's the shape to build against.
|
||||
- **401 on customer-facing marketplace calls** (`/cart`, `/orders`, etc.) —
|
||||
no unified "session expired, please re-auth" UX exists for the customer
|
||||
Telegram-session flow.
|
||||
**Recommended default:** return the standard envelope with
|
||||
`code: "UNAUTHORIZED"`; the frontend re-auth UX is a follow-up frontend
|
||||
task once the backend consistently returns this, not something to block
|
||||
the backend on.
|
||||
- **403 tenant-mismatch vs role-mismatch** distinct copy/code.
|
||||
**Recommended default:** two distinct codes (`TENANT_MISMATCH`,
|
||||
`FORBIDDEN`) — the frontend can pick copy per-code once they exist;
|
||||
collapsing them into one code loses information for no benefit.
|
||||
- **404 vs generic-error distinct UX** on catalog/product pages (currently
|
||||
identical).
|
||||
**Recommended default:** return real `404` for missing resources (not a
|
||||
generic `500`) — this is a backend correctness item, not a decision;
|
||||
the frontend UX unification is a separate, non-blocking follow-up.
|
||||
- **409 conflict handling on submit** (today only a proactive
|
||||
`isSlugTaken` pre-check exists; no reactive 409 handler).
|
||||
**Recommended default:** return `409` with the standard envelope on any
|
||||
uniqueness/concurrency conflict; treat the proactive pre-check as
|
||||
best-effort only (§3.21 item 5 — TOCTOU-prone by nature).
|
||||
- **422 `details[]` → inline field-error adapter** for admin forms (the
|
||||
client-side `fieldError()` convention exists but nothing feeds it from a
|
||||
backend response yet).
|
||||
**Recommended default:** populate `details[]` as `{ field, message }` per
|
||||
invalid field on every `422` — the client-side adapter already expects
|
||||
this shape, it just has nothing to consume yet.
|
||||
- **429 rate-limit contract end to end** — header vs body, retry-after
|
||||
value, and whether the frontend auto-retries (nothing exists today).
|
||||
**Recommended default:** standard `Retry-After` header (seconds) +
|
||||
envelope body with `code: "RATE_LIMITED"`; no frontend auto-retry (the
|
||||
user retries manually) — simplest safe default, matches how the rest of
|
||||
the app already treats errors as terminal-until-user-action.
|
||||
- **Maintenance-mode `maintenanceUntil` reliability** — advisory only, or
|
||||
can the frontend promise an ETA.
|
||||
**Recommended default:** advisory only — word the frontend copy as
|
||||
"expected back around X" not a guarantee, so an inaccurate estimate
|
||||
doesn't need special-case backend handling.
|
||||
- **Tenant-disabled status code and screen** — reuse `forbidden` copy (wrong
|
||||
wording today) vs. add a dedicated `AuthErrorCode`.
|
||||
**Recommended default:** dedicated `code: "TENANT_DISABLED"` (already
|
||||
named consistently elsewhere in this doc, §1.12) rather than overloading
|
||||
`forbidden` — one wrong-wording bug is enough of a reason not to add a
|
||||
second overload.
|
||||
- **Expired-token / invalid-signature body-code fix** — both are
|
||||
**pre-existing frontend bugs**, not just missing decisions:
|
||||
`toAuthErrorShape()` in `auth.service.ts` currently derives the error code
|
||||
@@ -3815,6 +3974,12 @@ per-status handling, except in the Ed25519 admin-auth module.
|
||||
so the `'session-expired'` and `'invalid-signature'` screens are dead code
|
||||
from any real backend response today. Fixing this requires both a backend
|
||||
body `error.code` and a frontend change to prefer it.
|
||||
**Recommended default:** backend returns `code: "TOKEN_EXPIRED"` /
|
||||
`code: "INVALID_SIGNATURE"` on the relevant `401`s; frontend fix (prefer
|
||||
body code over status-derived fallback) is tracked separately in
|
||||
`docs/KNOWN-ISSUES.md` and is not blocking backend work — the backend
|
||||
side of this can and should ship regardless of when the frontend fix
|
||||
lands.
|
||||
|
||||
---
|
||||
|
||||
@@ -4546,8 +4711,8 @@ itself is unavailable, not the whole API).
|
||||
any other request failure. `ApiService` (`src/app/services/api.service.ts`) has no
|
||||
per-endpoint error branching for 503; a failed `createCartPayment()`/`createOrder()`
|
||||
call surfaces through whatever generic error handling the checkout components already
|
||||
have for network failures (out of scope for this doc — see the sibling
|
||||
`ERROR_CONTRACT.md` task for the general error-response shape).
|
||||
have for network failures (out of scope for this section — see §6 for the
|
||||
general error-response shape).
|
||||
|
||||
**No frontend UI currently exists for this — requires a future frontend task.** The
|
||||
checkout flow would need a "payments unavailable" inline state (banner or disabled
|
||||
@@ -4593,7 +4758,7 @@ endpoints keep working. `403 Forbidden` is arguably more correct REST semantics
|
||||
+ `Retry-After` communicates "temporary" more clearly to a client and is
|
||||
recommended so the frontend can offer a countdown/retry consistent with §5's pattern.
|
||||
**Requires backend decision:** which status code is authoritative — this should be
|
||||
pinned down jointly with whatever `ERROR_CONTRACT.md` settles on for its 5xx
|
||||
pinned down jointly with whatever `§6` settles on for its 5xx
|
||||
conventions, since read-only is really "a subset of write endpoints return
|
||||
maintenance-503."
|
||||
|
||||
@@ -4603,7 +4768,7 @@ ahead of a failed request (e.g. `featureFlags.readOnly` or a dedicated
|
||||
`platformStatus.readOnly` field would need to be added to `BootstrapConfig` if the
|
||||
product wants a proactive banner instead of a reactive failure). Reactive handling
|
||||
(showing an error when the write call 503s) can reuse the same inline
|
||||
error-state pattern as §3/§6 once `ERROR_CONTRACT.md` defines the generic error body
|
||||
error-state pattern as §3/§6 once `§6` defines the generic error body
|
||||
handling.
|
||||
|
||||
---
|
||||
@@ -4679,12 +4844,8 @@ richer shape from §5, scoped to `module`, not a plain `featureFlags` boolean.
|
||||
|
||||
All maintenance-scenario responses use HTTP `503 Service Unavailable` (except the
|
||||
read-only debate in §4) with a `Retry-After` header (seconds, standard HTTP) and a
|
||||
JSON body. This is written to be consistent with, not contradict, whatever
|
||||
`ERROR_CONTRACT.md` (sibling task, in progress) settles on for its general
|
||||
structured-error envelope — if that doc defines a different top-level error
|
||||
shape (e.g. `{ error: { code, message, ... } }` vs. a flatter shape), this body
|
||||
should be nested under that envelope rather than duplicating a competing shape.
|
||||
Pending that reconciliation, the fields below are what the frontend needs regardless
|
||||
JSON body, nested under the §6 error envelope (`{ error: { code, message, ... } }`)
|
||||
rather than as a competing top-level shape. The fields below are what the frontend needs regardless
|
||||
of the outer envelope:
|
||||
|
||||
```json
|
||||
@@ -4722,7 +4883,7 @@ Per-scenario summary:
|
||||
| Global | 503 | `"global"` | On every endpoint, especially `/bootstrap` |
|
||||
| Per-tenant | 503 | `"tenant"` | On that tenant's `/bootstrap` and all its endpoints |
|
||||
| Per-module | 503 | `"module"` | Only on that module's endpoints (e.g. `/cart`, `/orders`) |
|
||||
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `ERROR_CONTRACT.md` |
|
||||
| Read-only | 503 or 403 | `"readonly"` | Only on write endpoints; GETs unaffected — pin down with `§6` |
|
||||
| Scheduled (advance notice) | 200, via `bootstrap.maintenanceNotice` | n/a | Not an error response — a proactive field on the normal `/bootstrap` payload, see §5 |
|
||||
| Temporary feature disable | 200, via `bootstrap.featureFlags.<key> = false` | n/a | Not an error response — existing bootstrap mechanism, see §6 |
|
||||
|
||||
@@ -4771,19 +4932,35 @@ these.
|
||||
|
||||
### Requires backend decision (full list)
|
||||
|
||||
Each has a **Recommended default** — apply it unless flagged otherwise.
|
||||
|
||||
- §1: whether a dedicated `GET /status`/`GET /maintenance` probe should exist for
|
||||
auto-recovery polling, beyond a plain 503 on `/bootstrap`.
|
||||
**Recommended default:** no dedicated probe — reuse `GET /bootstrap`'s
|
||||
existing 503/200 as the recovery signal; a second endpoint duplicates it.
|
||||
- §2: the `scope` discriminator (`"global"` vs `"tenant"`) so the frontend can tell
|
||||
the two apart from a single tenant's bootstrap response; and whether a
|
||||
disabled tenant's branding can still resolve for a branded takeover page.
|
||||
**Recommended default:** include `scope` in the 503 body (§7 shape);
|
||||
serve branding for a disabled-but-known tenant so the takeover page can
|
||||
still be branded (only truly unknown tenants get the unbranded default).
|
||||
- §3: none beyond adopting the §7 response shape per-endpoint — this one is mostly
|
||||
frontend-gap, not backend-undecided.
|
||||
- §4: which status code is authoritative for read-only (`503` vs `403`) — to be
|
||||
pinned down jointly with `ERROR_CONTRACT.md`.
|
||||
pinned down jointly with §6.
|
||||
**Recommended default:** `503` with `code: "READ_ONLY"` — it's a temporary
|
||||
service-state condition, not a permissions failure, so it belongs in the
|
||||
5xx family alongside the rest of maintenance mode, not 4xx.
|
||||
- §5: whether scheduled-maintenance notice ships via a `bootstrap.maintenanceNotice`
|
||||
field (proposed) or a separate polling endpoint.
|
||||
**Recommended default:** the `bootstrap.maintenanceNotice` field — it
|
||||
rides the existing bootstrap fetch/cache lifecycle for free; a separate
|
||||
polling endpoint adds a new recurring request for no added value.
|
||||
- §7: how this document's 503 body nests inside whatever outer envelope
|
||||
`ERROR_CONTRACT.md` defines.
|
||||
§6 defines.
|
||||
**Recommended default:** already resolved — nest under the §6 envelope
|
||||
(`{ error: { code, message, status, requestId, ... } }`) as shown in this
|
||||
section's JSON examples; no separate top-level shape.
|
||||
|
||||
### No frontend UI currently exists for this — requires a future frontend task (full list)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user