Files
marketplaces/docs/BACKEND_INTEGRATION.md

4372 lines
233 KiB
Markdown
Raw Normal View History

# Backend Integration — Canonical Specification
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
**This is the single source of truth for backend implementation.** It supersedes and merges `docs/archive/BACKEND_API.md`, `docs/AUTH.md`, `docs/ADMIN.md`, and `docs/BACKEND_API_REMAINING_WORK.md` (all archived — see `docs/archive/` and the note at the end of this file). It incorporates `docs/AUTHENTICATION.md` (§4) and `docs/ERROR_CONTRACT.md` (§6) in full; those files remain in place as standalone references but this document is authoritative. `docs/MAINTENANCE_MODE.md` is a companion doc, referenced from §6.
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.
## Table of contents
1. [Bootstrap](#1-bootstrap)
2. [Endpoint Documentation Framework](#2-endpoint-documentation-framework)
3. [CRUD Contracts](#3-crud-contracts)
4. [Authentication](#4-authentication)
5. [Security](#5-security)
6. [Error Model](#6-error-model)
7. [Uploads](#7-uploads)
8. [Real Backend Implementation Guide](#8-real-backend-implementation-guide)
9. [Backend Checklist](#9-backend-checklist)
---
## 1. Bootstrap
The bootstrap document is the single runtime-configuration payload that drives
the entire multi-tenant storefront/builder/backoffice. It is fetched once at
app startup and held in memory; nearly every feature (theme, navigation,
localization, catalog behavior, static pages, feature flags, widget registry)
reads from it rather than from dedicated per-feature endpoints.
Source of truth for this section:
`src/app/core/bootstrap/providers/api-bootstrap.provider.ts`,
`src/app/core/config/config.service.ts`,
`src/app/core/config/api-config.service.ts`,
`src/app/core/config/tenant-resolver.service.ts`,
`src/app/shared/models/config/*`, and the mock document
`src/assets/mock/bootstrap/bootstrap.json`. Cross-referenced against
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
`docs/context/BACKEND-AUDIT.md` §6, §2 and the prior `docs/archive/BACKEND_API.md`.
### 1.1 Request contract
| Property | Value |
|---|---|
| Method | `GET` |
| Route (API mode) | `/bootstrap` — relative; rewritten onto the resolved tenant API base by `apiBaseUrlInterceptor` (see §1.10) |
| Route (mock/local mode) | `GET /assets/mock/bootstrap/bootstrap.json` (static asset, no backend) |
| Request body | none |
| Query params | none |
| Auth requirement | **None.** Bootstrap is fetched before any login and must be publicly cacheable per tenant. It carries no `Authorization` header and no `WebSessionID` is required for it to succeed. |
**Headers.** The provider itself (`ApiBootstrapProvider`) sets no explicit
headers — it issues a plain `HttpClient.get<BootstrapConfig>('/bootstrap')`.
Because the URL is `/api`-relative only after interceptor rewrite (the literal
is `/bootstrap`, not `/api/bootstrap`), whether `apiHeadersInterceptor`'s
marketplace headers (`X-Region`, `X-Language`, `Currency`, `WebSessionID`)
attach depends on `ApiConfigService.isApiRequest()` matching the resolved URL.
In practice the backend must treat all of these as **optional** on the
bootstrap call — none are load-bearing for it, and the document is expected to
resolve from tenant/origin alone (§1.9).
> **Requires backend decision:** whether the bootstrap endpoint should honor
> an `X-Language`/`Accept-Language` hint to pre-select a localization, or
> always return the full multi-locale document (the frontend today always
> receives and holds the full multi-locale document and selects locale
> client-side).
### 1.2 Which provider actually fires
`CONFIG_PROVIDER` (`src/app/core/config/config-provider.token.ts`) is a factory
bound by `RuntimeProviderStrategyService.getBootstrapProviderMode()`:
- Returns **mock** (`MockBootstrapProvider` → static asset) when
`environment.useMockData === true`, **or** when
`environment.useMockBootstrapOnLocal === true` **and** the host is localhost.
- Otherwise returns **api** (`ApiBootstrapProvider``GET /bootstrap`).
With the current `environment.ts` (`useMockData: false`,
`useMockBootstrapOnLocal: true`), a localhost dev session reads the static
`bootstrap.json`; a deployed tenant host reads the live `GET /bootstrap`.
### 1.3 Response shape — `BootstrapConfig`
Copied faithfully from
`src/app/shared/models/config/bootstrap-config.model.ts`:
```ts
export interface BootstrapConfig {
schemaVersion: string;
generatedAt: string;
tenant: TenantConfig;
branding: BrandingConfig;
theme: ThemeConfig;
company: CompanyConfig;
featureFlags: FeatureFlagsConfig;
features?: MarketplaceFeaturesConfig;
apiEndpoints: ApiEndpointsConfig;
localization: LocalizationConfig;
seo: SeoConfig;
permissions: PermissionsConfig;
header?: HeaderConfig;
catalog?: CatalogConfig;
layout?: PlatformLayoutConfig;
navigation: NavigationConfig;
footer?: FooterConfig;
productPage?: ProductPageConfig;
userExperience?: UserExperienceConfig;
pages: PageConfig[];
staticPages?: StaticPagesConfig;
widgetRegistry?: WidgetRegistryConfig;
}
```
Required (non-optional) top-level keys the backend must always emit:
`schemaVersion, generatedAt, tenant, branding, theme, company, featureFlags,
apiEndpoints, localization, seo, permissions, navigation, pages`. Everything
marked `?` may be omitted; the frontend supplies defaults (e.g.
`DEFAULT_CATALOG_CONFIG`, `DEFAULT_USER_EXPERIENCE_CONFIG`) when absent.
Each nested field, with its source model file under
`src/app/shared/models/config/`:
- **`schemaVersion`** (`string`) — document schema version (e.g. `"1.0.0"`).
Not enforced by a validator today; see §1.6.
- **`generatedAt`** (`string`, ISO 8601) — when the tenant's config snapshot
was produced. Advisory/diagnostic only today.
- **`tenant`** (`TenantConfig`, `tenant.model.ts`) —
`{ id (UUID), slug, code, host, name, websiteBaseUrl, builderBaseUrl,
backofficeBaseUrl, defaultLocale, supportedLocales[], defaultCurrency,
supportedCurrencies[], timezone }`. Identifies the tenant and its per-surface
base URLs, locale/currency sets, and timezone.
- **`branding`** (`BrandingConfig`, `branding.model.ts`) —
`{ brandName, legalName, slogan?, logoUrl, logoCompactUrl?, faviconUrl,
appIconUrl?, socialImageUrl?, galleryUrls?, supportEmail?, supportPhone? }`.
Drives header logo, favicon, PWA icon, social share image.
- **`theme`** (`ThemeConfig`, `theme.model.ts`) —
`{ themeId, mode: 'light'|'dark'|'system', palette (12 named colors:
primary/secondary/accent/success/warning/danger/info/textPrimary/
textSecondary/backgroundPrimary/backgroundSecondary/border), typography
(primaryFontFamily, headingFontFamily?, baseFontSize), spacing (unit, scale[]),
borderRadiusScale (Record<string,string>), shadows (Record<string,string>),
iconSet }`. Consumed by `ThemeEngineService`/`BrandingEngineService` to emit
CSS variables at runtime.
- **`company`** (`CompanyConfig`) — legal entity + address + contacts block
(see mock JSON §1.5 for shape: `companyName, registrationNumber, taxId,
address{country,region,city,street,postalCode}, contacts{email,phone,
telegram,website}`).
- **`featureFlags`** (`FeatureFlagsConfig`, `feature-flags.model.ts`) — a
`Record<string, boolean>` with well-known keys `wishlist, compare, reviews,
questions, comments, recommendations, blog, chat, analytics, notifications,
coupons, loyalty, giftCards, invoices` plus an open index signature for
arbitrary custom flags. Toggle features on/off per tenant.
- **`features`** (`MarketplaceFeaturesConfig`, optional) — richer per-feature
config beyond boolean flags.
- **`apiEndpoints`** (`ApiEndpointsConfig`, `api-endpoints.model.ts`) —
`{ bootstrap: ApiEndpointConfig, website: Record<string,ApiEndpointConfig>,
builder: Record<string,ApiEndpointConfig>, backoffice:
Record<string,ApiEndpointConfig> }` where `ApiEndpointConfig =
{ path, method, timeoutMs? }`. **This is where a tenant declares its planned
per-surface endpoint paths at runtime.** The `website/builder/backoffice`
maps are empty `{}` in the mock today — no builder/backoffice CRUD path
exists as a code literal anywhere (BACKEND-AUDIT §24); those endpoints, if
ever built, are declared here rather than hard-coded.
- **`localization`** (`LocalizationConfig`, `localization.model.ts`) —
`{ defaultLocale, supportedLocales[], currencyByLocale (Record<locale,
currency>), dictionaries: [{ locale, dictionaryUrl, version }] }`. Note the
three supported locales are `ru, en, hy`; the marketplace API uses codes
`RU, EN, AM` (mapped by `apiHeadersInterceptor` / `ApiService.normalizeLang`,
`am↔hy`).
- **`seo`** (`SeoConfig`, `seo.model.ts`) —
`{ default: SeoPageConfig, byPageKey: Record<pageKey, SeoPageConfig> }` where
`SeoPageConfig = { title, description, canonicalUrl?, robots?, metaTags? }`.
- **`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
collision.)
- **`header`** (`HeaderConfig`, optional) — header layout config.
- **`catalog`** (`CatalogConfig`, `catalog-config.model.ts`, optional) — the
storefront catalog behavior contract:
`{ layout, loadingStrategy: 'pagination'|'loadMore'|'infiniteScroll',
navigationMode, defaultSort, availableSorts[], enabledFilters[],
showBreadcrumbs, showCategoryBanner, showSubcategoryChips, showRatings,
showDiscounts, showAvailability, suggestionsEnabled, searchHistoryEnabled }`.
`defaultSort`/`availableSorts` enumerate `relevance | latest | price_asc |
price_desc | rating | popular | discount` (see §2.3). Defaults in
`DEFAULT_CATALOG_CONFIG`.
- **`layout`** (`PlatformLayoutConfig`, optional) — global layout type.
- **`navigation`** (`NavigationConfig`, `navigation.model.ts`) —
`{ header: NavigationItemConfig[], footer: NavigationItemConfig[] |
FooterNavigationGroupConfig[], sidebar?: NavigationItemConfig[] }`. Items
carry `{ id, labelKey?|label?, route?, icon?, order?, visible?,
visibleWhenFlags?, children? }` — note `visibleWhenFlags` lets a nav item be
gated on a `featureFlags` key.
- **`footer`** (`FooterConfig`, `footer-config.model.ts`, optional) —
`{ logoUrl?, paymentIcons?, copyrightText? (string|localized), columns?
(FooterColumnConfig[]), socialLinks? }`. `legalPageKeys`/`staticPageKeys` are
`@deprecated` in favor of `columns` but still resolve for older saved configs.
- **`productPage`** (`ProductPageConfig`, optional) — per-PDP config; in the
mock: `rating{enabled}, reviews{enabled,pageSize,showSummary},
questions{enabled,pageSize}, tabs{enabled,items[]}, relatedProducts{enabled}`.
Note `reviews.pageSize`/`questions.pageSize` (5 in mock) are the storefront
engagement page sizes (§2.2).
- **`userExperience`** (`UserExperienceConfig`, `user-experience-config.model.ts`,
optional) — `{ wishlist, compare{maxItems,...}, recentlyViewed{maxItems,...},
share, continueBrowsing, savedSearches{maxItems} }` limits/toggles for the
guest-first UX features. Defaults in `DEFAULT_USER_EXPERIENCE_CONFIG`.
- **`pages`** (`PageConfig[]`, `page.model.ts`) — the dynamic page tree the
renderer builds routes from: each `{ id, key, title, route{path,exact?},
layout, sections: SectionConfig[], seoKey?, featureFlag?, visible? }`. Each
section holds ordered `widgets` with `props` (see mock §1.5 for a full
hero/categories/product-collection example). This is the builder's output.
- **`staticPages`** (`StaticPagesConfig`, `static-page.model.ts`, optional) —
either `Record<slug, StaticPageConfig>` or a legacy array. Each page carries
localized `title` + `html`/`content`, `route`, `seo`, `status:
'draft'|'published'`, `enabled`, `visibility`, `updatedAt`, etc. Storefront
legal/content pages live here (about-us, privacy-policy, terms-of-service in
the mock). Content-management edits operate on this in-memory (BACKEND-AUDIT
§16) — no dedicated content backend.
- **`widgetRegistry`** (`WidgetRegistryConfig`, optional) —
`{ manifestUrl: string }`; the URL `WidgetManifestService` fetches the widget
manifest from (§2 "Widget manifest" endpoint).
### 1.4 Versioning
`BootstrapConfig.schemaVersion` (string) **does** exist and is the schema/version
field. Current usage:
- The mock document sets `"schemaVersion": "1.0.0"`.
- `bootstrap-diagnostics.validator.ts:44` surfaces it as a diagnostic entry
(`['schemaVersion', bootstrap.schemaVersion]`).
- `AdminDashboardFacade` (`admin-dashboard.facade.ts:110`) derives the
`dashboard.healthBootstrapValid` health check purely as
`healthy: !!current?.schemaVersion` — i.e. "bootstrap is valid" today means
only "a schemaVersion string is present," **not** any real semantic-version
compatibility check.
> **Requires backend decision:** whether `schemaVersion` should be enforced
> (frontend rejecting/ warning on an unknown major version). No such
> enforcement exists today — any string is accepted, and only its presence is
> checked.
### 1.5 Example JSON
Realistic example, taken from the actual mock document
`src/assets/mock/bootstrap/bootstrap.json` (abridged — pages/staticPages
trimmed for length; full versions in that file):
```json
{
"schemaVersion": "1.0.0",
"generatedAt": "2026-07-03T00:00:00Z",
"tenant": {
"id": "tenant-default-001",
"slug": "default",
"code": "DEFAULT",
"host": "default.local",
"name": "Marketplace",
"websiteBaseUrl": "https://marketplace.local",
"builderBaseUrl": "https://builder.marketplace.local",
"backofficeBaseUrl": "https://backoffice.marketplace.local",
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"defaultCurrency": "RUB",
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
"timezone": "Europe/Moscow"
},
"branding": {
"brandName": "Marketplace",
"legalName": "Marketplace LLC",
"slogan": "Digital commerce marketplace",
"logoUrl": "/icons/icon-192x192.png",
"faviconUrl": "/favicon.ico",
"supportEmail": "support@marketplace.local",
"supportPhone": "+7-900-000-00-00"
},
"theme": {
"themeId": "default-light",
"mode": "light",
"palette": {
"primary": "#497671", "secondary": "#a1b4b5", "accent": "#a7ceca",
"success": "#10b981", "warning": "#f59e0b", "danger": "#ef4444",
"info": "#3b82f6", "textPrimary": "#1e3c38", "textSecondary": "#667a77",
"backgroundPrimary": "#ffffff", "backgroundSecondary": "#f5f5f5",
"border": "#d3dad9"
},
"typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 },
"spacing": { "unit": 4, "scale": [0, 4, 8, 12, 16, 24, 32, 48] },
"borderRadiusScale": { "sm": "8px", "md": "12px", "lg": "16px", "xl": "22px" },
"shadows": { "sm": "0 2px 8px rgba(0,0,0,0.1)" },
"iconSet": "default"
},
"company": {
"companyName": "Marketplace LLC",
"registrationNumber": "1027700000000",
"taxId": "7700000000",
"address": { "country": "Russia", "region": "Moscow", "city": "Moscow", "street": "Tverskaya 1", "postalCode": "125009" },
"contacts": { "email": "support@marketplace.local", "phone": "+7-900-000-00-00", "telegram": "@marketplace_support", "website": "https://marketplace.local" }
},
"featureFlags": {
"wishlist": true, "compare": true, "reviews": true, "blog": false,
"chat": false, "analytics": true, "notifications": true, "coupons": true,
"loyalty": false, "giftCards": false, "invoices": true
},
"apiEndpoints": {
"bootstrap": { "path": "/bootstrap", "method": "GET", "timeoutMs": 10000 },
"website": {}, "builder": {}, "backoffice": {}
},
"localization": {
"defaultLocale": "ru",
"supportedLocales": ["ru", "en", "hy"],
"currencyByLocale": { "ru": "RUB", "en": "USD", "hy": "AMD" },
"dictionaries": [
{ "locale": "ru", "dictionaryUrl": "/assets/i18n/ru.json", "version": "1.0.0" },
{ "locale": "en", "dictionaryUrl": "/assets/i18n/en.json", "version": "1.0.0" },
{ "locale": "hy", "dictionaryUrl": "/assets/i18n/hy.json", "version": "1.0.0" }
]
},
"seo": {
"default": { "title": "Marketplace", "description": "Digital commerce marketplace", "robots": "index,follow" },
"byPageKey": { "home": { "title": "Marketplace - Home", "canonicalUrl": "https://marketplace.local/", "robots": "index,follow" } }
},
"permissions": {
"definitions": [
{ "key": "builder.pages.edit", "description": "Edit pages in builder" },
{ "key": "backoffice.products.read", "description": "Read products in backoffice" }
],
"roles": [
{ "role": "builder_admin", "permissions": ["builder.pages.edit"] },
{ "role": "backoffice_manager", "permissions": ["backoffice.products.read"] }
]
},
"catalog": {
"layout": "grid", "navigationMode": "default", "defaultSort": "relevance",
"availableSorts": ["relevance", "latest", "price_asc", "price_desc", "rating", "popular", "discount"],
"enabledFilters": ["price", "availability", "rating", "brand", "category", "subcategory", "discount", "new", "color", "size", "attributes"],
"showBreadcrumbs": true, "showRatings": true, "showDiscounts": true, "showAvailability": true
},
"productPage": {
"rating": { "enabled": true },
"reviews": { "enabled": true, "pageSize": 5, "showSummary": true },
"questions": { "enabled": true, "pageSize": 5 },
"relatedProducts": { "enabled": true }
},
"navigation": {
"header": [
{ "id": "nav-home", "labelKey": "nav.home", "route": "/", "icon": "home", "order": 1 },
{ "id": "nav-search", "labelKey": "nav.search", "route": "/search", "icon": "search", "order": 2 }
],
"footer": [
{ "id": "footer-about", "labelKey": "nav.about", "route": "/about-us", "order": 1 }
]
},
"footer": {
"copyrightText": { "ru": "© 2026 Marketplace. Все права защищены.", "en": "© 2026 Marketplace. All rights reserved." },
"legalPageKeys": ["about-us", "privacy-policy", "terms-of-service"]
},
"widgetRegistry": { "manifestUrl": "/assets/mock/bootstrap/widget-manifest.json" },
"staticPages": {
"about-us": {
"route": "/about-us",
"title": { "ru": "О компании", "en": "About Us", "hy": "Մեր մասին" },
"html": { "ru": "<h2>О компании</h2><p>…</p>", "en": "<h2>About Us</h2><p>…</p>" }
}
},
"pages": [
{
"id": "page-home", "key": "home", "title": "Home",
"route": { "path": "/", "exact": true },
"layout": { "type": "default" }, "seoKey": "home", "visible": true,
"sections": [
{
"id": "section-hero", "type": "hero", "order": 1,
"layout": { "strategy": "hero", "columns": 1, "gap": "1.5rem", "align": "stretch" },
"visible": true,
"widgets": [
{
"id": "widget-hero-main", "type": "hero", "version": "1.0.0", "order": 1,
"props": {
"title": { "ru": "Добро пожаловать на Маркетплейс", "en": "Welcome to Marketplace Platform" },
"ctaLabel": { "ru": "Начать покупки", "en": "Start Shopping" }
}
}
]
}
]
}
]
}
```
### 1.6 Lifecycle
- 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
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()`
at construction: `UiRuntimeFacade` (`ui-runtime.facade.ts:32`),
`PlatformRuntimeService`, `ThemeEngineService`, `BrandingEngineService`, the
`FooterComponent`, etc. Because `ConfigService` de-dupes (§1.7), the first
one triggers the single HTTP call and the rest share it.
- The `languageGuard` (`src/app/guards/language.guard.ts`, wrapping every
`:lang` route) runs on navigation and preloads the locale dictionary; it is
**not** itself the bootstrap fetcher, but it runs in the same startup window.
Bootstrap and language resolution are effectively concurrent — the app does
not gate rendering on bootstrap completion, so components must tolerate a
transient null snapshot.
### 1.7 Caching behavior (what the frontend actually does)
`ConfigService` (`src/app/core/config/config.service.ts`) is the cache, and it
is **in-memory only** — no `localStorage`, no HTTP cache header logic:
- Holds `bootstrapSnapshot: BootstrapConfig | null` (last successful value) and
a shared `bootstrap$` observable with `shareReplay(1)`.
- `loadBootstrap(forceRefresh = false)`: returns the cached `bootstrap$` when a
snapshot exists and `forceRefresh` is false; otherwise (re)issues the
provider call.
- On error, it clears both `bootstrap$` and `bootstrapSnapshot` so the next
call retries cleanly (`catchError` rethrows).
- A `bootstrapRevision` signal increments on every successful load/override —
consumers use it to react to config changes.
- `getBootstrapSnapshot()` returns the current in-memory value synchronously
(used by `WidgetManifestService`, diagnostics, etc.).
- The cache does **not** survive a page reload — every full page load re-fetches
bootstrap. There is also a separate client-side `cacheInterceptor`
(GET-response caching, BACKEND-AUDIT §3) that may cache the `/bootstrap`
response at the HTTP layer within a session, but nothing persists it across
sessions.
### 1.8 ETag / conditional requests
**Requires backend decision — no ETag / conditional-request logic exists in
the frontend today.** The bootstrap provider issues a plain unconditional
`GET`; nothing reads or sends `ETag`, `If-None-Match`, `Last-Modified`, or
`If-Modified-Since`. If the backend wants conditional revalidation of the
(large, per-tenant, infrequently changing) bootstrap document, both the wire
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
`src/app/core/config/tenant-resolver.service.ts`):
- `TenantResolverService.getTenantKey()`: localhost →
`environment.fallbackTenantKey ?? 'default'`; otherwise the **first DNS
label** of the hostname, skipping a leading `www` (e.g.
`dexarmarket.api.dexarmarket.ru``dexarmarket`; `www.acme.com``acme`).
- That tenant key feeds `ApiConfigService.getBaseUrl()`, which picks the
marketplace API base: localhost → `environment.localhostApiUrl` (`/api`);
else `environment.tenantApiBaseUrls[tenantKey]`; else
`environment.tenantApiTemplate` with `{tenant}` substituted; else (only when
`allowBootstrapApiOverride` is true — off by default) a value read out of the
already-loaded bootstrap (`apiEndpoints.website.baseUrl` /
`tenant.apiBaseUrl`).
- **No `X-Tenant` header or `/tenant/{id}` path prefix is ever sent.** Tenant
isolation for `GET /bootstrap` is achieved purely by *which origin/subdomain
the request goes to* — the backend must infer the tenant from the request
host, and return that tenant's bootstrap document.
### 1.10 Draft vs Published
The storefront and the builder read the **same** in-memory `BootstrapConfig`;
there is no separate "draft bootstrap" vs "live bootstrap" endpoint today
(BACKEND-AUDIT §17):
- The builder (`ProjectEditorFacade`,
`src/app/features/project-editor/facade/project-editor.facade.ts`) loads the
bootstrap via `ConfigService.loadBootstrap(true)` (force refresh), edits an
in-memory `History<BootstrapConfig>` (undo/redo), and persists **drafts to
localStorage** through `ProjectEditorDraftStorageService`.
- `ConfigService.applyBootstrapOverride(next)` swaps the in-memory snapshot
(deep-cloned) so a preview reflects edits immediately — but this is purely
client-side.
- **There is no publish/write HTTP call.** No `PUT /bootstrap`, no
builder-write endpoint exists anywhere in code. "Publishing a marketplace"
(writing the edited bootstrap back so the live storefront serves it) is
**FUTURE / LOCAL-ONLY today**. Any builder publish endpoint would be declared
under `apiEndpoints.builder` (runtime-declared) and is a backend build item,
not something the frontend currently calls.
> **Requires backend decision:** the entire draft→publish write path
> (endpoint, method, optimistic-concurrency / version check on save, and
> whether draft state is server-persisted or stays localStorage-only).
### 1.11 Fallback behavior (bootstrap fails to load)
- `ConfigService` does **not** substitute a default document on failure — it
clears its cache and rethrows, so each subscribing consumer sees the error.
- There is no global "bootstrap failed" full-screen error page. Instead,
individual engines degrade: `ThemeEngineService`/`BrandingEngineService`
subscribe with `take(1)` and simply do nothing extra on error (the app keeps
its compiled default styles); optional config getters fall back to hard-coded
defaults (`DEFAULT_CATALOG_CONFIG`, `DEFAULT_USER_EXPERIENCE_CONFIG`);
`WidgetManifestService` falls back to the static manifest URL on any bootstrap
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.
> **Requires backend decision / frontend follow-up:** whether a hard bootstrap
> failure should present a dedicated tenant-level error screen. Today the
> failure is silent-ish (default theme, empty dynamic pages) rather than a
> blocking error, which is a UX gap if the backend can actually return a fatal
> bootstrap error.
### 1.12 Failure responses
The frontend does not parse a bootstrap-specific error body — a failed
`GET /bootstrap` surfaces as a bare `HttpErrorResponse` handled per §1.11. For
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
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
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
hit it. Again, no maintenance concept exists frontend-side today.
---
## 2. Endpoint Documentation Framework
This section defines the **general contract every endpoint in the system
follows**, so the reader has the framework before the per-domain endpoint
catalog (a separate section). Where the frontend does not define something,
it is marked **"Requires backend decision"** rather than invented.
### 2.1 Standard success response envelope
**There is no `ApiResponse<T>` / `{ success, data, error }` envelope in this
codebase.** Every live caller types its `HttpClient` call to the **bare payload
type** and consumes the raw JSON directly:
- `ApiService.getItem()``HttpClient.get<Item>(...)` (the item object itself).
- `ApiService.getCategories()``get<Category[]>` (bare array).
- `ApiBootstrapProvider``get<BootstrapConfig>` (bare object).
- `TelegramSessionApiService``get<Record<string, unknown>>` then normalizes.
The generic `ApiResponse<T>` shape in the org coding-standards is **not** used
here — do not assume an envelope. Success responses are the raw resource
(object, array, or `{ items, total }` for lists — see §2.2).
> **Requires backend decision:** whether to *introduce* a success envelope
> going forward. If adopted it would be a breaking change to every existing
> live caller listed in §2.6 and BACKEND-AUDIT §4, each of which currently
> expects the bare payload. Recommendation: keep bare payloads for the existing
> live endpoints; only new endpoints could opt into an envelope, and even then
> the frontend has no envelope-unwrapping layer today (one would be new work).
### 2.2 Standard pagination contract
Two distinct pagination shapes already exist in the frontend; a backend should
align to these rather than invent a third:
**A) Offset/count style (marketplace list + search endpoints)** — used by the
live `ApiService`:
- Request query params: **`count`** (page size, default 50) and **`skip`**
(offset, default 0). Applied on `GET /category/{id}`, `GET /searchitems`,
`GET /items/randomitems`.
- Response for `searchItems`: `{ items: Item[], total: number }`. Other list
endpoints (`getCategoryItems`, `getRandomItems`) return a **bare array** with
no total.
**B) Page/pageSize style (admin lists, storefront engagement lists, media)** —
used by facade/gateway list results:
- Request: `{ page: number, pageSize: number }` (plus filters — §2.4). See
`AdminOrderListFilters`, `EngagementListQuery`, `MediaListParams`.
- Response: `{ items: T[], total: number, page: number, pageSize: number }`.
Canonical shapes: `EngagementListResult<T>`
(`core/products/models/product-engagement.model.ts:55`), `AdminOrdersListResult`
(`features/admin/orders/models/admin-order.model.ts:63`), `MediaListResult`
(`{ items, total }` only). The UI derives `totalPages = ceil(total /
pageSize)` client-side (e.g. `pagination.component.ts`, admin list pages).
Notes / open items:
- The two styles use different param names (`count`/`skip` vs `page`/`pageSize`)
— a backend serving both storefront and admin must support both, or a
reconciliation decision is needed.
- **No cursor/keyset pagination anywhere.** The only `cursor` matches in source
are CSS `cursor:` properties. There is no `nextCursor`/`hasMore` on the wire
(`catalog-state.model.ts` has a client-derived `hasMore` for infinite-scroll,
computed as `skip + pageSize < total`, not a backend field).
> **Requires backend decision:** exact page-size **limits/maximums** (the
> frontend sends 50 as a default `count` but enforces no server-side cap),
> whether to standardize on offset vs page-number, and whether large lists
> should move to cursor pagination (nothing in the frontend implies or requires
> cursors today).
### 2.3 Standard sorting contract
Sorting is **enumerated in the bootstrap `catalog` config**, not free-form:
- `CatalogConfig.availableSorts` / `CatalogConfig.defaultSort` (see §1.3,
`catalog-config.model.ts`) enumerate the allowed values:
`relevance | latest | price_asc | price_desc | rating | popular | discount`.
- The live wire param is `sort` on `GET /searchitems`
(`ApiService.searchItems`), whose accepted set is a subset:
`relevance | price_asc | price_desc | popular | rating` (the code's
`options.sort` union). `latest`/`discount` are catalog-config values without a
confirmed search-endpoint mapping.
- Admin lists sort primarily via their filter objects / client-side; there is
no shared admin `sort` query-param convention on the wire yet (admin CRUD is
mock-only, BACKEND-AUDIT §14).
> **Requires backend decision:** reconcile the catalog-config sort vocabulary
> (7 values) with the search-endpoint `sort` vocabulary (5 values) — and define
> the wire encoding for admin list sorting (field + direction) when those
> endpoints are built.
### 2.4 Standard filtering contract
Filtering shapes already used by the frontend:
- **Storefront search/catalog** — `GET /searchitems` accepts optional
`categoryIDs` (comma-joined ints), `minPrice`, `maxPrice`, `tag`, plus `sort`
(§2.3). `CatalogConfig.enabledFilters` declares which filter UIs a tenant
exposes (`price, availability, rating, brand, category, subcategory, discount,
new, color, size, attributes`).
- **Admin lists** — each domain has a filters object, all following a
`{ search: string, <field>: 'all' | <enum>, page, pageSize }` shape. Examples:
- `AdminOrderListFilters` = `{ search, status: 'all'|AdminOrderStatus, page,
pageSize }`.
- `AdminMonitoringEventFilters` = `{ category: 'all'|AdminMonitoringCategory,
search }` (note: no page/pageSize on this one).
- `MediaListParams` = `{ page?, pageSize?, search?, folder?, tag?, kind?,
sort? }`.
The convention: a text `search` string plus enum facets where the sentinel
**`'all'`** means "no filter on this facet."
> **Requires backend decision:** the wire encoding of these filters as query
> params for the (not-yet-built) admin endpoints — the shapes above are
> in-memory facade filter objects, not confirmed query-string contracts. Also
> whether `'all'` is sent literally or omitted to mean unfiltered.
### 2.5 Standard search contract
- Live full-text search: `GET /searchitems?search=<q>&count=&skip=[&categoryIDs
&minPrice&maxPrice&tag&sort]` → `{ items: Item[], total: number }`
(`ApiService.searchItems`). The query param is **`search`**.
- The `SearchFacade` (BACKEND-AUDIT §18) is a **client-side orchestration** over
`ProductFacade`/`CategoryFacade` — history, trending, autocomplete, and cache
are all localStorage/in-memory. There is **no dedicated search backend
endpoint** beyond `/searchitems`; autocomplete/suggestions are derived
client-side today.
> **Requires backend decision:** whether server-side autocomplete/suggestion
> and trending endpoints are wanted (nothing on the wire today), and the
> min-query-length / debounce contract if so.
### 2.6 Nullable-field convention
The frontend does **not** ascribe distinct semantics to `null` vs `undefined`
vs an omitted key on the wire — it treats all three as "absent" and applies
defaults defensively:
- Model interfaces mark optional fields with `?` (implying may be omitted);
a handful use explicit `T | null` (e.g. `AuthSession.userId: number | null`,
`AuthSession.username: string | null` where `null` specifically means "known
to be absent").
- The tolerant normalizers (`ApiService.normalizeItem/normalizeCategory`,
`TelegramSessionApiService.normalizeWebSession`) coalesce `null`/`undefined`/
missing uniformly via `??` and default fallbacks — they never branch on
`null` vs `undefined`.
- **Convention to follow:** the backend may omit an optional key or send `null`
interchangeably; both are handled. Explicit `null` is only meaningful where a
model types the field as `… | null` to signal "definitively no value" (auth
user id/username being the notable examples).
### 2.7 JWT / authentication header format
Two coexisting mechanisms; full spec in **`docs/AUTHENTICATION.md`** (this
section only references it, does not redefine):
- **Telegram session auth (LIVE)** — no `Authorization: Bearer`. Identity is a
**`WebSessionID`** header (auth session id, or a persisted anonymous 32-hex id
from `localStorage['web_session_id']`) attached to marketplace API requests by
`apiHeadersInterceptor`. Admin requests additionally carry an
**`AdminWebSessionID`** header (`adminAuthHeadersInterceptor`, gating URL
segments `/admin/`, `/backoffice/`, `/builder/`, `/media/`).
- **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
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,
rotation, expiry, role hierarchy, and tenant-scoping open items.
### 2.8 Live endpoint domains (real HTTP, already implemented)
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.)
#### 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.
| Endpoint | Method | Auth | Body / Headers | Response |
|---|---|---|---|---|
| `/users/sessions` | POST | none | body `{ webSessionID }` (client-generated GUID) + header `WebSessionID: <same guid>` | `{ webSessionID, url }` — normalized to `WebSessionStart`; `url` = Telegram deep link |
| `/users/sessions/{id}` | GET | none | — | Session object, field-tolerant → normalized `AuthSession` |
| `/users/sessions/{id}` | DELETE | none | header `WebSessionID: <id>` | ignored (frontend clears local state regardless) |
TypeScript interfaces (`src/app/models/auth.model.ts`):
```ts
export interface AuthSession {
sessionId: string;
userId: number | null;
username: string | null;
displayName: string;
active: boolean;
expires: string; // ISO 8601
}
export interface WebSessionStart {
webSessionID: string;
url: string;
}
```
Example — create session:
```http
POST https://api.dexarmarket.ru:445/users/sessions
WebSessionID: 3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11
Content-Type: application/json
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
```
```json
{
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
"url": "https://t.me/myAMLKYCBOT?start=3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11"
}
```
Example — poll session (active):
```http
GET https://api.dexarmarket.ru:445/users/sessions/3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11
```
```json
{
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
"status": "active",
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
"expiresAt": "2026-07-26T05:00:00Z"
}
```
The frontend reads fields **field-tolerantly** (accepts many casings/aliases —
see `normalizeWebSession`, and `docs/AUTHENTICATION.md` §1.3 for the full alias
priority lists). A backend should send a real `expiresAt`/`expires` (else the
frontend fabricates `now + 3600s`).
#### 2.8.2 Marketplace / storefront API (LIVE)
Base: `ApiConfigService.getBaseUrl()` (tenant-resolved; `/api` on localhost,
else `https://{tenant}.api.dexarmarket.ru:445` / `https://api.dexarmarket.ru:445`).
Source: `src/app/services/api.service.ts` (`ApiService`). Headers attached by
`apiHeadersInterceptor`: `X-Region`, `X-Language` (RU/EN/AM), `Currency`
(default RUB), `WebSessionID`. The primary product wire DTO is `Item`
(`src/app/models/item.model.ts`), reconciled by the large tolerant
`normalizeItem()` adapter (BACKEND-AUDIT §7 — treat it as the tolerance
contract). This domain's full per-endpoint catalog belongs to the CRUD-domain
section; summarized here as the framework anchor:
| Endpoint | Method | Auth | Params / Body | Response |
|---|---|---|---|---|
| `/ping` | GET | session headers | — | `{ message }` |
| `/category` | GET | session headers | — | `Category[]` (normalized) |
| `/category/{id}` | GET | session headers | `count`, `skip` | `Item[]` |
| `/items/{id}` | GET | session headers | — | `Item` |
| `/searchitems` | GET | session headers | `search`, `count`, `skip`, `categoryIDs?`, `minPrice?`, `maxPrice?`, `tag?`, `sort?` | `{ items: Item[], total: number }` |
| `/items/randomitems` | GET | session headers | `count`, `category?` | `Item[]` (featured/random) |
| `/websession/{sessionId}` | POST | session headers | item array | cart echo |
| `/items/{id}/callback` | POST | session headers | `{ rating, comment, sessionID, timestamp }` | `{ message }` (review) |
| `/items/{id}/questiion` | POST | session headers | `{ question, sessionID, timestamp }` | `{ message }` (question — **literal typo `questiion` matches backend spec**) |
| `/purchase-email` | POST | session headers | `{ email, phone?, telegramUserId, items[] }` | `{ message }` |
#### 2.8.3 Cart / order / payment API (LIVE)
Order/cart-payment endpoints on the **marketplace base**; QR/card status on the
**payment base** `environment.qrApiUrl` (= `https://qr.vitanova.network/api`).
Source: `src/app/services/api.service.ts`. DTOs are inline in that file.
| Endpoint | Method | Base | Auth | Body | Response |
|---|---|---|---|---|---|
| `/cart` | POST | marketplace | session headers | `CartPaymentRequest` | `QrCreateResponse` |
| `/orders` | POST | marketplace | session headers | `CreateOrderRequest` | `CreateOrderResponse` (fire-and-forget after payment) |
| `/qr` | POST | `qrApiUrl` | headers `authorization-key`, `userid-value` | `QrCreateRequest` | `QrCreateResponse` |
| `/qr/dynamic/{partnerId}/{qrId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
| `/card/{partnerId}/{orderId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
Const `partnerId` = `web-97ec-9c57-4dde-9037-3a68f7f83750`
(`ApiService.cartPaymentPartnerId`). Key interfaces (copied from
`api.service.ts`):
```ts
export interface CartPaymentRequest {
amount: number;
currency: 'RUB';
siteuserID: string;
siteorderID: string;
redirectUrl: string;
telegramUsername: string;
paymentMethod: 'qr' | 'card';
qrDescription?: string;
customerID?: string;
items: Array<{ itemID: number; price: number; name: string; quantity?: number; delivery?: DeliveryOption[] }>;
}
export interface CreateOrderRequest {
items: Array<{ productId: string; name: string; quantity: number; price: number }>;
customer: { name: string; email: string; phone: string };
payment?: { method: string; currency: string };
shipping?: { address: string; method: string; trackingNumber: string };
}
export interface CreateOrderResponse {
id: string;
orderNumber: string;
status: string;
total: number;
currency: string;
}
export interface QrCreateResponse {
qrId?: string; qrID?: string; nspkID?: string; nspkId?: string;
nspkurl?: string; orderID?: string; url?: string; bankUrl?: string;
status?: string; qrStatus?: string; qrExpirationDate?: string; qrTTL?: number;
payload?: string; Payload?: string; qrUrl?: string;
partnerqrID?: string | number; partnerID?: string | number;
partnerId?: string | number; PartnerID?: string | number;
}
export interface QrDynamicStatusResponse {
additionalInfo: string; paymentPurpose: string; amount: number; code: string;
createDate: string; currency: string; order: string; status: string;
qrId: string; transactionDate: string; transactionId: number; qrExpirationDate: string;
}
```
The `QrCreateResponse` is deliberately alias-tolerant (many casings for id /
url / partner fields) — the frontend resolves the effective id/link via
`resolvePaymentQrId()` / `resolvePaymentLink()` / `resolveBankPaymentUrl()`. A
backend can pick one canonical casing; the frontend will still read it.
Example — create cart payment:
```http
POST https://api.dexarmarket.ru:445/cart
WebSessionID: 3f1c2a0e-…
Content-Type: application/json
{
"amount": 4990,
"currency": "RUB",
"siteuserID": "8823771",
"siteorderID": "order-2026-0007",
"redirectUrl": "https://marketplace.local/checkout/done",
"telegramUsername": "buyer_ivan",
"paymentMethod": "qr",
"items": [{ "itemID": 101, "price": 4990, "name": "Wireless Keyboard", "quantity": 1 }]
}
```
```json
{ "qrId": "QR-77f0", "nspkurl": "https://qr.nspk.ru/AD10…", "status": "created", "qrExpirationDate": "2026-07-26T04:10:00Z" }
```
#### 2.8.4 Widget manifest (LIVE)
Source: `src/app/widgets/registry/widget-manifest.service.ts`. URL comes from
`bootstrap.widgetRegistry.manifestUrl`, falling back to
`/assets/mock/bootstrap/widget-manifest.json`.
| Endpoint | Method | Auth | Response |
|---|---|---|---|
| `<bootstrap.widgetRegistry.manifestUrl>` | GET | none | `WidgetManifestFile` |
On any error it falls back to `of({ widgets: [] })` (never throws to the UI).
Interface (`src/app/widgets/contracts/widget-manifest.contract.ts`):
```ts
export interface WidgetManifestFile {
widgets: WidgetManifestEntry[];
}
export interface WidgetManifestEntry {
type: string;
version: string;
componentKey: string;
supportedLayouts: WidgetLayoutSupport[]; // 'stack'|'grid'|'hero'|'carousel'|'split'
supportedDataSources: WidgetDataSourceName[]; // 'featured'|'latest'|'category'|'manual'|'related'|'future'|'root'|'parent'
settingsSchema: WidgetSettingsSchema; // { type: 'object', properties, required? }
defaultSettings: Record<string, unknown>;
metadataSupport?: WidgetMetadataSupport;
enabled?: boolean;
}
```
Example response:
```json
{
"widgets": [
{
"type": "hero",
"version": "1.0.0",
"componentKey": "HeroWidgetComponent",
"supportedLayouts": ["hero"],
"supportedDataSources": ["manual"],
"settingsSchema": { "type": "object", "properties": { "title": { "type": "string" } } },
"defaultSettings": { "title": "Welcome" },
"enabled": true
}
]
}
```
#### 2.8.5 Regions / location (LIVE)
Source: `src/app/services/location.service.ts`.
| Endpoint | Method | Base | Auth | Response |
|---|---|---|---|---|
| `/regions` | GET | marketplace | session headers | `Region[]` |
On error, falls back **silently** to 6 hardcoded regions (moscow, spb, yerevan,
minsk, almaty, tbilisi) — no user-visible error. The selected region's `id`
feeds the `X-Region` header on subsequent marketplace calls. (An external
geo-IP call `GET http://ip-api.com/json/...` is used for auto-detect — external,
not a platform backend.) Interface (`src/app/models/location.model.ts`):
```ts
export interface Region {
id: string;
city: string;
country: string;
countryCode: string;
timezone?: string;
}
```
Example response:
```json
[
{ "id": "moscow", "city": "Москва", "country": "Россия", "countryCode": "RU", "timezone": "Europe/Moscow" },
{ "id": "yerevan", "city": "Ереван", "country": "Армения", "countryCode": "AM", "timezone": "Asia/Yerevan" }
]
```
### 2.9 Consolidated "Requires backend decision" items (this section)
- Bootstrap: `X-Language`/`Accept-Language` handling on `GET /bootstrap` (§1.1).
- Bootstrap: whether `schemaVersion` is enforced vs presence-only (§1.4).
- Bootstrap: ETag / conditional-request strategy — none exists today (§1.8).
- Bootstrap: the entire draft→publish write path + save-time version/concurrency
check — no write endpoint exists (§1.10).
- Bootstrap: dedicated fatal-bootstrap-failure UX vs silent degradation (§1.11).
- Framework: whether to introduce a success envelope (breaking for all live
callers) — none exists today (§2.1).
- Framework: pagination page-size limits/max; offset vs page-number
standardization; cursor vs offset (nothing implies cursors) (§2.2).
- Framework: reconcile catalog-config sort vocabulary (7) vs search-endpoint
`sort` vocabulary (5); wire encoding for admin list sorting (§2.3).
- Framework: query-string encoding for admin list filters and the `'all'`
sentinel (§2.4).
- Framework: server-side autocomplete/trending search endpoints + min-query
contract (§2.5).
---
## 3. CRUD Contracts
Exhaustive per-domain, per-endpoint contract for every backend touch-point the
Angular frontend expects. Derived from source on branch `B2B` and cross-checked
against `docs/context/BACKEND-AUDIT.md` (the this-session audit — the ground
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
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
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
endpoint and *why*, but the wire shape of the error body is owned by that doc.
### 3.0 Conventions used in this section
**Two API base URLs are in play** (`BACKEND-AUDIT.md` §2):
- **Marketplace / tenant API** — `ApiConfigService.getBaseUrl()`
(`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
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.
**LIVE vs PROPOSED paths.** Only the storefront catalog/cart/engagement reads and
the single `AdminCategoriesApiGateway` have **literal HTTP paths in code**. Every
other admin CRUD path below is **PROPOSED** — no URL literal exists anywhere in
the frontend for it (`BACKEND-AUDIT.md` §24: "No literal `/admin/*`, `/builder/*`,
or per-admin-domain backoffice CRUD paths exist in code"). The admin gateways are
in-memory `*LocalGateway` classes that never construct a URL. The proposals here
follow the **one real precedent** in the codebase — `AdminCategoriesApiGateway`
(`src/app/features/admin/categories/services/admin-categories-api.gateway.ts`),
which uses:
```
GET {base}/backoffice/categories ?search&visibility&includeDeleted
GET {base}/backoffice/categories/{id}
POST {base}/backoffice/categories (body = category minus server-owned fields)
PUT {base}/backoffice/categories/{id}
DELETE {base}/backoffice/categories/{id} (soft delete)
POST {base}/backoffice/categories/{id}/restore
GET {base}/backoffice/categories/slug-taken ?slug&excludingId
```
All other admin paths below are proposed by analogy to this shape (`{base}/backoffice/<domain>`).
Builder/config domains (nav, footer, branding, languages, CMS, homepage, widgets)
are proposed under `{base}/builder/...` because the bootstrap document declares a
`apiEndpoints.builder` record for exactly this, but **no builder write call exists
in code today** — the entire builder/CMS surface is LOCAL-ONLY (in-memory
bootstrap + localStorage drafts), so those endpoints are marked **PROPOSED /
FUTURE** throughout.
**Headers (all requests).** Marketplace API requests carry `X-Region`,
`X-Language` (`RU|EN|AM`), `Currency` (default `RUB`), and `WebSessionID`
(`apiHeadersInterceptor`, `BACKEND-AUDIT.md` §3). Requests whose URL contains
`/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.
**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
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).
**Required permission / role.** The frontend's only permission model is the
coarse `ROLE_PERMISSIONS` table (`AUTHENTICATION.md` §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).
**Error responses.** Unless a domain-specific note says otherwise, every endpoint
below can return the envelope from `ERROR_CONTRACT.md` 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") —
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
below is the *contract the backend should honor*, not behavior the current UI
distinguishes.
**Validation rules.** Admin create/edit forms
(`admin-product-form.component.ts`, `admin-category-form.component.ts`) use **no
Angular reactive `Validators`** — confirmed by source search (zero
`Validators.*` matches). Form state is built by factories
(`admin-products-form.factory.ts`, `admin-categories-form.factory.ts`) that seed
empty defaults (e.g. `slug: ''`). Therefore **the frontend enforces essentially
no field validation** on admin CRUD; every validation rule below beyond "the
field exists in the model / is non-nullable in TS" is **Requires backend
decision**. The one real client-side validation convention lives in the builder
(`ProjectValidator``ProjectEditorFacade.fieldError()`), which is client-only
and localStorage-scoped, not backend-fed.
**Pagination.** The admin list models that paginate use a uniform shape:
`filters { …, page: number, pageSize: number }` in, `{ items[], total, page,
pageSize }` out (products, orders, transactions, reviews). Categories, users,
monitoring, customers, analytics return **plain arrays with no pagination**
noted per domain.
---
### 3.1 Products
Two distinct surfaces: **storefront catalog read** (LIVE) and **admin CRUD**
(MOCK-ONLY, no seam — `BACKEND-AUDIT.md` §14).
#### 3.1.a Storefront catalog read — LIVE
Backed by `ApiService` (`src/app/services/api.service.ts`) via
`ApiProductDataProvider``PRODUCT_DATA_PROVIDER``ProductFacade`. The wire
DTO is `Item` (`src/app/models/item.model.ts`); `ApiService.normalizeItem()` is
the tolerance contract (the single largest inline mapper — a backend engineer
should treat it as authoritative for accepted field name variants; see
`BACKEND-AUDIT.md` §7).
| # | Method | Path (LIVE literal) | Purpose | Success |
|---|---|---|---|---|
| P1 | GET | `{base}/items/{itemID}` | Single product | `200` single `Item` (raw wire shape) |
| P2 | GET | `{base}/category/{categoryID}?count&skip` | Products in a category | `200` `Item[]` |
| P3 | GET | `{base}/searchitems?search&count&skip[&categoryIDs&minPrice&maxPrice&tag&sort]` | Search | `200` `{ items: Item[], total: number }` |
| P4 | GET | `{base}/items/randomitems?count[&category]` | Featured/random | `200` `Item[]` |
- **Headers/JWT:** marketplace headers (§3.0). No JWT; anonymous `WebSessionID`
is sufficient. Public read.
- **Query params (P2/P3/P4):** `count` (default 50), `skip` (default 0, offset
pagination). P3 search: `search` (string, required), `categoryIDs`
(comma-joined numeric ids), `minPrice`, `maxPrice` (numbers), `tag` (string),
`sort``relevance | price_asc | price_desc | popular | rating`. Signature:
`ApiService.searchItems()` lines 551-590.
- **Pagination/sorting/filtering:** offset-based (`count`/`skip`); sort + price
+ category + tag filters only on P3. P2 has no sort/filter params. Response P3
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
empty-state. `500/503` generic. Calls retry x2 with exponential backoff
(`ApiService.retryConfig`).
- **Validation:** none (read).
- **Example P3 request:** `GET {base}/searchitems?search=phone&count=24&skip=0&categoryIDs=3,7&minPrice=1000&sort=price_asc`
- **Example P3 response:**
```json
{ "items": [ { "id": "1024", "name": "Phone X", "price": 62560, "currency": "RUB", "imgs": ["https://…/x.webp"], "remaining": 12 } ], "total": 87 }
```
**`Item` variants/options (part of P1 payload, not separate endpoints).**
`normalizeItem` accepts `variantOptions[]` (`{ key, label, labels?, options: [{
value, label?, labels?, available? }] }`) and `specificationGroups[]`. The admin
side (§3.1.b) models variants as a flat priced list (`AdminProductVariant`). There
is **no dedicated variants/options CRUD endpoint** — variants are created/updated
as part of the product create/update body. Flagged: variant shape differs between
storefront (`variantOptions` grouped) and admin (`variants` flat priced rows) —
reconciliation is **Requires backend decision** (adapter lives in
`admin-product-form.factory.ts variantsToBackendRows/variantsFromBackendRows`).
#### 3.1.b Admin products CRUD — PROPOSED (MOCK-ONLY today, no DI seam)
Contract: `AdminProductsGateway`
(`src/app/features/admin/products/services/admin-products-gateway.interface.ts`).
Impl today: `AdminProductsLocalGateway` (in-memory + localStorage), injected
**concretely** by `AdminProductsFacade` — no token, so a backend requires
introducing a DI seam first (`BACKEND-AUDIT.md` §14, key finding). DTO:
`AdminProduct` (`src/app/features/admin/products/models/admin-product.model.ts`,
41-field interface incl. `media`, `variants`, `variantAttributes`, `specifications`,
`attributes`, `translations`, `seo`, `badges`, flags, `reviews`, `questions`).
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| AP1 | GET | `{base}/backoffice/products?search&categoryId&visibility&stock&includeArchived&sort&page&pageSize` | `loadProducts(filters)` | `200 AdminProductsListResult` |
| AP2 | GET | `{base}/backoffice/products/{id}` | `loadProduct(id)` | `200 AdminProduct` / `404` |
| AP3 | GET | `{base}/backoffice/products/categories` | `loadCategories()` | `200 AdminProductCategoryOption[]` (`{id,title}`) |
| AP4 | POST | `{base}/backoffice/products` | `createProduct(product)` | `201 AdminProduct` |
| AP5 | PUT | `{base}/backoffice/products/{id}` | `updateProduct(product)` | `200 AdminProduct` |
| AP6 | DELETE | `{base}/backoffice/products/{id}` | `deleteProduct(id)` | `204` |
| AP7 | POST | `{base}/backoffice/products/{id}/duplicate` | `duplicateProduct(id)` | `201 AdminProduct` / `404` |
| AP8 | POST | `{base}/backoffice/products/{id}/archive` | `archiveProduct(id)` | `204` |
| AP9 | POST | `{base}/backoffice/products/{id}/restore` | `restoreProduct(id)` | `200 AdminProduct` / `404` |
- **Headers/JWT:** admin auth (§3.0). Today `AdminWebSessionID`; target Bearer JWT.
- **Permission (Proposed):** read (AP1-AP3) → `backoffice.read`; writes
(AP4-AP9) → `backoffice.write`. Finer than that = Requires backend decision.
- **AP1 query params** (from `AdminProductListFilters`): `search` (string),
`categoryId` (string|null), `visibility``all|visible|hidden`, `stock`
`all|in_stock|low_stock|out_of_stock`, `includeArchived` (bool), `sort`
`title|price|priority|stock|updated`, `page` (number), `pageSize` (number).
This is the full pagination + sort + filter + search surface — no other
sort keys exist. Response `AdminProductsListResult { items, total, page, pageSize }`.
- **AP4/AP5 request body:** an `AdminProduct`. By the category precedent
(`AdminCategoriesApiGateway.createCategory` strips `id, itemsCount, deletedAt,
createdAt, updatedAt`), the create body should **omit server-owned fields**
(`id`, `createdAt`, `updatedAt`; also `reviews`/`questions` which are derived).
Update sends the full object with `id` in the path. **Requires backend
decision:** exact server-owned field list for products (the frontend local
gateway does not enforce one).
- **Validation:** none client-side (no `Validators`). Model non-nullable fields
(`name`, `slug`, `sku`, `categoryId`, `price`, `currency`, `quantity`) are the
only implicit "required" signal. Uniqueness of `slug`/`sku`, price ≥ 0, etc.
are all **Requires backend decision** (no `isSlugTaken` equivalent exists for
products, unlike categories). Recommended `422 VALIDATION_FAILED` with
`details[].field` for `sku`/`slug`/`price`.
- **Error responses:** `404` (AP2/AP5-AP9 unknown id), `409 CONFLICT`
(duplicate slug/sku on AP4/AP5 — Requires backend decision, no proactive
pre-check exists), `422` (field validation), plus the common set.
- **Example AP4 request (trimmed):**
```json
{ "name": "Phone X", "slug": "phone-x", "sku": "PHN-X-001", "barcode": "", "brand": "Acme", "categoryId": "3", "visible": true, "archived": false, "priority": 0, "price": 62560, "discount": 0, "currency": "RUB", "quantity": 100, "stockStatus": "in_stock", "media": { "images": [], "gallery": [], "videos": [] }, "specifications": [], "attributes": [], "variantAttributes": [], "variants": [], "relatedProductIds": [], "translations": {}, "seo": { "metaTitle": "", "metaDescription": "", "keywords": "" }, "badges": [] }
```
- **Example AP1 response:**
```json
{ "items": [ { "id": "p_1024", "name": "Phone X", "slug": "phone-x", "sku": "PHN-X-001", "categoryId": "3", "price": 62560, "currency": "RUB", "quantity": 100, "stockStatus": "in_stock", "visible": true, "archived": false } ], "total": 213, "page": 1, "pageSize": 20 }
```
---
### 3.2 Categories
Three surfaces: storefront read (LIVE, legacy + clean stack), and admin CRUD
(the **only** admin domain with a real HTTP impl).
#### 3.2.a Storefront category read — LIVE
| # | Method | Path (LIVE literal) | Backed by | Success |
|---|---|---|---|---|
| C1 | GET | `{base}/category` | `ApiService.getCategories()` (legacy) and `ApiCategoryRepository` (clean stack, `CATEGORY_REPOSITORY`, retry x2) | `200` array of category wire objects |
- Two parallel stacks read the same endpoint (`BACKEND-AUDIT.md` §8). Legacy
wire shape: `Category`/`Subcategory` (`src/app/models/category.model.ts`) via
`normalizeCategory()`. Clean wire DTO: `CategoryDto`/`CategoryNameDto`
(`src/app/core/categories/dto/category.dto.ts`) via `CategoryMapper`. Both
tolerate `names[]` (multi-lang, incl. the `valuue` typo variant), nested
`subcategories[]`, `img``icon`, string↔numeric ids.
- **Headers/JWT:** marketplace headers, no JWT, public.
- **Response `CategoryDto` fields:** `categoryID?`, `parentID?`, `name?`,
`icon?`, `img?`, `priority?`, `visible?`, `categoriesCount?`, `itemCount?`,
`names?: [{ language, value|valuue }]`, `subcategories?: CategoryDto[]`.
- **Error responses:** `500/503` → falls through to empty categories (clean
stack retries twice first). No 404 (collection endpoint).
- **Example response:**
```json
[ { "categoryID": 3, "parentID": 0, "name": "Phones", "icon": "https://…/phones.svg", "priority": 10, "visible": true, "itemCount": 213, "names": [ { "language": "RU", "value": "Телефоны" }, { "language": "EN", "value": "Phones" } ], "subcategories": [] } ]
```
- **Note (`BACKEND-AUDIT.md` §8):** two different `Category` TS types exist —
flag for backend/naming reconciliation.
#### 3.2.b Admin categories CRUD — LIVE (real HTTP gateway exists)
Contract: `AdminCategoriesGateway`. **Real impl:** `AdminCategoriesApiGateway`
(literal paths below — the canonical precedent for all other admin domains).
Mock impl: `AdminCategoriesLocalGateway`. Bound via `ADMIN_CATEGORIES_GATEWAY`
token (MOCK-SWAPPABLE — this domain *can* be rebound to the real gateway today).
DTO: `AdminCategory` (`src/app/features/admin/categories/models/admin-category.model.ts`).
| # | Method | Path (LIVE literal) | Gateway method | Success |
|---|---|---|---|---|
| AC1 | GET | `{base}/backoffice/categories?search&visibility&includeDeleted` | `loadCategories(filters)` | `200 AdminCategory[]` |
| AC2 | GET | `{base}/backoffice/categories/{id}` | `loadCategory(id)` | `200 AdminCategory` / `null` on error |
| AC3 | POST | `{base}/backoffice/categories` | `createCategory(cat)` | `201 AdminCategory` |
| AC4 | PUT | `{base}/backoffice/categories/{id}` | `updateCategory(cat)` | `200 AdminCategory` |
| AC5 | DELETE | `{base}/backoffice/categories/{id}` | `deleteCategory(id)` | `204` (**soft** delete) |
| AC6 | POST | `{base}/backoffice/categories/{id}/restore` | `restoreCategory(id)` | `200 AdminCategory` / `null` |
| AC7 | GET | `{base}/backoffice/categories/slug-taken?slug&excludingId` | `isSlugTaken(slug,excludingId)` | `200 { taken: boolean }` |
- **Headers/JWT:** admin auth (§3.0).
- **Permission (Proposed):** AC1/AC2/AC7 → `backoffice.read`; AC3-AC6 →
`backoffice.write`.
- **AC1 query params** (`AdminCategoryListFilters`): `search` (string),
`visibility``all|visible|hidden`, `includeDeleted` (bool sent as string).
**No pagination** — returns a plain array. No sort param.
- **AC3 request body:** `AdminCategory` **minus** `id, itemsCount, deletedAt,
createdAt, updatedAt` (server-owned — stripped by the real gateway, line 38).
AC4 sends the full object, `id` in path.
- **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).
- **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
`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`
slug conflict. Common set otherwise.
- **Example AC3 request:**
```json
{ "parentId": null, "title": "Phones", "slug": "phones", "description": "", "icon": "", "imageUrl": "", "imageAlt": "", "order": 0, "visible": true, "status": "published", "translations": {}, "seo": { "metaTitle": "", "metaDescription": "", "keywords": "" }, "attributes": [] }
```
- **Example AC7 response:** `{ "taken": true }`
- **Note:** categories have **no hard delete** — only soft delete + restore
(gateway doc comment). `deletedAt` timestamps the soft delete.
---
### 3.3 Orders
Two surfaces: **checkout order creation** (LIVE, storefront) and **admin order
management** (MOCK-ONLY, no seam). These are separate models — the storefront
`CreateOrderRequest`/`CreateOrderResponse` (inline in `api.service.ts`) is not the
admin `AdminOrder`.
#### 3.3.a Cart → payment → order creation — LIVE
Cart contents are **LOCAL-ONLY** (`CartService`, localStorage key
`marketplace_cart` + Telegram CloudStorage — no cart backend). Checkout produces
these live calls (`BACKEND-AUDIT.md` §10). Payment DTOs are inline in
`src/app/services/api.service.ts`.
| # | Method | Path | Base | Request → Response |
|---|---|---|---|---|
| O1 | POST | `/cart` | marketplace | `CartPaymentRequest``QrCreateResponse` (creates the QR/card payment) |
| O2 | POST | `/qr` | qrApiUrl (headers `authorization-key`, `userid-value`) | `QrCreateRequest``QrCreateResponse` |
| O3 | GET | `/qr/dynamic/{partnerId}/{qrId}` | qrApiUrl | → `QrDynamicStatusResponse` (poll) |
| O4 | GET | `/card/{partnerId}/{orderId}` | qrApiUrl | → `QrDynamicStatusResponse` (poll) |
| O5 | POST | `/orders` | marketplace | `CreateOrderRequest``CreateOrderResponse` (records paid cart; **fire-and-forget** after payment) |
| O6 | POST | `/purchase-email` | marketplace | email/phone + items → `{ message }` (receipt) |
| O7 | POST | `/websession/{sessionId}` | marketplace | item array → any (server-side cart mirror) |
- `partnerId` const = `web-97ec-9c57-4dde-9037-3a68f7f83750`.
- **Headers/JWT:** marketplace headers (O1/O5/O6/O7); O2-O4 use the payment API
with `authorization-key`/`userid-value`. No JWT; customer session via
`WebSessionID`.
- **`CartPaymentRequest` (O1):** `{ amount, currency:'RUB', siteuserID,
siteorderID, redirectUrl, telegramUsername, paymentMethod:'qr'|'card',
qrDescription?, customerID?, items:[{ itemID, price, name, quantity?,
delivery? }] }`.
- **`CreateOrderRequest` (O5):** `{ items:[{ productId, name, quantity, price }],
customer:{ name, email, phone }, payment?:{ method, currency }, shipping?:{
address, method, trackingNumber } }`.
- **`CreateOrderResponse` (O5):** `{ id, orderNumber, status, total, currency }`.
- **`QrCreateResponse`** is a very tolerant union (`api.service.ts` 21-41) —
resolvers pick the first present of `qrId/qrID/nspkID/…`. A backend can emit
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,
Requires backend decision). Payment status polling handles its own timeouts.
- **Validation:** none client-side. Amounts/quantities ≥ 0, valid email/phone =
Requires backend decision.
- **Example O5 request:**
```json
{ "items": [ { "productId": "1024", "name": "Phone X", "quantity": 1, "price": 62560 } ], "customer": { "name": "Ivan", "email": "ivan@example.com", "phone": "+79990000000" }, "payment": { "method": "qr", "currency": "RUB" } }
```
- **Example O5 response:** `{ "id": "ord_88", "orderNumber": "R-000088", "status": "pending", "total": 62560, "currency": "RUB" }`
#### 3.3.b Admin orders management — PROPOSED (MOCK-ONLY, no seam)
Contract: `AdminOrdersGateway`. Impl: `AdminOrdersLocalGateway` (injected
concretely by `AdminOrdersFacade`, and reused by `AdminCustomersFacade` and
`AdminAnalyticsFacade`). DTO: `AdminOrder`
(`src/app/features/admin/orders/models/admin-order.model.ts`).
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| AO1 | GET | `{base}/backoffice/orders?search&status&page&pageSize` | `loadOrders(filters)` | `200 AdminOrdersListResult` |
| AO2 | GET | `{base}/backoffice/orders/{id}` | `loadOrder(id)` | `200 AdminOrder` / `404` |
| AO3 | PATCH | `{base}/backoffice/orders/{id}/status` | `updateStatus(id,status)` | `200 AdminOrder` |
| AO4 | POST | `{base}/backoffice/orders/{id}/refund` | `requestRefund(id)` | `200 AdminOrder` |
| AO5 | POST | `{base}/backoffice/orders/{id}/notes` | `addNote(id,note,internal)` | `200 AdminOrder` |
| AO6 | POST | `{base}/backoffice/orders/{id}/archive` | `archiveOrder(id)` | `200 AdminOrder` |
| AO7 | POST | `{base}/backoffice/orders/{id}/restore` | `restoreOrder(id)` | `200 AdminOrder` |
| AO8 | DELETE | `{base}/backoffice/orders/{id}` | `deleteOrder(id)` | `204` |
- **Headers/JWT:** admin auth (§3.0).
- **Permission (Proposed):** AO1/AO2 → `backoffice.read`; AO3-AO8 →
`backoffice.write`.
- **AO1 query params** (`AdminOrderListFilters`): `search`, `status`
`all|pending|processing|shipped|delivered|cancelled|refunded`, `page`,
`pageSize`. Response `AdminOrdersListResult { items, total, page, pageSize }`.
No sort param.
- **AO3 body:** `{ status: AdminOrderStatus }` where status ∈ the 6-value union
above. **State-transition rules** (which transitions are legal, e.g.
`delivered → pending`?) are **Requires backend decision** — the frontend
enforces none. `payment.status` (`unpaid|paid|refund_requested|refunded`) is a
separate axis; AO4 (`requestRefund`) is what moves it to `refund_requested`.
- **AO5 body:** `{ note: string, internal: boolean }``internal:true` writes
`internalNotes`, else `notes` (both plain strings on `AdminOrder`). Appends a
timeline entry.
- **`AdminOrder` timeline:** `timeline: [{ status, timestamp, eventKey:
'created'|'statusChanged'|'refundRequested' }]` — server should append on each
mutation.
- **Validation:** none client-side. Legal status transitions, refund
eligibility = Requires backend decision.
- **Error responses:** `404` (unknown id), `409` (illegal transition — proposed),
common set.
- **Example AO3 request:** `PATCH …/orders/ord_88/status` body `{ "status": "shipped" }`
- **Example AO2 response (trimmed):**
```json
{ "id": "ord_88", "orderNumber": "R-000088", "status": "shipped", "customer": { "name": "Ivan", "email": "ivan@example.com", "phone": "+79990000000" }, "payment": { "method": "qr", "status": "paid", "amount": 62560, "currency": "RUB" }, "shipping": { "address": "…", "method": "courier", "trackingNumber": "TRK1" }, "items": [ { "productId": "1024", "name": "Phone X", "quantity": 1, "price": 62560 } ], "total": 62560, "currency": "RUB", "notes": "", "internalNotes": "", "timeline": [ { "status": "pending", "timestamp": "2026-07-20T10:00:00Z", "eventKey": "created" } ], "archived": false, "createdAt": "2026-07-20T10:00:00Z", "updatedAt": "2026-07-21T09:00:00Z" }
```
---
### 3.4 Customers — PROPOSED (derived, no gateway of its own)
Per `BACKEND-AUDIT.md` §14 and the model comment
(`src/app/features/admin/customers/models/admin-customer.model.ts`): a customer is
**not a stored entity** — `AdminCustomersFacade` derives customers by grouping
`AdminOrder` records by `customer.email` (it injects `AdminOrdersLocalGateway`).
Every `AdminCustomer` field is a real aggregate over that customer's orders, never
fabricated.
**Frontend implication:** there is **no customer CRUD in the frontend at all**
no create/update/delete, only a derived list/detail. A backend can satisfy this
either by (a) exposing a real customers endpoint returning the aggregate shape, or
(b) letting the frontend keep deriving from orders. This is **Requires backend
decision** (whether customers become a first-class backend resource).
| # | Method | Path (PROPOSED) | Purpose | Success |
|---|---|---|---|---|
| CU1 | GET | `{base}/backoffice/customers` | List (derived aggregate) | `200 AdminCustomer[]` |
| CU2 | GET | `{base}/backoffice/customers/{email}` | Detail (aggregate + orders) | `200 AdminCustomer` / `404` |
- **Key = `email`** (customers are keyed by email, not an id).
- **`AdminCustomer`:** `{ email, name, phone, orderCount, totalSpent, currency,
firstOrderAt, lastOrderAt, addresses[], orders: AdminOrder[] }`.
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`.
- **Pagination/sort/filter:** none in the model (plain array). Derivation is
entirely client-side today.
- **Validation:** n/a (read-only, derived).
- **Error responses:** common read set; `404` if a real customers endpoint is
built and the email is unknown.
- **Example CU1 response (trimmed):**
```json
[ { "email": "ivan@example.com", "name": "Ivan", "phone": "+79990000000", "orderCount": 3, "totalSpent": 187680, "currency": "RUB", "firstOrderAt": "2026-01-10T…", "lastOrderAt": "2026-07-20T…", "addresses": ["…"], "orders": [] } ]
```
---
### 3.5 Users — PROPOSED (MOCK-ONLY, no seam)
Contract: `AdminUsersGateway`
(`src/app/features/admin/users/services/admin-users-gateway.interface.ts`). Impl:
`AdminUsersLocalGateway` (injected concretely by `AdminUsersFacade`). DTOs in
`admin-user.model.ts`: `AdminUser`, `AdminRole`, `AdminInvitation`, `AdminSession`,
`AdminUserAuditEntry`.
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| U1 | GET | `{base}/backoffice/users` | `loadUsers()` | `200 AdminUser[]` |
| U2 | GET | `{base}/backoffice/roles` | `loadRoles()` | `200 AdminRole[]` |
| U3 | GET | `{base}/backoffice/invitations` | `loadInvitations()` | `200 AdminInvitation[]` |
| U4 | GET | `{base}/backoffice/users/{userId}/sessions` | `loadSessions(userId)` | `200 AdminSession[]` |
| U5 | GET | `{base}/backoffice/users/{userId}/audit` | `loadAudit(userId)` | `200 AdminUserAuditEntry[]` |
| U6 | PATCH | `{base}/backoffice/users/{userId}/role` | `setUserRole(userId,roleId)` | `200 AdminUser` |
| U7 | PATCH | `{base}/backoffice/users/{userId}/status` | `setUserStatus(userId,status)` | `200 AdminUser` |
| U8 | POST | `{base}/backoffice/invitations` | `inviteUser(email,roleId,scope)` | `201 AdminInvitation` |
| U9 | DELETE | `{base}/backoffice/invitations/{id}` | `revokeInvitation(id)` | `204` |
| U10 | DELETE | `{base}/backoffice/sessions/{sessionId}` | `revokeSession(sessionId)` | `204` |
- **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
backend decision.
- **U6 body:** `{ roleId: string }`. **U7 body:** `{ status: AdminUserStatus }`
where status ∈ `active|invited|suspended`. **U8 body:** `{ email, roleId,
scope }` where scope ∈ `marketplace|office`.
- **`AdminRole`** here is `{ id, name, permissions: string[], builtIn: boolean }`
**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
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.**
- **Roles CRUD:** the gateway exposes only `loadRoles()` (read). There is **no
create/update/delete role** endpoint in the frontend — role definitions are
read-only from the frontend's perspective. Whether roles are backend-editable =
Requires backend decision.
- **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).
- **Validation:** none client-side. Email format, role existence, self-demotion
guards = Requires backend decision.
- **Error responses:** `404` (unknown user/session/invitation), `409` (e.g.
inviting an already-registered email — proposed), `422` (bad email/role),
common set.
- **Example U8 request:** `{ "email": "new.admin@example.com", "roleId": "role_editor", "scope": "office" }`
- **Example U1 response (trimmed):**
```json
[ { "id": "u_1", "name": "Alice", "telegramUsername": "@alice", "email": "alice@example.com", "scope": "office", "roleId": "role_owner", "status": "active", "lastLoginAt": "2026-07-24T…", "createdAt": "2026-01-01T…" } ]
```
---
### 3.6 Roles
Covered structurally under Users (§3.5, endpoint U2 `loadRoles`). Summary of the
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
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
the users screen. `AdminUser.roleId` references `AdminRole.id` here (a
string id like `role_owner`), **not** the string-union value.
- **The two do not reference each other.** A backend role table must decide
whether `AdminRole.id` (#2) resolves to one of the five #1 union values (i.e.
`builtIn` roles map onto the permission model) and whether custom
(`builtIn:false`) roles with arbitrary `permissions[]` are allowed. **Requires
backend decision** — the frontend does not resolve this collision.
- **No role CRUD endpoint** exists in the frontend beyond read (`loadRoles`).
Custom-role create/edit/delete = Requires backend decision.
---
### 3.7 Media — PROPOSED (MOCK-SWAPPABLE via abstract-class token)
Contract: abstract class `MediaRepository` (`src/app/core/media/media-repository.ts`).
Bound in `app.config.ts``MockMediaRepository` (`useClass`). MOCK-SWAPPABLE (a
real impl drops in via the same class token — no new seam needed). DTO:
`MediaAsset` (`src/app/core/media/models/media-asset.model.ts`).
**Scope note (per task):** this section covers the **CRUD list/delete/metadata**
endpoints only. Upload *mechanics* (multipart, presigned URLs, chunking) are
deferred to a sibling "Uploads" section of the master doc — here `upload` is
listed for completeness but its transport is out of scope.
| # | Method | Path (PROPOSED) | Repository method | Success |
|---|---|---|---|---|
| M1 | GET | `{base}/media?page&pageSize&search&folder&tag&kind&sort` | `list(params)` | `200 MediaListResult { items, total }` |
| M2 | POST | `{base}/media` (multipart — see Uploads section) | `upload(file,options)` | `201 MediaAsset` |
| M3 | DELETE | `{base}/media/{id}` | `remove(id)` | `204` |
| M4 | PATCH | `{base}/media/{id}` | `update(id,patch)` | `200 MediaAsset` |
| M5 | GET | `{base}/media/folders` | `listFolders()` | `200 string[]` |
- **Headers/JWT:** the `adminAuthHeadersInterceptor` already gates `/media/`
paths (§3.0, anticipatory) — admin auth applies. Permission (Proposed):
`backoffice.read` (M1/M5), `backoffice.write` (M2/M3/M4).
- **M1 query params** (`MediaListParams`): `page?`, `pageSize?`, `search?`,
`folder?`, `tag?`, `kind?``image|svg|pdf|other`, `sort?`
`recent|name|size`. Response `{ items: MediaAsset[], total }` (offset paginated).
- **M4 patch body** — restricted to metadata: `Partial<Pick<MediaAsset, 'altText'
| 'tags' | 'folder' | 'caption' | 'description' | 'decorative'>>`. The
repository signature **forbids** patching `url/filename/mimeType/size/width/
height` — a backend should reject those on PATCH.
- **`MediaAsset`:** `{ id, url, thumbnailUrl?, filename, mimeType, size, width?,
height?, altText?: Record<locale,string>, caption?, description?, decorative?,
tags?, folder?, createdAt }`.
- **Validation:** none client-side. Allowed mime types / max size = Requires
backend decision (also relevant to the Uploads section).
- **Error responses:** `404` (M3/M4 unknown id), `422` (invalid patch field),
common set. `MediaUsageService` exists client-side to warn on in-use assets —
whether delete is blocked when an asset is referenced is Requires backend
decision.
- **Example M4 request:** `PATCH …/media/asset_9` body `{ "altText": { "en": "Product hero", "ru": "…" }, "tags": ["hero"], "decorative": false }`
- **Example M1 response (trimmed):**
```json
{ "items": [ { "id": "asset_9", "url": "https://…/hero.webp", "filename": "hero.webp", "mimeType": "image/webp", "size": 48213, "width": 1200, "height": 630, "folder": "products", "createdAt": "2026-07-01T…" } ], "total": 342 }
```
---
### 3.8 CMS — Static pages — PROPOSED / FUTURE (LOCAL-ONLY today)
**Status: LOCAL-ONLY.** `ContentManagementFacade`
(`src/app/features/content-management/facade/content-management.facade.ts`) →
`ContentPageService` operates entirely on the **already-loaded
`BootstrapConfig.staticPages`** — there are **no dedicated CMS backend calls**
(`BACKEND-AUDIT.md` §16). Editing writes to in-memory bootstrap + localStorage
drafts; "publishing" = writing bootstrap back, for which **no client HTTP call
exists**. So every endpoint below is **PROPOSED / FUTURE**.
Backend-shaped model: `StaticPageConfig` / `StaticPagesConfig` /
`ResolvedStaticPage` (`src/app/shared/models/config/static-page.model.ts`). Editor
view model: `ContentPage` (`src/app/features/content-management/models/content-page.model.ts`);
`ContentPageService` is the adapter (`toBootstrapRecord`/`serializePages`).
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| CM1 | GET | `{base}/builder/static-pages` | List pages | `200 StaticPagesConfig` |
| CM2 | GET | `{base}/builder/static-pages/{id}` | Single page | `200 StaticPageConfig` / `404` |
| CM3 | POST | `{base}/builder/static-pages` | Create page | `201 StaticPageConfig` |
| CM4 | PUT | `{base}/builder/static-pages/{id}` | Update page | `200 StaticPageConfig` |
| CM5 | DELETE | `{base}/builder/static-pages/{id}` | Delete page | `204` |
- **Reality check:** today the frontend gets these pages **inside `GET /bootstrap`**
and never calls CM1-CM5. If the backend keeps CMS inside bootstrap, these
endpoints may never exist — the alternative is a builder-publish endpoint that
writes the whole bootstrap (see §3.15). **Requires backend decision:**
per-page CMS endpoints vs. whole-bootstrap publish.
- **Headers/JWT:** admin/builder auth (§3.0). Permission (Proposed):
`builder.read` (CM1/CM2), `builder.write` (CM3-CM5).
- **`StaticPageConfig` key fields:** `{ id, slug, title: string|Localized,
showInFooter?, showInHeader?, showInSitemap?, icon?, order?, requiresAuthentication?,
translations?: Record<locale, { title?, html?, seo? }>, html?: string|Localized,
seo?: StaticPageSeoConfig, visible?, enabled?, status?: 'draft'|'published',
heroImage?, gallery?, updatedAt? }`. Two lifecycle switches: `enabled` (master
on/off) and per-page `status` (`draft` never resolves on storefront even if the
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
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),
`422` (validation), common set. None handled specially by the UI today.
- **Example CM4 request (trimmed):**
```json
{ "id": "about", "slug": "about-us", "title": { "en": "About us", "ru": "О нас" }, "showInFooter": true, "status": "published", "enabled": true, "html": { "en": "<h1>About</h1>" }, "seo": { "title": { "en": "About us" }, "robots": "index,follow" } }
```
---
### 3.9 Homepage — PROPOSED / FUTURE (LOCAL-ONLY)
**Status: LOCAL-ONLY** (project-editor, `BACKEND-AUDIT.md` §17). The homepage is a
**composition of `SectionConfig``WidgetConfig`** stored inside
`BootstrapConfig` (`layout`/`pages`). Edited via `ProjectEditorFacade` +
`homepage-section.component.ts` / `homepage-overview.component.ts` against
in-memory bootstrap; persisted only to localStorage drafts. No HTTP save/publish
call exists.
Models: `SectionConfig` (`src/app/shared/models/config/section.model.ts`) —
`{ id, type, order, layout?: { strategy: 'stack'|'grid'|'hero'|'carousel'|'split',
columns?, gap?, align? }, visibility?, widgets: WidgetConfig[], visible? }`.
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| HP1 | GET | `{base}/builder/homepage` | Load section/widget composition | `200 SectionConfig[]` |
| HP2 | PUT | `{base}/builder/homepage` | Replace composition (reorder, add/remove sections & widgets) | `200 SectionConfig[]` |
- **Why a single PUT, not granular CRUD:** the editor mutates an in-memory tree
and saves it whole (undo/redo over `History<BootstrapConfig>`); there is no
per-section/per-widget network operation in the frontend. A whole-composition
PUT matches how the client actually works. Granular section/widget endpoints
would be new backend design. **Requires backend decision.**
- **Headers/JWT:** builder auth (§3.0). Permission (Proposed): `builder.read`
(HP1), `builder.write` (HP2).
- **Section ordering** is by the numeric `order` field; widgets carry their own
`order` within a section.
- **Validation:** `ProjectValidator` (client-side, localStorage-scoped). Section
`type` allow-list, widget `type` must exist in the widget manifest (§3.10) =
Requires backend decision.
- **Error responses:** `422` (invalid section/widget type or malformed tree),
common set.
- **Example HP2 request (trimmed):**
```json
[ { "id": "hero", "type": "hero", "order": 0, "layout": { "strategy": "hero" }, "visible": true, "widgets": [ { "id": "w1", "type": "hero-banner", "version": "1.0.0", "order": 0, "props": { "title": "Sale" } } ] } ]
```
---
### 3.10 Widgets — PROPOSED / FUTURE (LOCAL-ONLY) + widget manifest (LIVE)
Two things named "widgets": the **manifest** (LIVE) and the **widget instances**
inside sections (LOCAL-ONLY, edited via project-editor `widgets-section.component.ts`).
#### 3.10.a Widget manifest — LIVE
`WidgetManifestService` (`src/app/widgets/registry/widget-manifest.service.ts`):
| # | Method | Path (LIVE) | Success |
|---|---|---|---|
| W1 | GET | `bootstrap.widgetRegistry.manifestUrl` (fallback `/assets/mock/bootstrap/widget-manifest.json`) | `200 WidgetManifestFile` |
- Read-only registry of available widget types + their settings schema
(`WidgetManifestEntry/File`, `WidgetSettingsSchema``src/app/widgets/contracts/widget-manifest.contract.ts`).
- **Headers/JWT:** the manifest URL is bootstrap-driven; public read.
- **Validation/errors:** on failure falls back to the static asset. No mutation.
#### 3.10.b Widget instances CRUD — PROPOSED / FUTURE
Widget instances (`WidgetConfig`, `src/app/shared/models/config/widget.model.ts`)
live inside `SectionConfig.widgets[]` and are created/updated/removed/reordered
purely in-memory via `ProjectEditorFacade` (localStorage drafts). There is **no
per-widget backend call** — they are saved as part of the homepage composition
(§3.9 HP2). So there is **no separate widget CRUD endpoint** unless the backend
chooses granular editing (Requires backend decision). If it does:
| # | Method | Path (PROPOSED / FUTURE) | Purpose |
|---|---|---|---|
| W2 | POST | `{base}/builder/sections/{sectionId}/widgets` | Add widget |
| W3 | PUT | `{base}/builder/sections/{sectionId}/widgets/{widgetId}` | Update widget |
| W4 | DELETE | `{base}/builder/sections/{sectionId}/widgets/{widgetId}` | Remove widget |
- **`WidgetConfig`:** `{ id, type, version, title?, subtitle?, order?, padding?,
visibility?: { desktop?, tablet?, mobile? }, animation?, style?: Record<string,
string>, permissions?: { requireAuthenticated?, roles?, permissions? }, props:
Record<string, unknown>, actions?, featureFlag?, visible? }`.
- **Permission (Proposed):** `builder.write`. **Validation:** widget `type` must
exist in the manifest (W1); `props` shape validated against
`WidgetSettingsSchema`. Both client-side/manifest-driven; backend enforcement =
Requires backend decision.
- **Recommendation:** treat §3.9 HP2 (whole-composition PUT) as canonical and
W2-W4 as optional granular sugar the frontend does not need today.
---
### 3.11 Navigation — PROPOSED / FUTURE (LOCAL-ONLY via builder)
**Status: LOCAL-ONLY.** Header/footer nav is `BootstrapConfig.navigation`
(`src/app/shared/models/config/navigation.model.ts`), edited via
`ProjectEditorFacade` + `navigation-section.component.ts`. Facade methods
(all in-memory + localStorage; `project-editor.facade.ts`): `addNavLink`,
`addStaticPageNavLink`, `removeNavLink`, `updateNavLink`, `updateNavLinkLabel`,
`reorderNavLink` — all take `target: 'header' | 'footer'`. No HTTP call exists.
Model: `NavigationConfig { header: NavigationItemConfig[], footer:
NavigationItemConfig[] | FooterNavigationGroupConfig[], sidebar? }`.
`NavigationItemConfig { id, labelKey?, label?: string|Localized, route?, type?,
key?, icon?, order?, visible?, visibleWhenFlags?, children? }`.
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| N1 | GET | `{base}/builder/navigation` | Load nav config | `200 NavigationConfig` |
| N2 | PUT | `{base}/builder/navigation` | Replace nav config (add/remove/update/reorder items) | `200 NavigationConfig` |
- **Whole-config PUT** matches the editor (it mutates the nav tree in memory and
saves whole). Granular per-item endpoints = Requires backend decision.
- **Headers/JWT:** builder auth. Permission (Proposed): `builder.read` (N1),
`builder.write` (N2).
- **Ordering:** `reorderNavLink(target, id, direction: -1|1)` moves an item; the
numeric `order` field is authoritative on the wire.
- **Localized labels:** `updateNavLinkLabel(target, id, value, locale?)` writes
into `label` as a `{ [locale]: string }` map.
- **Validation:** none client-side beyond the builder validator. Route validity,
no orphan `children` = Requires backend decision.
- **Errors:** `422` (malformed tree), common set.
- **Example N2 request (trimmed):**
```json
{ "header": [ { "id": "n1", "label": { "en": "Catalog", "ru": "Каталог" }, "route": "/catalog", "order": 0, "visible": true } ], "footer": [] }
```
---
### 3.12 Footer — PROPOSED / FUTURE (LOCAL-ONLY via builder)
**Status: LOCAL-ONLY.** `BootstrapConfig.footer` (`FooterConfig`,
`src/app/shared/models/config/footer-config.model.ts`), edited via
`footer-section.component.ts` + `ProjectEditorFacade`. No HTTP call.
`FooterConfig { logoUrl?, paymentIcons?: [{ src, alt, width?, height? }],
copyrightText?: string|Localized, columns?: [{ id, title, links: [{ id, label,
pageKey?, url? }] }], socialLinks?: [{ id, label, url, icon? }], legalPageKeys?
(deprecated), staticPageKeys? (deprecated) }`.
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| F1 | GET | `{base}/builder/footer` | Load footer config | `200 FooterConfig` |
| F2 | PUT | `{base}/builder/footer` | Replace footer (payment icons, columns/legal links, copyright, socials) | `200 FooterConfig` |
- **Headers/JWT:** builder auth. Permission (Proposed): `builder.read` (F1),
`builder.write` (F2).
- **Link resolution:** `FooterLinkConfig.pageKey` (preferred, references a CMS
page id — stays correct if the page route changes) vs. `url` (raw external).
`legalPageKeys`/`staticPageKeys` are **deprecated**, superseded by `columns`
a backend should accept them for back-compat but write `columns`.
- **Validation:** none client-side. Payment-icon URL validity, non-empty column
titles = Requires backend decision.
- **Errors:** `422`, common set.
- **Example F2 request (trimmed):**
```json
{ "logoUrl": "https://…/logo.svg", "paymentIcons": [ { "src": "https://…/visa.svg", "alt": "Visa", "width": 40, "height": 24 } ], "copyrightText": { "en": "© 2026 Acme" }, "columns": [ { "id": "legal", "title": "Legal", "links": [ { "id": "l1", "label": "Terms", "pageKey": "terms" } ] } ], "socialLinks": [] }
```
---
### 3.13 Branding / Theme — PROPOSED / FUTURE (LOCAL-ONLY via builder)
**Status: LOCAL-ONLY.** `BootstrapConfig.branding` (`BrandingConfig`,
`src/app/shared/models/config/branding.model.ts`) and `BootstrapConfig.theme`
(`theme.model.ts`), edited via `branding-section.component.ts` /
`theme-section.component.ts` / `brand-overview.component.ts`. No HTTP call.
`BrandingConfig { brandName, legalName, slogan?, logoUrl, logoCompactUrl?,
faviconUrl, appIconUrl?, socialImageUrl?, galleryUrls?, supportEmail?,
supportPhone? }`.
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| B1 | GET | `{base}/builder/branding` | Load branding + theme | `200 { branding: BrandingConfig, theme: ThemeConfig }` |
| B2 | PUT | `{base}/builder/branding` | Update branding | `200 BrandingConfig` |
| B3 | PUT | `{base}/builder/theme` | Update theme (colors, tokens) | `200 ThemeConfig` |
- **Headers/JWT:** builder auth. Permission (Proposed): `builder.read` (B1),
`builder.write` (B2/B3). Could be `settings.manage` if branding is treated as a
tenant-settings concern — Requires backend decision.
- **Theme model** (`src/app/shared/models/config/theme.model.ts`, not re-listed
here) carries color tokens; the editor computes contrast client-side
(`sections/brand/contrast.util.ts`). Contrast/accessibility is enforced
client-side only.
- **Validation:** none backend-driven. `brandName`, `logoUrl`, `faviconUrl`
non-optional in the model; valid email for `supportEmail` = Requires backend
decision.
- **Errors:** `422`, common set.
- **Example B2 request (trimmed):**
```json
{ "brandName": "Acme", "legalName": "Acme LLC", "logoUrl": "https://…/logo.svg", "faviconUrl": "https://…/favicon.ico", "supportEmail": "help@acme.com" }
```
---
### 3.14 Languages — PROPOSED / FUTURE (LOCAL-ONLY via builder)
**Status: LOCAL-ONLY.** Locale management is `BootstrapConfig.localization` +
`BootstrapConfig.tenant.supportedLocales/defaultLocale`, edited via
`languages-section.component.ts` + `ProjectEditorFacade` methods `addLocale`,
`removeLocale`, `setDefaultLocale` — all mutate in-memory bootstrap only. No HTTP.
`LocalizationConfig { defaultLocale, supportedLocales: string[], currencyByLocale:
Record<locale,currency>, dictionaries: [{ locale, dictionaryUrl, version }] }`.
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| L1 | GET | `{base}/builder/languages` | Load locale config | `200 LocalizationConfig` |
| L2 | POST | `{base}/builder/languages` | Add a supported locale | `201 LocalizationConfig` |
| L3 | DELETE | `{base}/builder/languages/{code}` | Remove a locale | `200 LocalizationConfig` |
| L4 | PUT | `{base}/builder/languages/default` | Set default locale | `200 LocalizationConfig` |
- **Headers/JWT:** builder auth. Permission (Proposed): `builder.write` for
L2-L4, `builder.read` for L1. Could be `settings.manage` — Requires backend
decision.
- **Client-side rules that a backend should mirror:** locale code is lowercased
and trimmed on add; adding an already-present locale is rejected client-side
with `builder.languageAlreadyAdded` (so **`409 CONFLICT`** on L2 duplicate is
the right backstop). L2 body: `{ code: string }` (a locale like `en`, `ru`,
`hy`). L4 body: `{ code: string }` — must be an already-supported locale.
- **Removing a locale** shows a confirm dialog (destructive: strips that locale's
content). Whether removing the *default* locale is allowed = Requires backend
decision (the UI lets you set a new default first).
- **Validation:** non-empty lowercased code; membership check for default. Valid
BCP-47 code = Requires backend decision.
- **Errors:** `409` (duplicate add), `422` (invalid code / removing last locale),
common set.
- **Example L2 request:** `{ "code": "hy" }`
---
### 3.15 Marketplace Settings — PROPOSED / FUTURE
There is **no dedicated "settings" admin gateway or page** beyond the builder
sections already covered (§3.8-3.14) and `BootstrapConfig`'s remaining sub-configs
(`featureFlags`, `features`, `seo`, `catalog`, `header`, `layout`,
`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).
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
only to localStorage drafts + in-memory bootstrap. A real backend needs **one of**:
| # | Method | Path (PROPOSED / FUTURE) | Purpose | Success |
|---|---|---|---|---|
| S1 | GET | `{base}/bootstrap` | Load full config (**LIVE** — this one exists) | `200 BootstrapConfig` |
| S2 | PUT | `{base}/builder/bootstrap` (or `/builder/publish`) | Persist the edited bootstrap (publish) | `200 BootstrapConfig` |
| S3 | GET | `{base}/builder/settings` | General tenant settings (feature flags, catalog, SEO) | `200` (subset of BootstrapConfig) |
| S4 | PUT | `{base}/builder/settings` | Update general settings | `200` |
- **S1 is the only LIVE endpoint here** (`ApiBootstrapProvider`, `GET /bootstrap`).
- **S2 is the single most important FUTURE decision:** a whole-bootstrap publish
endpoint vs. the per-section builder endpoints proposed in §3.8-3.14. The
frontend has an `apiEndpoints.builder` record slot for exactly this but no code
that calls it. **Requires backend decision:** whole-document publish vs.
granular section writes (and optimistic-concurrency / versioning via
`BootstrapConfig.schemaVersion`/`generatedAt`).
- **Headers/JWT:** builder/settings auth. Permission (Proposed): `settings.manage`
(S2/S4), `builder.read`/`settings` read for S1/S3.
- **Validation:** `ProjectValidator` runs client-side before a (currently local)
publish. Backend-side schema validation of the whole `BootstrapConfig` =
Requires backend decision.
- **Errors:** `422` (invalid config), `409` (concurrent edit — proposed, if
versioning adopted), common set.
---
### 3.16 Transactions — PROPOSED (MOCK-ONLY, no seam)
Contract: `AdminTransactionsGateway`
(`src/app/features/admin/transactions/services/admin-transactions-gateway.interface.ts`).
Impl: `AdminTransactionsLocalGateway` (injected concretely by
`AdminTransactionsFacade`). DTO: `AdminTransaction`
(`src/app/features/admin/transactions/models/admin-transaction.model.ts`).
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| T1 | GET | `{base}/backoffice/transactions?search&status&type&page&pageSize` | `loadTransactions(filters)` | `200 AdminTransactionsListResult` |
| T2 | POST | `{base}/backoffice/transactions/{id}/retry` | `retryFailed(id)` | `200 AdminTransaction` |
| T3 | PATCH | `{base}/backoffice/transactions/{id}/fraud-flag` | `setFraudFlag(id,flagged)` | `200 AdminTransaction` |
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`
(T1), `backoffice.write` (T2/T3).
- **T1 query params** (`AdminTransactionListFilters`): `search`, `status`
`all|pending|success|failed|retried`, `type``all|payment|refund|qr_payment`,
`page`, `pageSize`. Response `AdminTransactionsListResult { items, total, page,
pageSize }`. No sort param.
- **T2:** only meaningful for `status:'failed'` transactions (method name
`retryFailed`); a backend should reject retry on non-failed (proposed `409`).
- **T3 body:** `{ flagged: boolean }`.
- **`AdminTransaction`:** `{ id, orderId, orderNumber, type, method, status,
amount, currency, fraudFlag, audit: [{ action, actor, timestamp }], createdAt,
updatedAt }`. Transactions link to orders via `orderId` — how they relate to the
live payment records (§3.3.a QR/card) is **Requires backend decision** (the
live payment API and the admin transactions model are currently unconnected).
- **Validation:** none client-side.
- **Errors:** `404` (unknown id), `409` (retry on non-failed — proposed), common
set.
- **Example T3 request:** `PATCH …/transactions/tx_5/fraud-flag` body `{ "flagged": true }`
---
### 3.17 Reviews — storefront submission (LIVE) + admin moderation (MOCK-ONLY)
#### 3.17.a Storefront review/question submission — LIVE
Via `ApiService` (`ProductDataProvider.submitReview/submitQuestion`). Reviews and
questions are **read** by deriving them from the `GET /items/{id}` payload — there
are **no dedicated list endpoints** (`BACKEND-AUDIT.md` §11). Writes:
| # | Method | Path (LIVE literal) | Request | Success |
|---|---|---|---|---|
| R1 | POST | `{base}/items/{itemID}/callback` | `{ rating, comment, sessionID, timestamp }` | `200 { message }` |
| R2 | POST | `{base}/items/{itemID}/questiion` | `{ question, sessionID, timestamp }` | `200 { message }` |
- **Note the literal typo `questiion`** (R2) — matches the backend spec, must be
preserved (`api.service.ts` line 617).
- **Headers/JWT:** marketplace headers; customer `WebSessionID` identifies the
reviewer. No JWT.
- **Models:** `SubmitReviewInput`/`SubmitQuestionInput`
(`src/app/core/products/models/product-engagement.model.ts`). `rating` is
1-5 stars (`RatingStars`).
- **Validation:** none client-side. Rating range 1-5, non-empty comment, one
review per customer/product = Requires backend decision.
- **Errors:** `404` (unknown item), `422` (bad rating), `429` (spam throttle —
proposed, nothing client-side), common set.
- **Example R1 request:** `{ "rating": 5, "comment": "Great", "sessionID": "…", "timestamp": "2026-07-25T12:00:00Z" }`
#### 3.17.b Admin review moderation — PROPOSED (MOCK-ONLY, no seam)
Contract: `AdminModerationGateway`
(`src/app/features/admin/moderation/services/admin-moderation-gateway.interface.ts`).
Impl: `AdminModerationLocalGateway` (injected concretely by
`AdminModerationFacade`; reused by `AdminAnalyticsFacade`). DTO: `AdminReview`
(`src/app/features/admin/moderation/models/admin-review.model.ts`).
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| MR1 | GET | `{base}/backoffice/moderation/reviews?search&status&rating&page&pageSize` | `loadReviews(filters)` | `200 AdminReviewsListResult` |
| MR2 | GET | `{base}/backoffice/moderation/reviews/{id}` | `loadReview(id)` | `200 AdminReview` / `404` |
| MR3 | PATCH | `{base}/backoffice/moderation/reviews/{id}/status` | `setReviewStatus(id,status,note)` | `200 AdminReview` |
| MR4 | PATCH | `{base}/backoffice/moderation/reviews/{id}/visible` | `setReviewVisible(id,visible)` | `200 AdminReview` |
| MR5 | PATCH | `{base}/backoffice/moderation/reviews/{id}/pinned` | `setReviewPinned(id,pinned)` | `200 AdminReview` |
| MR6 | PATCH | `{base}/backoffice/moderation/reviews/{id}/featured` | `setReviewFeatured(id,featured)` | `200 AdminReview` |
| MR7 | POST | `{base}/backoffice/moderation/reviews/{id}/notes` | `addModeratorNote(id,note)` | `200 AdminReview` |
| MR8 | DELETE | `{base}/backoffice/moderation/reviews/{id}` | `deleteReview(id)` | `204` |
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`
(MR1/MR2), `backoffice.write` (MR3-MR8).
- **MR1 query params** (`AdminReviewListFilters`): `search`, `status`
`all|pending|approved|rejected|spam`, `rating``all` or a number 1-5, `page`,
`pageSize`. Response `AdminReviewsListResult { items, total, page, pageSize }`.
- **MR3 (approve/reject) body:** `{ status: AdminReviewStatus, note: string }`
where status ∈ `pending|approved|rejected|spam`. This is the approve/reject
action — approving = `status:'approved'`, rejecting = `'rejected'`, marking spam
= `'spam'`. The `note` is a moderator reason, appended to the review timeline.
- **`AdminReview`:** `{ id, productId, productName, customerName, customerEmail,
rating, text, photos[], status, visible, pinned, featured, reportCount,
moderatorNotes, timeline: [{ eventKey: 'submitted'|'statusChanged'|'restored'|
'hidden', actor: 'admin'|'customer', status?, note, timestamp }], createdAt,
updatedAt }`. Server appends timeline entries on each mutation.
- **Relation to storefront reviews (§3.17.a):** the admin `AdminReview` and the
storefront `Review` (`product-engagement.model.ts`) are different types over the
same underlying data. How a `POST /items/{id}/callback` submission surfaces in
the moderation queue = Requires backend decision (they are unconnected today).
- **Validation:** none client-side.
- **Errors:** `404` (unknown id), common set.
- **Example MR3 request:** `{ "status": "approved", "note": "Looks legit" }`
---
### 3.18 Reports — PROPOSED (MOCK-ONLY, no seam)
Part of `AdminModerationGateway` (same gateway/facade as reviews §3.17.b). DTO:
`AdminReport` (`src/app/features/admin/moderation/models/admin-report.model.ts`).
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| RP1 | GET | `{base}/backoffice/moderation/reports` | `loadReports()` | `200 AdminReport[]` |
| RP2 | PATCH | `{base}/backoffice/moderation/reports/{id}/status` | `setReportStatus(id,status)` | `200 AdminReport` |
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`
(RP1), `backoffice.write` (RP2).
- **RP1:** returns a **plain array, no pagination/filter params** (`loadReports()`
takes no arguments).
- **RP2 body:** `{ status: AdminReportStatus }` where status ∈
`open|resolved|dismissed`.
- **`AdminReport`:** `{ id, targetType: 'product'|'review'|'customer'|'category'
|'unknown', targetId, targetLabel, reason, reporterEmail, status, createdAt }`.
A report points at another entity via `targetType`+`targetId`.
- **No report *creation* endpoint in the frontend** — reports are read + status
changed only. Who/what creates reports (customer-facing "report this" flow) =
Requires backend decision (no such storefront submission exists in code).
- **Validation:** none client-side. **Errors:** `404` (RP2 unknown id), common set.
- **Example RP2 request:** `{ "status": "resolved" }`
---
### 3.19 Monitoring — PROPOSED, READ-ONLY (MOCK-ONLY, no seam)
Contract: `AdminMonitoringGateway`
(`src/app/features/admin/monitoring/services/admin-monitoring-gateway.interface.ts`).
Impl: `AdminMonitoringLocalGateway` (injected concretely by
`AdminMonitoringFacade`). **Read-only** — the gateway has no mutation methods
(`BACKEND-AUDIT.md` §14). DTOs in `admin-monitoring.model.ts`.
| # | Method | Path (PROPOSED) | Gateway method | Success |
|---|---|---|---|---|
| MO1 | GET | `{base}/backoffice/monitoring/events?category&search` | `loadEvents(filters)` | `200 AdminMonitoringEvent[]` |
| MO2 | GET | `{base}/backoffice/monitoring/queues` | `loadQueues()` | `200 AdminQueue[]` |
| MO3 | GET | `{base}/backoffice/monitoring/webhooks` | `loadWebhooks()` | `200 AdminWebhookDelivery[]` |
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`.
- **MO1 query params** (`AdminMonitoringEventFilters`): `category`
`all|audit|security|login|failed_login|api|error|warning`, `search`. **No
pagination** (plain array) — a backend feeding a real event stream would likely
need to add paging/time-range params; **Requires backend decision** on
windowing.
- **Models:** `AdminMonitoringEvent { id, category, level: 'info'|'warning'
|'error', message, technicalDetail?, actor, timestamp }`; `AdminQueue { name,
depth, status: 'healthy'|'degraded'|'down' }`; `AdminWebhookDelivery { id,
endpoint, event, status: 'delivered'|'failed'|'pending', timestamp }`.
- **Webhooks here are read-only observability** (delivery log), **not** webhook
configuration CRUD — there is no create/update/delete webhook endpoint in the
frontend. Webhook *management* = Requires backend decision (out of current scope).
- **Validation/errors:** read-only; common read error set.
- **Example MO2 response:** `[ { "name": "orders", "depth": 3, "status": "healthy" } ]`
---
### 3.20 Analytics — PROPOSED, DERIVED / NO REAL DATA SOURCE (MOCK-ONLY)
Per `BACKEND-AUDIT.md` §14: `AdminAnalyticsFacade` has **no gateway of its own**
it derives everything by reusing orders/products/moderation local gateways +
`ADMIN_CATEGORIES_GATEWAY` + `AdminDashboardFacade`. Several analytics values are
**honestly `null` because no data source exists** (e.g. `conversionRate`,
`retentionPercent` — model comments: "null = unknown - no visitor/traffic tracking
exists yet. Never fabricated"). DTOs in `admin-analytics.model.ts`.
| # | Method | Path (PROPOSED) | Purpose | Success |
|---|---|---|---|---|
| AN1 | GET | `{base}/backoffice/analytics/summary?range` | KPI summary | `200 AdminAnalyticsSummary` |
| AN2 | GET | `{base}/backoffice/analytics/products?range` | Product analytics | `200 AdminProductAnalytics` |
| AN3 | GET | `{base}/backoffice/analytics/customers?range` | Customer analytics | `200 AdminCustomerAnalytics` |
| AN4 | GET | `{base}/backoffice/analytics/series?range&metric` | Time series | `200 AdminAnalyticsSeriesPoint[]` |
- **Headers/JWT:** admin auth (§3.0). Permission (Proposed): `backoffice.read`.
- **`range` query param** ∈ `7 | 30 | 90` (`AdminAnalyticsDateRange`) — the only
filter the frontend models.
- **`AdminAnalyticsSummary`:** `{ revenueTotal, currency, ordersCount,
avgOrderValue, productsCount, categoriesCount, customersCount, conversionRate:
number | null }`. **`conversionRate` MUST be `null` when unknown** — a backend
must not fabricate it absent real visitor/traffic tracking. Same for
`AdminCustomerAnalytics.retentionPercent`.
- **What has no backend data source at all (mark accordingly):** conversion rate,
retention, and anything needing visitor/traffic/pageview data — there is **no
traffic analytics ingestion anywhere in the frontend**. Building real analytics
(beyond order/product aggregates) is **Requires backend decision** end to end.
- **Validation/errors:** read-only; common read set. A backend that can't compute
a metric should return `null`, not omit or fake it.
- **Example AN1 response:**
```json
{ "revenueTotal": 4820000, "currency": "RUB", "ordersCount": 213, "avgOrderValue": 22629, "productsCount": 512, "categoriesCount": 24, "customersCount": 178, "conversionRate": null }
```
**Dashboard metrics (adjacent).** `AdminDashboardMetricsGateway.loadMetrics()`
(`ADMIN_DASHBOARD_METRICS_GATEWAY`, MOCK-SWAPPABLE token) returns
`AdminDashboardMetrics { categoriesCount, productsCount }` — the one other
token-bound admin gateway. Proposed: `GET {base}/backoffice/dashboard/metrics`
`200 { categoriesCount, productsCount }`, permission `backoffice.read`.
---
### 3.21 Cross-cutting "Requires backend decision" register (this section)
Consolidated so the master doc can dedupe against the auth/error registers:
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.
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).
3. **Server-owned field lists** on create/update for products/orders/etc. (only
categories defines one, by stripping `id/itemsCount/deletedAt/createdAt/updatedAt`).
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.
5. **Product slug/sku uniqueness** — no proactive check exists (unlike categories'
`isSlugTaken`); decide `409` behavior.
6. **Order status-transition legality** and refund eligibility rules.
7. **Customers as a first-class resource** vs. derived-from-orders aggregate.
8. **`AdminRole` duplication** — auth string-union vs. users-management interface
(naming reconciliation; role CRUD / custom roles).
9. **Remote session revocation propagation** (`revokeSession`) to a logged-in
client (`AUTHENTICATION.md` §8 — no push/poll exists).
10. **Media**: allowed mime/size; whether in-use assets are delete-blocked.
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.
12. **Languages**: duplicate-add `409`, removing the default/last locale, BCP-47
validation.
13. **Transactions ↔ live payment records** are unconnected models; how they link.
14. **Reviews**: storefront `Review` vs. admin `AdminReview` linkage; how a
submitted review enters the moderation queue.
15. **Reports**: no creation flow exists — who creates reports.
16. **Monitoring**: event windowing/pagination for a real stream; webhook
*management* (vs. read-only delivery log) is out of scope.
17. **Analytics**: `conversionRate`/`retentionPercent` must stay `null` when
unknown; no traffic/visitor data source exists anywhere — real analytics is a
from-scratch backend build.
18. **Variants/options** shape reconciliation between storefront `variantOptions`
(grouped) and admin `variants` (flat priced rows); no dedicated variants
endpoint (created within the product body).
---
_All paths repo-relative to `F:\dx\remote\marketplaces\`. Storefront/category
literals and the `AdminCategoriesApiGateway` paths are verified in source; every
other endpoint is PROPOSED per the conventions in §3.0._
---
## 4. Authentication
Standalone, backend-implementable authentication contract for the marketplace
platform (Angular frontend, branch `B2B`). Derived directly from source —
`src/app/core/auth/**`, `src/app/core/admin-auth/**`,
`src/app/services/{auth,telegram-session-api}.service.ts`,
`src/app/components/telegram-login/**`, `src/app/guards/language.guard.ts`,
`src/app/app.routes.ts`, `src/app/app.config.ts`,
`src/app/core/config/tenant-resolver.service.ts` — plus
`docs/context/BACKEND-AUDIT.md` (this-session audit) and the prior
`docs/AUTH.md`. Where the frontend does not already imply a behavior, this
document says **"Requires backend decision"** rather than inventing one.
Two authentication mechanisms coexist in the codebase today, at different
maturity levels:
| Mechanism | Used by | Status |
|---|---|---|
| Telegram QR / deep-link session auth | Storefront customers **and** admin/backoffice (same API) | **LIVE** — real endpoints, in production use |
| Ed25519 challenge/response admin auth | Admin/backoffice (intended replacement) | **Frontend fully wired, backend endpoints do not exist yet** (404s today) |
Both are documented in full below. Nothing here should be read as "the
platform has JWTs today" — it does not, except inside the not-yet-live
Ed25519 flow.
---
### Table of contents
1. [Mechanism A — Telegram QR / session login (LIVE)](#1-mechanism-a--telegram-qr--session-login-live)
2. [Mechanism B — Ed25519 challenge/response admin auth (NOT LIVE)](#2-mechanism-b--ed25519-challengeresponse-admin-auth-not-live)
3. [JWT structure](#3-jwt-structure)
4. [Refresh token](#4-refresh-token)
5. [Token expiration handling](#5-token-expiration-handling)
6. [Token rotation](#6-token-rotation)
7. [Logout](#7-logout)
8. [Session invalidation](#8-session-invalidation)
9. [Role hierarchy](#9-role-hierarchy)
10. [Tenant isolation](#10-tenant-isolation)
11. [Permission model / route guards](#11-permission-model--route-guards)
12. [Open items — "Requires backend decision"](#12-open-items--requires-backend-decision)
---
### 1. Mechanism A — Telegram QR / session login (LIVE)
Single source for **both** customer and admin login:
`TelegramSessionApiService` (`src/app/services/telegram-session-api.service.ts`).
There is no separate admin backend endpoint — the same three calls back the
customer `AuthService` (`src/app/services/auth.service.ts`) and the admin
`AdminAuthService` (`src/app/core/admin-auth/admin-auth.service.ts`). Only the
**storage** differs (cookie name, in-memory signal), so an admin QR scan
never authenticates the customer session or vice versa.
#### 1.1 Endpoints (base = `environment.authApiUrl`, e.g. `https://api.dexarmarket.ru:445`)
| Method | Path | Request | Response |
|---|---|---|---|
| POST | `/users/sessions` | body `{ webSessionID }` (client-generated GUID), header `WebSessionID: <same guid>` | `{ webSessionID, url }``url` is the Telegram bot deep link |
| GET | `/users/sessions/{id}` | — | Session object, heavily field-tolerant (see §1.3) |
| DELETE | `/users/sessions/{id}` | header `WebSessionID: <id>` | ignored/discarded |
#### 1.2 Frontend-driven flow
The frontend, not the backend, generates the session id. Sequence:
1. User opens login (storefront "Sign in" or admin `/admin-login` gate).
2. Frontend generates a random GUID client-side (`generateGuid()`,
`src/app/shared/util/guid.util.ts`) — this **is** the `webSessionID`, sent
to the backend, not received from it.
3. `POST {authApiUrl}/users/sessions` with `{ webSessionID }` body and
`WebSessionID` header set to the same value. Backend response's own id
field is preferred if present (see `extractSessionId` — checks
`webSessionID/WebSessionID/webSessionId/sessionID/SessionID/sessionId/id/ID`
in that order), else the frontend's generated GUID is used as fallback.
4. Frontend builds two login links from the returned id:
- Web: `https://t.me/{bot}?start={webSessionID}` (`getBotLoginUrl`)
- App deep link: `tg://resolve?domain={bot}&start={webSessionID}`
(`getBotAppLoginUrl`)
- `bot` = `environment.telegramBot` (`'myAMLKYCBOT'` in current env config;
code fallback `'DexarSupport_bot'` if the env key is absent).
5. Frontend renders both as a QR code (external image generator
`https://api.qrserver.com/v1/create-qr-code/...` — not a backend of this
platform, purely a QR bitmap renderer for the `url`) plus the app deep
link for mobile. This is orchestrated by `QrLoginEngine`
(`src/app/shared/qr-login/qr-login.engine.ts`) shared identically by both
customer and admin modes via `TelegramLoginComponent`
(`src/app/components/telegram-login/telegram-login.component.ts`,
`[mode]="'customer' | 'admin'"`).
6. User scans the QR (or taps the deep link on mobile) and completes the
Telegram bot interaction out-of-band. The **backend** is expected to mark
that `webSessionID` as active/logged-in once the Telegram bot confirms the
user, associating a Telegram user identity with it.
7. Frontend polls `GET /users/sessions/{id}` (`checkSessionOnce`, driven by
`QrLoginEngine`'s polling loop) until the session normalizes to `active:
true`, or the user cancels/times out.
8. On an active session, the frontend calls `activateSession()` internally
(sets in-memory signal, stores the id per §1.4, schedules a re-check —
see §5) and redirects: customer → wherever the login was triggered from;
admin → `/{lang}/backoffice/dashboard` (hardcoded in
`telegram-login.component.ts`).
#### 1.3 Session response normalization (backend field tolerance)
`TelegramSessionApiService.normalizeWebSession()` is deliberately
tolerant of multiple backend field-naming conventions (evidence the backend
contract was never fully pinned down). A backend implementation can emit any
of these; the frontend reads the **first key found** in this priority order:
- **Active/status**: `status`, `Status`, `active`, `Active`, `loggedIn`,
`LoggedIn`, `isLoggedIn`, `IsLoggedIn`, `authenticated`, `Authenticated`.
Value is considered "active" if boolean `true`/`1`, or (case-insensitive)
one of `true, 1, active, authenticated, confirmed, success, logged_in`.
- **User object**: nested under `user`/`User`/`telegramUser`/`TelegramUser`,
else the top-level response object itself is used as the user record.
- **Username**: `username`/`Username` (user object first, then top-level).
- **First/last name**: `firstName`/`first_name`/`FirstName`/`First_name`,
`lastName`/`last_name`/`LastName`/`Last_name` — joined with a space if both
present.
- **Display name**: explicit `displayName`/`DisplayName`/`name`/`Name` (user
object or top-level) wins; else falls back to `username`; else falls back
to the joined full name; else literal `'Telegram User'`.
- **Telegram user id**: `userId`/`telegramUserId`/`telegramUserID`/
`TelegramUserID`/`id`/`ID` (user object), else `userId`/`telegramUserId`/
`telegramUserID`/`TelegramUserID`/`userID`/`UserID`/`UserId` (top-level).
- **Session id**: same priority list as `extractSessionId` above.
- **Expiry**: `expiresAt`/`ExpiresAt`/`expires`/`Expires` (ISO 8601 string);
if absent, the frontend fabricates `now + 3600s` client-side — **the
backend should always send a real `expiresAt`/`expires`** so the frontend's
refresh-scheduling (§5) reflects the true session lifetime rather than a
guessed one.
Normalized shape consumed by the frontend (`AuthSession`,
`src/app/models/auth.model.ts`):
```ts
interface AuthSession {
sessionId: string;
userId: number | null;
username: string | null;
displayName: string;
active: boolean;
expires: string; // ISO 8601
}
```
#### 1.4 Storage (customer vs admin — kept fully separate)
| | Customer (`AuthService`) | Admin (`AdminAuthService`) |
|---|---|---|
| Cookie name | `webSessionID` | `adminSessionID` |
| Cookie attrs | `Max-Age=3600; Path=/; SameSite=Lax` (+`Secure` over HTTPS) | `Max-Age=3600; Path=/; SameSite=Strict` (+`Secure` over HTTPS) |
| In-memory state | `sessionSignal`, `statusSignal` (`unknown\|checking\|authenticated\|expired\|unauthenticated`) | Same shape, separate signals |
| Extra storage | — | Reserved JWT-pair slots `localStorage['adminToken']` / `localStorage['adminRefreshToken']`**unused today**, see §12 |
Both send a `WebSessionID` header on every marketplace API request via
`apiHeadersInterceptor` (see `docs/context/BACKEND-AUDIT.md` §3) — this is
the anonymous-or-authenticated session identity the backend correlates
requests against; there is no `Authorization: Bearer` header in this
mechanism.
#### 1.5 Session re-check / soft refresh (not a token refresh)
Both `AuthService` and `AdminAuthService` self-schedule a re-check of
`GET /users/sessions/{id}` 60 seconds before `expires`, minimum 30s out
(`scheduleSessionRefresh`). This is **not** a refresh-token exchange — it
just re-polls the same session-status endpoint and re-activates if still
active, or clears local state if not. There is no rotation of the
`webSessionID` itself in this mechanism.
#### 1.6 Admin dev bypass (non-production only)
`AdminAuthService.devBypassLogin()` fabricates a local session
(`sessionId: 'dev-bypass-{timestamp}'`, `active: true`, 1-hour expiry) and
activates it directly, skipping the QR flow entirely. Guarded by
`environment.production` at runtime (not just build-time) — the checked
condition is inside the function body, so it is dead code in a production
build.
#### 1.7 Sequence diagram — storefront/admin Telegram-QR login
```mermaid
sequenceDiagram
participant User as User (browser)
participant FE as Frontend (AuthService / AdminAuthService)
participant BE as Backend (authApiUrl)
participant TG as Telegram bot
User->>FE: Open login (customer checkout, or /admin-login gate)
FE->>FE: generate webSessionID (client GUID)
FE->>BE: POST /users/sessions { webSessionID } (header WebSessionID)
BE-->>FE: 200 { webSessionID, ... }
FE->>FE: build QR + tg:// deep link from webSessionID
FE-->>User: render QR code / "Open in Telegram" button
User->>TG: scan QR / tap deep link, confirm in bot
TG->>BE: (out of band) associate webSessionID with Telegram user
loop poll every N seconds
FE->>BE: GET /users/sessions/{webSessionID}
BE-->>FE: session (active:false while pending)
end
BE-->>FE: session (active:true, user fields, expires)
FE->>FE: activateSession(): store cookie, set signals,<br/>schedule re-check at expires-60s
alt mode = admin
FE-->>User: redirect to /{lang}/backoffice/dashboard
else mode = customer
FE-->>User: close dialog, resume prior action (e.g. checkout)
end
```
---
### 2. Mechanism B — Ed25519 challenge/response admin auth (NOT LIVE)
**Status: frontend fully implemented and wired to real `HttpClient` calls;
the backend does not implement these endpoints yet — calls 404/error today.**
No route currently requires this flow (`ed25519AuthGuard` is not referenced
by any route in `app.routes.ts`; the live admin gate is still
`adminAuthGuard` / Telegram QR, §1). This is the target contract for closing
the security gap in §1: today the Telegram session API has no concept of
"admin," so the backend cannot distinguish an admin login attempt from a
customer one at the moment of login. Ed25519 closes that by requiring proof
of possession of a specific, pre-registered private key before any session
is issued.
#### 2.1 Key generation (device-local, once per device)
`Ed25519KeypairService` (`src/app/core/auth/services/ed25519-keypair.service.ts`):
- `getOrCreateKeyPair()`: generates a **non-extractable** Ed25519 keypair via
`crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify'])`
(real WebCrypto Ed25519 — RFC 8032, not a placeholder), persists the raw
`CryptoKey` handles in IndexedDB (`admin-auth-ed25519` DB, object store
`keypair`, single record `id: 'device-keypair'`).
- The private key is never exported, serialized, or transmitted — by
construction (`extractable: false`), not by convention or policy.
- `sign(message)`: signs a UTF-8-encoded string with `crypto.subtle.sign
('Ed25519', privateKey, ...)`, returns a base64-encoded signature.
- `clear()`: deletes the IndexedDB record ("forget this device"). A new
keypair generated after this requires re-registration with the backend
(§2.2) before it can complete a login.
- **Public key registration is explicitly out of scope for the frontend.**
An Owner/Administrator must associate a new device's `publicKeyBase64`
with an admin account through some out-of-band mechanism (backend admin
tool, one-time enrollment link, etc.) — not prescribed here (see §12).
#### 2.2 Login flow, step by step
Orchestrated by `AuthService.login()` (`src/app/core/auth/services/auth.service.ts`,
distinct from the customer/admin `AuthService` in §1 despite the identical
class name — different module, `core/auth/` vs `services/`):
1. `GET {authApiUrl}/api/admin/auth/challenge` → `AuthChallenge { nonce,
issuedAt, expiresAt }` (all ISO 8601 except `nonce`, an opaque string).
2. `Ed25519KeypairService.getOrCreateKeyPair()` (generates on first use).
3. `Ed25519KeypairService.sign(nonce)` — signs the **raw nonce string
exactly as received**, no additional framing/prefix/hashing applied
client-side.
4. `POST {authApiUrl}/api/admin/auth/verify` with body
`VerifySignatureRequest { publicKey, signature, nonce }` (`publicKey` =
base64 raw Ed25519 public key, `signature` = base64 signature over the
nonce, `nonce` = the same value echoed back).
5. Backend must: re-derive the exact signed message from the nonce it
issued, verify the signature against its own `publicKey → admin account`
mapping, confirm the nonce hasn't expired or been used before, and only
then issue tokens.
6. On success: `200 AuthTokenPair { token, refreshToken }`.
`SessionService.activate(tokens)` decodes the JWT (§3), stores both
tokens (§4), and schedules the next refresh (§5).
7. On failure: `401`/`403``AuthService` maps it through
`authErrorCodeFromStatus()` to `invalid-signature` (or a more specific
code — see §2.5) and the UI routes to
`/admin-login/error/invalid-signature`.
#### 2.3 API contracts (all under `{environment.authApiUrl}/api/admin/auth`)
| Method | Path | Request body | Response | Notes |
|---|---|---|---|---|
| GET | `/challenge` | — | `200 AuthChallenge` | `{ nonce, issuedAt, expiresAt }` |
| POST | `/verify` | `VerifySignatureRequest { publicKey, signature, nonce }` | `200 AuthTokenPair` \| `401` \| `403` | Issues `{ token, refreshToken }` |
| POST | `/refresh` | `RefreshTokenRequest { refreshToken }` | `200 AuthTokenPair` \| `401` | Rotation expected — see §6 |
| POST | `/logout` | `{ refreshToken }` | `204` (frontend clears local state regardless of response code/body) | Should revoke server-side |
Types: `src/app/core/auth/models/auth-api.model.ts`. HTTP client:
`src/app/core/auth/services/auth-api.service.ts` (`AuthApiService`) — thin
wrapper, no retries, no fabricated mock responses.
#### 2.4 Sequence diagram — admin login with Ed25519 signing
```mermaid
sequenceDiagram
participant Admin as Admin (browser)
participant FE as Frontend (AuthService, core/auth)
participant Key as Ed25519KeypairService (WebCrypto + IndexedDB)
participant BE as Backend
Admin->>FE: Click "Sign in"
FE->>BE: GET /api/admin/auth/challenge
BE-->>FE: 200 { nonce, issuedAt, expiresAt }
FE->>Key: getOrCreateKeyPair() (generate on first use, non-extractable)
Key-->>FE: { publicKeyBase64 }
FE->>Key: sign(nonce)
Key-->>FE: signature (base64)
FE->>BE: POST /api/admin/auth/verify { publicKey, signature, nonce }
alt signature valid & publicKey is a provisioned admin key & nonce fresh/unused
BE-->>FE: 200 { token, refreshToken }
FE->>FE: SessionService.activate(tokens):<br/>decode JWT claims, persist, schedule refresh
FE-->>Admin: redirect to /backoffice
else invalid signature / unknown key / expired or reused nonce
BE-->>FE: 401 / 403
FE-->>Admin: redirect to /admin-login/error/invalid-signature
end
```
#### 2.5 Error screens
Single component `AuthErrorPageComponent` at route `/admin-login/error/:code`
renders all five, keyed by route param. `authErrorCodeFromStatus()`
(`src/app/core/auth/models/auth-error.model.ts`) maps HTTP status → code:
`401→unauthorized`, `403→forbidden`, `0→backend-unavailable`,
`5xx→backend-unavailable`, else `unauthorized`.
| Code | Trigger | User action offered |
|---|---|---|
| `session-expired` | Refresh token rejected/expired | Sign in again |
| `invalid-signature` | `verify` returns 401/403 during login, or any client-side failure in the challenge→sign→verify chain that isn't a clearer HTTP-derived code | Try again |
| `unauthorized` | Route guard sees no active session | Sign in |
| `forbidden` | `permissionGuard` denies (authenticated but insufficient role) | Back to dashboard |
| `backend-unavailable` | Network error / 5xx / status 0 | Retry |
#### 2.6 Interceptor status — NOT registered
`src/app/core/auth/interceptors/auth.interceptor.ts` exists (adds
`Authorization: Bearer` + reactive 401-refresh-and-retry, see §5) but **is
not included** in `app.config.ts`'s `withInterceptors([...])` list today.
Confirmed in `app.config.ts`:
```
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor,
apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor])
```
`authInterceptor` is absent. Until it is registered, no request in the app
automatically attaches the Ed25519-flow JWT as a bearer token — this
confirms the mechanism is fully dormant, not partially live.
#### 2.7 Module map
```
src/app/core/auth/
├── auth.routes.ts # /admin-login, /admin-login/error/:code
├── models/
│ ├── auth-api.model.ts # AuthChallenge, VerifySignatureRequest, AuthTokenPair, JwtClaims
│ ├── auth-error.model.ts # AuthErrorCode, authErrorCodeFromStatus()
│ └── permission.model.ts # AdminRole, Permission, ROLE_PERMISSIONS
├── services/
│ ├── ed25519-keypair.service.ts # WebCrypto keygen/sign, IndexedDB persistence
│ ├── auth-api.service.ts # HttpClient calls to the 4 endpoints in §2.3
│ ├── jwt.service.ts # decode-only JWT parsing
│ ├── session.service.ts # token/claims state, persistence, refresh scheduling
│ ├── permission.service.ts # role -> permission set
│ ├── auth.service.ts # orchestrates challenge -> sign -> verify -> refresh -> logout
│ └── auth-facade.service.ts # public surface for components
├── interceptors/
│ └── auth.interceptor.ts # Authorization: Bearer + 401 refresh-and-retry (NOT registered, §2.6)
├── guards/
│ ├── ed25519-auth.guard.ts # requires SessionService.isAuthenticated() (not referenced by any route)
│ └── permission.guard.ts # permissionGuard(permission) factory
└── pages/
├── admin-login-page.component.* # sign-in UI
└── auth-error-page.component.* # parameterized error screen (§2.5)
```
`AuthFacade` (`src/app/core/auth/services/auth-facade.service.ts`) is the
only thing components/pages should depend on; `AuthService`/
`SessionService`/`PermissionService` are internal collaborators.
---
### 3. JWT structure
Only defined for Mechanism B (Ed25519 flow) — Mechanism A (§1) issues no JWT,
only an opaque session id. Expected claims
(`src/app/core/auth/models/auth-api.model.ts::JwtClaims`):
```ts
interface JwtClaims {
sub: string; // admin account id
role: AdminRole; // 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly'
iat: number; // seconds since epoch (standard `iat`)
exp: number; // seconds since epoch (standard `exp`)
publicKey: string; // the Ed25519 public key this token was issued for
}
```
`JwtService.decode()` (`src/app/core/auth/services/jwt.service.ts`) does
**decode-only** parsing (base64url payload → JSON), and validates only the
minimal shape: `sub` is a string, `role` is a string, `exp` is a number — if
any of these three checks fail, decoding returns `null` and the caller
(`SessionService`) discards the session as malformed.
The frontend never verifies the JWT signature — it has no trusted key to
check it against; that is exclusively the backend's job on every
subsequent admin request. A decoded-but-unverified claim is UX (role-gated
menus, expiry countdowns) — never proof of authorization to any client-side
check.
---
### 4. Refresh token
Defined only for Mechanism B. `AuthTokenPair { token, refreshToken }` is
returned by both `/verify` and `/refresh`. Storage
(`SessionService`, `src/app/core/auth/services/session.service.ts`):
- `localStorage['ed25519AdminToken']` — access token (JWT)
- `localStorage['ed25519AdminRefreshToken']` — refresh token (opaque to the
frontend; never decoded, only round-tripped)
Both are written together in `activate()` and cleared together in `clear()`.
There is no separate expiry tracked for the refresh token client-side —
the frontend only reacts to a `401` from `/refresh` (see §5/§6).
Separately, `AdminAuthService` (Mechanism A, Telegram) reserves
`localStorage['adminToken']` / `localStorage['adminRefreshToken']` with
`getAdminToken()`/`setAdminTokens()`/`clearAdminTokens()` methods —
**written by no code path today** ("reserved for once the backend issues
admin access/refresh tokens... unused until then," per the source comment).
These are a distinct, currently-dead pair of storage keys from the Ed25519
ones above; do not conflate them.
---
### 5. Token expiration handling
#### 5.1 Mechanism A (Telegram session) — expiry via re-poll
See §1.5. `expires` from the session payload drives a `setTimeout` at
`max(expiresMs - now - 60_000, 30_000)` that re-calls `GET /users/sessions/
{id}`; if the backend now reports inactive, local state is cleared to
`unauthenticated`. There is no interceptor-level reactive handling for this
mechanism — a 401/expired session surfaces only through the next explicit
`checkSessionOnce()` poll or session re-check timer, not a per-request
retry.
#### 5.2 Mechanism B (Ed25519/JWT) — proactive + reactive
`SessionService.scheduleRefresh(claims)`: computes
`refreshInMs = max(claims.exp*1000 - now - 60_000, 5_000)` and sets a timer.
When it fires, `AuthService.refresh()` runs automatically
(`session.onRefreshDue(callback)` wiring, set up once in `AuthService`'s
constructor to avoid a circular DI dependency between the two services).
`SessionService.restore()` (intended to run once at app bootstrap, from an
`APP_INITIALIZER` calling `AuthFacade.restoreSession()` — **not yet wired
into the bootstrap process today**, see §12): reads persisted tokens,
decodes claims, and either resumes with a scheduled refresh or marks
`expired` immediately without any network call, so a stale session is
caught before any component/guard runs.
`authInterceptor` (present in source, not registered — §2.6) is documented
as: catch a 401 on any admin-gated request → attempt one `refresh()`
retry the original request once on success → route to `session-expired` on
failure. Does not retry more than once; a second 401 after an
apparently-successful refresh is treated as a server-side problem, not a
transient race.
#### 5.3 Sequence diagram — token expiration / refresh (Ed25519 flow)
```mermaid
sequenceDiagram
participant FE as Frontend (SessionService)
participant IC as authInterceptor (not yet registered, §2.6)
participant BE as Backend
Note over FE: Timer fires ~60s before JWT exp
FE->>BE: POST /api/admin/auth/refresh { refreshToken }
alt refresh token still valid
BE-->>FE: 200 { token, refreshToken }
FE->>FE: activate(tokens) - reschedules next refresh
else refresh token expired/revoked
BE-->>FE: 401
FE->>FE: SessionService.markExpired()
FE-->>FE: route to /admin-login/error/session-expired
end
Note over IC: Reactive path - any 401 on an admin request<br/>(inactive until authInterceptor is registered)
IC->>BE: Admin API request (expired token)
BE-->>IC: 401
IC->>BE: POST /api/admin/auth/refresh (single retry)
alt refresh succeeds
BE-->>IC: 200 tokens
IC->>BE: retry original request with new token
else refresh fails
IC-->>FE: propagate error, route to session-expired
end
```
---
### 6. Token rotation
**Mechanism A**: no token to rotate — the `webSessionID` itself is stable
for the life of the session; expiry is handled by re-polling status (§5.1),
not by issuing a new id.
**Mechanism B**: rotation is *expected* by the frontend but not verifiable
until the backend exists. Per the source comment in `AuthApiService` and the
security notes in the prior `docs/AUTH.md`:
- Every `POST /refresh` response is expected to include a **new**
`refreshToken`; the backend should invalidate the one just used
(single-use refresh tokens).
- The frontend always stores whatever pair it receives from `/verify` or
`/refresh` and never reuses an old refresh token after a successful
rotation — there is no client-side retry logic that would resend a stale
refresh token.
- **Requires backend decision**: refresh-token reuse detection / revocation
cascade (e.g. if a rotated-out refresh token is presented again, should the
backend revoke the entire token family as a compromise signal?). Nothing
in the frontend implies or depends on this — it is a pure backend policy
choice.
---
### 7. Logout
**Mechanism A** (`AdminAuthService.logout()` / `AuthService.logout()` in
`src/app/services/auth.service.ts`): `DELETE /users/sessions/{id}` with
`WebSessionID` header, then unconditionally clears local state (cookie,
signals, timers) regardless of the HTTP result.
**Mechanism B** (`AuthService.logout()` in `src/app/core/auth/services/`):
clears `SessionService` state **immediately and unconditionally** (before
the network call resolves), then best-effort calls
`POST /api/admin/auth/logout { refreshToken }` if a refresh token was
present; any error from that call is swallowed (`catchError(() =>
throwError(() => null))`). If no refresh token exists locally, no network
call is made at all. **Backend implication**: the frontend cannot be relied
upon to reliably deliver the logout call (network failure, tab closed
mid-request, etc.) — server-side session/token expiry must not depend on a
client-issued logout ever arriving. `AuthFacade.logout()` additionally
always navigates to `/admin-login` (default) via `finalize()`, regardless of
API outcome.
#### 7.1 Sequence diagram — logout (both mechanisms)
```mermaid
sequenceDiagram
participant User
participant FE as Frontend
participant BE as Backend
User->>FE: Click "Log out"
FE->>FE: Clear local session state immediately<br/>(cookie / tokens / signals / timers)
alt Mechanism A (Telegram session)
FE->>BE: DELETE /users/sessions/{id} (header WebSessionID)
BE-->>FE: any response (ignored)
else Mechanism B (Ed25519/JWT) - only if a refresh token existed
FE->>BE: POST /api/admin/auth/logout { refreshToken }
BE-->>FE: 204 (or error, swallowed)
end
FE-->>User: redirect to login page
```
---
### 8. Session invalidation
Client-side triggers that clear local auth state, both mechanisms:
- Explicit logout (§7).
- Session status re-check (Mechanism A) returning `active: false` (§1.5).
- JWT decode failure on restore (Mechanism B) — a malformed/unparsable
stored token is treated as no session at all (`SessionService.restore()`
calls `clear()`).
- Refresh failure (Mechanism B) — any error from `/refresh` calls
`SessionService.markExpired()`.
- `AdminAuthService.clearAuthState()` also clears the reserved
`adminToken`/`adminRefreshToken` keys (§4) even though nothing currently
writes them, for forward-compatibility once Mechanism A gains a token
pair.
**Requires backend decision**: server-side session/token revocation
propagation — e.g., can an Owner revoke another admin's active session
remotely (relevant given `AdminUsersGateway.revokeSession` already exists as
a **mock-only** admin-users gateway method per
`docs/context/BACKEND-AUDIT.md` §14)? If so, the frontend has no push
mechanism (no websocket, no polling of "is my token still valid" beyond the
scheduled refresh) to learn about a remote revocation before its next
refresh/request attempt — a session could remain "authenticated" client-side
for up to the refresh interval after a backend-side revocation. If real-time
revocation is required, that is new frontend work, not something already
implied by existing code.
---
### 9. Role hierarchy
**Discrepancy flagged by the backend audit — `AdminRole` is defined twice
with different meanings. Reconciliation needed before backend
implementation:**
1. `src/app/core/auth/models/permission.model.ts` — a **string union** used
by the Ed25519/JWT flow's `role` claim and `PermissionService`:
```ts
type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
```
Ordered highest-to-lowest privilege by convention (not enforced in code —
`PermissionService` does not rely on ordering, only exact role → permission
set lookup).
2. `src/app/features/admin/users/models/admin-user.model.ts` — an
**interface** describing an admin-users-management row (`{ id, name, ...
}`), unrelated in shape to #1 and used only by the mock admin-users
gateway/facade (`AdminUsersGateway`, MOCK-ONLY, no backend seam per the
audit).
These two `AdminRole` symbols do not currently reference each other and are
imported from different modules by different features. A backend
implementer should treat #1 (the permission-model union) as the JWT/role
claim contract for auth purposes, and flag #2 for a naming rename (e.g.
`AdminUserRoleRecord`) rather than assuming they describe the same concept.
This document does not resolve the collision — it is called out so a human
reconciles it before building the backend role table.
#### 9.1 Permission-to-role mapping (from `ROLE_PERMISSIONS`)
| Role | Permissions |
|---|---|
| `Owner` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage`, `settings.manage` |
| `Administrator` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage` |
| `Editor` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write` |
| `Support` | `backoffice.read` |
| `ReadOnly` | `backoffice.read`, `builder.read` |
This is deliberately coarse and mirrors the bootstrap-level
`PermissionsConfig` shape (`src/app/shared/models/config/permissions.model.ts`).
Finer-grained, per-domain permissions (e.g. "can edit prices but not delete
products") do not exist anywhere client-side and stay server-side —
**Requires backend decision** if finer granularity is ever needed.
---
### 10. Tenant isolation
`TenantResolverService` (`src/app/core/config/tenant-resolver.service.ts`)
resolves tenant by **subdomain**, not by header or path prefix:
```ts
getTenantKey(): string {
if (isLocalhost()) return environment.fallbackTenantKey ?? 'default';
const segments = hostname.split('.').filter(Boolean);
if (segments.length === 0) return environment.fallbackTenantKey ?? 'default';
if (segments[0] === 'www' && segments.length > 1) return segments[1];
return segments[0];
}
```
- `isLocalhost()` matches `localhost`, `127.0.0.1`, `::1`.
- On a real host, the tenant key is the **first DNS label**, skipping a
leading `www`. E.g. `dexarmarket.api.dexarmarket.ru` → tenant key
`dexarmarket`; `www.acme.com``acme`.
- This tenant key feeds `ApiConfigService.getBaseUrl()`
(`src/app/core/config/api-config.service.ts`, documented in
`docs/context/BACKEND-AUDIT.md` §2) to pick the marketplace API base URL:
localhost → `environment.localhostApiUrl` (`/api`); else
`environment.tenantApiBaseUrls[tenantKey]`; else
`environment.tenantApiTemplate` with `{tenant}` substituted; else
(gated by `allowBootstrapApiOverride`, off by default) a value read out of
the already-loaded bootstrap document
(`bootstrap.apiEndpoints.website.baseUrl` /
`bootstrap.tenant.apiBaseUrl`).
- **No `X-Tenant` header or `/tenant/{id}/...` path prefix is sent by the
frontend anywhere** — tenant isolation for the marketplace API is achieved
purely by **which base URL/subdomain the request is sent to**, not by a
request attribute the backend reads per-call. Auth (both mechanisms) does
**not** carry any tenant identifier in its request bodies or headers
either — `POST /users/sessions`, `/api/admin/auth/challenge`, etc. are all
called against `environment.authApiUrl`, a single fixed origin, with no
per-tenant variation in the auth-flow code today.
- **Requires backend decision**: if auth (session creation, Ed25519
challenge/verify) must be tenant-scoped (e.g. an admin's Ed25519 public key
should only authorize them for one tenant's backoffice), the frontend
currently has no mechanism to communicate which tenant a login attempt is
for beyond whatever the backend can infer from the request's origin/
Referer header — nothing in the auth payloads carries a tenant id
explicitly. This would be new frontend work if required.
---
### 11. Permission model / route guards
#### 11.1 `adminAuthGuard` (live, Mechanism A) — `src/app/core/admin-auth/admin-auth.guard.ts`
```ts
export const adminAuthGuard: CanActivateFn = () => {
const adminAuth = inject(AdminAuthService);
if (adminAuth.isAuthenticated()) return true;
adminAuth.requestLogin();
return false;
};
```
Checks only `AdminAuthService.isAuthenticated()` (Telegram session status
`=== 'authenticated'`) — no role/permission check at all. Applied to
`/edit`, `/edit/:section`, and `/backoffice` (and its children) in
`app.routes.ts`. This guard **cannot** distinguish admin roles from each
other — it is purely "is there an active admin Telegram session," which is
the exact gap Mechanism B is meant to close.
#### 11.2 `ed25519AuthGuard` (dormant, Mechanism B) — `src/app/core/auth/guards/ed25519-auth.guard.ts`
Requires `SessionService.isAuthenticated()` (JWT status `=== 'authenticated'`).
Not referenced by any route in `app.routes.ts` today — confirmed by source
search. Exists purely as the cutover target (see §2's "not live" status).
#### 11.3 `permissionGuard(permission)` — `src/app/core/auth/guards/permission.guard.ts`
Factory guard that checks `PermissionService.has(permission)` against the
Mechanism B role → permission table (§9.1). Also unused by any live route
until Mechanism B is cut over, but ready to gate specific admin sub-routes
by permission once it is (e.g. `permissionGuard('users.manage')` on a users
page).
#### 11.4 `languageGuard` — `src/app/guards/language.guard.ts`
Not an authentication guard, but gates every localized route (`:lang`
segment wraps the entire route tree in `app.routes.ts`). Behavior:
- If `:lang` param is a known, enabled language: preload its translation
pack (`TranslateService.preloadLanguage`), set it as current
(`LanguageService.setLanguage`), allow navigation.
- If known but **disabled**: redirect to the current default language,
preserving the rest of the path.
- If unrecognized entirely: treat the URL as a legacy no-lang-prefix URL and
redirect to `/{defaultLang}{originalUrl}`, preserving query string/fragment
via `router.parseUrl` (not a hand-built `UrlTree`, to avoid double-encoding
the query string into the path segment).
#### 11.5 `canDeactivate` guards (dirty-state guards, not auth)
Also not authentication, but listed since the task asked for "what guards
check": `projectEditorDirtyGuard`, `adminProductDirtyGuard`,
`adminCategoryDirtyGuard` — all gate navigation *away* from an in-progress
editor (builder section, product editor, category editor) to warn about
unsaved changes. They read editor dirty-state signals, not auth state, and
are unrelated to session/token validity.
#### 11.6 What the frontend actually gates, summarized
| Concern | Mechanism | Guard/service |
|---|---|---|
| "Is there an active admin session at all" | Telegram (A) | `adminAuthGuard``AdminAuthService.isAuthenticated()` |
| "Is there an active admin JWT session" | Ed25519 (B), not live | `ed25519AuthGuard``SessionService.isAuthenticated()` |
| "Does this role have permission X" | Ed25519 (B), not live | `permissionGuard(permission)``PermissionService.has()` |
| "Is `:lang` valid/enabled" | n/a | `languageGuard` |
| "Unsaved editor changes" | n/a | `*DirtyGuard` (project editor / product / category) |
**Every one of these is a client-side UX gate only.** None of them are a
substitute for server-side authorization — the backend must independently
verify role/permission on every admin mutation regardless of what a route
guard decided, per the security note already present in the prior
`docs/AUTH.md` and repeated here: a passing client-side check is not proof
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:
- **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.
- **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.
- **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.
- **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.
- **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.
- **`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).
- **`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.
- **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.
- **`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.
---
## 5. Security
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.
### 5.1 Origin
The frontend calls a small, fixed set of origins, all declared in
`src/environments/environment.ts` / `environment.production.ts`:
| Origin | Source key | Used for |
|---|---|---|
| `https://api.dexarmarket.ru:445` | `tenantApiBaseUrls.default`, `authApiUrl`, and prod `apiUrl`/`localhostApiUrl` | marketplace API + session auth + Ed25519 admin auth |
| `https://{tenant}.api.dexarmarket.ru:445` | `tenantApiTemplate` | per-tenant marketplace API (subdomain-substituted) |
| `https://qr.vitanova.network/api` | `qrApiUrl` | payment / QR / card status |
| `/api` (relative) | dev `apiUrl`/`localhostApiUrl` | localhost, proxied by nginx per tenant |
| `https://api.qrserver.com/...` | literal in `ApiService` | QR **bitmap** image only (not a platform backend) |
| `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
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
CORS headers (§5.2). `ip-api.com` is called over plaintext HTTP — a mixed-content
and privacy concern flagged here; the frontend already tolerates its failure
(falls back to 6 hardcoded regions).
### 5.2 CORS
**Bearer/header-only, never credentialed.** No `HttpClient` call in the
codebase sets `withCredentials: true` (grep: zero matches in `src/`). Identity
travels in **headers**, not cookies:
- `WebSessionID` on every marketplace API request (`apiHeadersInterceptor`).
- `AdminWebSessionID` + `Authorization: Bearer <token>` on `/admin/`,
`/backoffice/`, `/builder/`, `/media/` requests (`adminAuthHeadersInterceptor`).
- `authorization-key` / `userid-value` on QR payment calls.
The auth **cookies** (`webSessionID`, `adminSessionID`) are same-site,
first-party, and are **not** sent cross-origin to the API — they exist only so
the SPA can restore its own session locally. **Backend requirement:** the API
must accept these requests as CORS with `Access-Control-Allow-Origin` set to
the specific SPA origin(s) and `Access-Control-Allow-Headers` including
`WebSessionID, AdminWebSessionID, Authorization, X-Region, X-Language, Currency`
(and the QR API's `authorization-key, userid-value`). Because no request is
credentialed, `Access-Control-Allow-Credentials` is **not** required and
`Allow-Origin` may be specific per tenant.
### 5.3 CSRF
**Not applicable to the API surface, by construction.** All API authentication
is bearer/header-based (§5.2) with **no cookie ever transmitted to the API**
and `withCredentials` never set. A CSRF attack relies on the browser
auto-attaching an ambient credential (cookie) to a forged cross-site request;
since the API authenticates on a header the attacker's page cannot set
cross-origin, a classic CSRF POST cannot forge an authenticated call. There is
zero CSRF-token handling in the frontend (no XSRF interceptor, no
`X-CSRF-Token`/`X-XSRF-TOKEN` reference anywhere in `src/`), and none is
needed for the current design. **Backend note:** this guarantee holds only as
long as the API does **not** start accepting a cookie as an auth credential —
if a future endpoint authenticates via cookie, CSRF protection becomes
mandatory. Requires backend decision only if that design change is made.
### 5.4 JWT
Cross-reference `docs/AUTHENTICATION.md` §3§4. Security-relevant storage
angle only:
- **Live today (Telegram session, Mechanism A):** no JWT at all — an opaque
`webSessionID` in a cookie + in-memory signal. The reserved
`localStorage['adminToken']` / `adminRefreshToken` slots are **dead** (nothing
writes them).
- **Ed25519 flow (Mechanism B, dormant):** access + refresh tokens live in
`localStorage` (`ed25519AdminToken`, `ed25519AdminRefreshToken`).
**XSS exposure implication:** `localStorage` is readable by any script in the
origin, so a single XSS foothold (see §5.8) exfiltrates the admin token pair
outright — there is no `HttpOnly` cookie protecting them. This is the
standard SPA trade-off and is acceptable **only** if the XSS surface (§5.8)
is genuinely closed server-side. **Requires backend decision:** whether admin
tokens should instead be delivered as `HttpOnly; Secure; SameSite` cookies
to remove the XSS-exfiltration path — the frontend currently assumes
`localStorage`, so this would be a coordinated frontend+backend change.
### 5.5 Refresh
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`
§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
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
does, however, apply **debounce** on user-driven request bursts, which implies
the natural request cadence a backend limiter should tolerate rather than block:
- Search-as-you-type: `debounceTime(220)` in `SearchFacade`
(`src/app/features/search/facade/search.facade.ts:98`) — so a typing user
produces at most ~45 `GET /searchitems` calls/second, not one per keystroke.
- Project-editor autosave: ~300 ms debounce before a draft commit
(localStorage only, not a backend call today).
**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
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
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;
`ed25519-keypair.service.ts`). The `VerifySignatureRequest` body is
`{ publicKey, signature, nonce }` and nothing else. Therefore **all** replay
protection is the backend's responsibility via the nonce it issues:
`AuthChallenge { nonce, issuedAt, expiresAt }`. **Backend requirement:** treat
each nonce as single-use, bind it to the issuing client, reject it after
`expiresAt`, and reject any reuse. The frontend contributes no independent
freshness signal — there is no client timestamp or per-request nonce on any
other call either (the Telegram session flow has no signing at all).
### 5.8 XSS
The frontend renders **backend/CMS-controlled HTML** in three places — this is
the single most important input for what the backend must sanitize:
| Location | File | Handling today |
|---|---|---|
| Storefront static page body (CMS content) | `src/app/pages/static-page/static-page.component.ts:104-106` | Runs `DomSanitizer.sanitize(SecurityContext.HTML, html)` **first**, then `bypassSecurityTrustHtml(sanitized)`. So it is Angular-sanitized before injection into `[innerHTML]`. |
| Static-page **preview** (editor) | `src/app/features/content-management/components/static-page-preview/static-page-preview.component.ts:49` | `bypassSecurityTrustHtml(sanitized)` after a sanitize pass. |
| Payment bank-frame URL | `src/app/pages/cart/cart.component.ts:475` | `bypassSecurityTrustResourceUrl(bankUrl)` — trusts a backend-supplied iframe URL. |
The static-page path is the risky one: CMS body HTML originates from the
bootstrap document (`BootstrapConfig.staticPages`, backend-served). It **is**
run through Angular's sanitizer client-side, but Angular sanitization strips
active content for the DOM context only — it is **not** a substitute for
server-side sanitization/storage validation. **Backend requirement:** treat all
CMS static-page HTML and any product-description HTML as untrusted on write,
sanitize/allowlist server-side, and never assume the client sanitizer is the
only defense. The `bypassSecurityTrustResourceUrl` on the bank payment frame
means the backend-supplied payment URL must be strictly validated server-side
(a malicious value renders in an iframe with `ResourceUrl` trust). Product
descriptions are currently normalized to plain strings by
`ApiService.normalizeItem` and are **not** rendered via `[innerHTML]` today, so
they are lower-risk — but that holds only while descriptions stay plain-text.
Note: the project-editor HTML editor (`marketplace-html-editor.component.ts`)
uses `innerHTML` on a `contenteditable` surface for authoring; that is
author-local editor state, not backend-rendered content, so it is out of the
backend's sanitization scope.
### 5.9 CSP
**No CSP meta tag in `src/index.html`** (confirmed — only SEO/OG/Twitter/theme
meta tags are present). CSP is delivered **at the web-server layer** via
`nginx.conf`, and only on the primary tenant block:
- `dexarmarket.ru` server block (`nginx.conf:41`) sets a full
`Content-Security-Policy`:
`default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;`
plus `X-Frame-Options: SAMEORIGIN`, `X-Content-Type-Options: nosniff`,
`X-XSS-Protection: 1; mode=block`, `Referrer-Policy: strict-origin-when-cross-origin`,
`Permissions-Policy: camera=(), microphone=(), geolocation=()`.
- **The `lovero.store` block and the new-tenant template block set the other
security headers but omit the `Content-Security-Policy` header entirely** —
an inconsistency the backend/ops team should reconcile so every tenant gets
the same CSP.
**Backend/ops observations:** the current CSP is permissive — `script-src`
allows `'unsafe-inline'` and `'unsafe-eval'`, `connect-src 'self' https:`
allows connections to any HTTPS origin, and `img-src` allows any `https:` and
`data:`. This weakens the XSS defense-in-depth that would otherwise back up
§5.8. **Requires backend decision:** whether to tighten the CSP (drop
`unsafe-inline`/`unsafe-eval`, pin `connect-src` to the known API origins in
§5.1) and whether to apply it uniformly across all tenant server blocks.
### 5.10 Upload validation (client-side, in the media picker/library)
The only upload path is `MockMediaRepository.upload()`
(`src/app/core/media/mock-media-repository.service.ts`). Before storing, it
runs `validateFile(file)`:
- **Size:** rejects if `file.size > MAX_FILE_SIZE_BYTES` where
`MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024` (**10 MB**, line 9).
- **Type:** rejects if `file.type` is not in `ALLOWED_MIME_TYPES`
(line 12) — see §5.12 for the exact list.
It then transforms before storage: raster images are downscaled to
`MAX_IMAGE_DIMENSION = 2000` px on the longest edge at `COMPRESS_QUALITY = 0.85`
(canvas re-encode to JPEG/PNG); GIFs pass through untouched; **SVGs are
sanitized** (§5.11). Validation is keyed on `file.type` (the browser-reported
MIME), which is trivially spoofable — see §5.11. The `<input type=file>` in the
media picker does not set an `accept` attribute filtering the OS picker, so the
MIME check is the only gate.
### 5.11 Media validation (backend-side expectations)
Because the client checks above run in the browser and can be bypassed
(direct API call, forged `Content-Type`, renamed file), the backend must
**re-validate everything independently**:
- **Re-derive the real content type** from magic bytes / content sniffing, not
the client-declared MIME or filename extension. `assetKind()` client-side
keys purely off the declared MIME string.
- **Re-enforce the size limit** server-side (10 MB is the frontend's number,
§5.10 / §5.12 — the backend may set its own, but must enforce one).
- **Re-sanitize SVGs.** The frontend's `sanitizeSvg()` only strips `<script>`
elements and `on*="..."` inline event-handler attributes via regex
(lines 124-131). This is **naive** — it misses `<foreignObject>`,
`javascript:` URIs in `href`/`xlink:href`, CSS-based vectors, and namespaced
handlers. SVG is served with `image/svg+xml` and can execute script when
loaded as a document, so the backend must run a real SVG sanitizer/allowlist,
never trust the client's regex pass.
- **Strip/normalize metadata** (EXIF, embedded payloads) on ingest.
- **Serve uploaded assets from an isolated origin / with `Content-Disposition`
and `X-Content-Type-Options: nosniff`** so a malicious file can't be
interpreted as active content on the app origin.
### 5.12 Maximum file sizes
Exact client-enforced constant: **10 MB**
`MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024`
(`mock-media-repository.service.ts:9`). The user-facing error is
`"File exceeds the 10MB limit."`. Related transform constants (not rejection
limits): `MAX_IMAGE_DIMENSION = 2000` px, `COMPRESS_QUALITY = 0.85`. No
per-account or aggregate storage quota is enforced client-side — see §7 Limits.
### 5.13 Mime validation
Exact client-accepted MIME allowlist
(`ALLOWED_MIME_TYPES`, `mock-media-repository.service.ts:12`):
```
image/jpeg, image/png, image/webp, image/gif, image/svg+xml, application/pdf
```
These map to the `MediaAssetKind` union (`image` | `svg` | `pdf` | `other`)
via `assetKind()`. The media picker's type filter exposes only
`image / svg / pdf` to the user. No extension allowlist exists separately — the
check is MIME-string only. The backend allowlist should mirror this set (and
re-derive type from content, §5.11).
### 5.14 Audit logging
The admin **Monitoring** feature defines an event taxonomy the backend is
expected to populate. `AdminMonitoringCategory`
(`src/app/features/admin/monitoring/models/admin-monitoring.model.ts:1`) is:
```
'audit' | 'security' | 'login' | 'failed_login' | 'api' | 'error' | 'warning'
```
Each `AdminMonitoringEvent` carries `{ id, category, level (info|warning|error),
message, technicalDetail?, actor, timestamp }`. The page filters events by these
categories (`admin-monitoring-page.component.ts:26`). This is **MOCK-ONLY today**
(no backend seam — see `docs/context/BACKEND-AUDIT.md` §14), but it defines the
**event types the backend audit log must eventually emit** for the UI to be
meaningful:
- `audit` — admin actions taken (create/update/delete on any admin domain).
- `security` — security-relevant events (permission changes, revocations).
- `login` / `failed_login` — successful and failed authentication attempts,
with an `actor` identity.
- `api` / `error` / `warning` — request-level diagnostics.
Additionally, `AdminUserAuditEntry`
(`src/app/features/admin/users/models/admin-user.model.ts:43`) implies a
per-user audit trail of `roleChanged` / `statusChanged` events with `actor` and
`timestamp`. **Backend requirement:** every admin mutation and auth event must
be recorded with actor + timestamp so these two audit surfaces have real data;
each event needs a stable `category`/`eventKey`, a human `message`, and an
optional `technicalDetail`. **Requires backend decision:** retention window,
whether failed-login events capture source IP/device (the `AdminSession` model
carries `device`/`ip`, so the frontend already anticipates it), and the exact
action→category mapping.
### 5.15 Permission matrix (role × domain × actions)
**Critical finding first:** the **live** admin gate does **no role check at
all**. `adminAuthGuard` (`src/app/core/admin-auth/admin-auth.guard.ts`) only
verifies `AdminAuthService.isAuthenticated()` — a binary "is there an active
Telegram admin session." No admin component conditionally shows/hides actions
by the current user's role (grep of `src/app/features/admin`: the `permission`/
`role` matches are all about **managing other users as data** — role labels,
the users-management `AdminRole` record, audit entries — never gating the
current operator's own UI). So today, any authenticated admin can invoke every
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
`Owner | Administrator | Editor | Support | ReadOnly`; permission domains are
`backoffice`, `builder`, `users`, `settings`:
| Role | backoffice.read | backoffice.write | builder.read | builder.write | users.manage | settings.manage |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| **Owner** | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Administrator** | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| **Editor** | ✅ | ✅ | ✅ | ✅ | — | — |
| **Support** | ✅ | — | — | — | — | — |
| **ReadOnly** | ✅ | — | ✅ | — | — | — |
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 —
there is no per-domain granularity (e.g. "edit prices but not delete products")
anywhere client-side.
**Backend requirements / decisions:**
- The backend **must** independently authorize every admin mutation against the
authenticated role — a passing client guard is never proof of authorization.
- **Requires backend decision:** finer-grained per-domain permissions if ever
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
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.
---
---
## 6. Error Model
Single unified error-response format the backend must return for every non-2xx
response across all API surfaces (marketplace API, payment/QR API, session
auth API, Ed25519 admin auth API, and any future admin/builder/backoffice
APIs). Derived by cross-referencing every place the Angular frontend
currently parses, catches, or reacts to an HTTP error — see
`docs/context/BACKEND-AUDIT.md` for the full backend-surface audit this is
based on.
**Finding: the frontend does not currently parse any backend error envelope.**
No `HttpInterceptor` in the pipeline (`src/app/app.config.ts`
`mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor,
adminAuthHeadersInterceptor, cacheInterceptor`) inspects error responses —
all five only touch outgoing requests or successful GET caching. Every
consumer that reacts to failure does so on the RxJS/`HttpErrorResponse`
level (`error.status`, `error.message`), never on a parsed JSON error body.
The one exception is the Ed25519 admin-auth flow, which has a client-side
`AuthErrorCode` union but (see §"Known frontend gap" below) currently derives
it from **HTTP status only**, not from any body field. Because of this, the
envelope below is a **clean proposal, not a reverse-engineered contract**
every shape decision is marked accordingly.
### The envelope
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "b3f1c2a0-4e21-4d3a-9e77-1e8f6a2d9c11",
"details": [
{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." }
]
}
}
```
**Requires backend decision: adopt this envelope.** The frontend has no
existing opinion to preserve (no code reads `error.error.code` today), so
this is a recommendation, chosen to be consistent with the shapes the
frontend *does* already have opinions about:
- Top-level `{ code, message, status }` mirrors the existing `AuthError`
interface (`src/app/core/auth/models/auth-error.model.ts:13-18`) almost
field-for-field — reusing that shape means the Ed25519 auth module can
parse the new envelope with only a `status` fallback removed, not a
rewrite.
- `details[]` entries `{ field, code, message }` mirror the existing
client-side `ProjectValidationIssue` convention (`code, message, section,
fieldKey, severity` — `src/app/features/project-editor/services/
project-validator.service.ts:23-36`, consumed via `ProjectEditorFacade
.fieldError(fieldKey)`). No backend field-error shape exists to preserve
today (admin CRUD is 100% local/mock — see BACKEND-AUDIT.md §14), so this
is the closest existing frontend convention to align a real one to.
- `requestId` is new (no frontend code reads it yet) — recommended so
support/ops can correlate a user-visible failure to server logs. If
adopted, the frontend would need a small addition to surface it in
error-state UI (not present today).
Field notes:
| Field | Required | Notes |
|---|---|---|
| `error.code` | yes | Stable, machine-readable, `UPPER_SNAKE_CASE`. Never localized. This is what the frontend should branch on, not `message`. |
| `error.message` | yes | Human-readable fallback (English), safe to show only when the frontend has no i18n mapping for `code`. Never the sole signal for UI branching. |
| `error.status` | yes | Must equal the HTTP status of the response (redundant with the transport layer, but the frontend's own `AuthError.status` already carries this, so keep parity). |
| `error.requestId` | recommended | Opaque correlation id, echoed in logs. |
| `error.details` | only for 422 | Array of field-level issues, see §422 below. |
---
### Status-by-status contract
#### 401 — Unauthenticated / expired token
```json
{
"error": {
"code": "UNAUTHENTICATED",
"message": "Authentication is required to access this resource.",
"status": 401,
"requestId": "…"
}
}
```
**Frontend reaction today:**
- **Admin Ed25519 flow** (`AuthService.login()`/`refresh()` in
`src/app/core/auth/services/auth.service.ts`): any `HttpErrorResponse` with
status 401 is mapped via `authErrorCodeFromStatus()` → `AuthErrorCode
'unauthorized'`, surfaced by `AuthErrorPageComponent`
(`src/app/core/auth/pages/auth-error-page.component.ts`) with copy
"Unauthorized… Sign in" and a button that calls `router.navigateByUrl
('/admin-login')`.
- **Customer Telegram session auth** (`TelegramSessionApiService`,
`AuthService` customer-facing, `src/app/services/auth.service.ts`): no
code branches on a 401 status anywhere — session validity is instead
polled via `checkSessionOnce()` returning `AuthSession | null`. **Requires
backend decision**: whether/how a mid-session 401 on a customer-facing
marketplace call (e.g. `POST /cart`, `POST /orders`) should be surfaced —
today it would fall through to each caller's generic `catchError`/`error:`
handler (if any) with no unified "session expired, please re-auth" UX.
- **Admin backoffice CRUD (products/orders/users/etc.)**: these facades
(`AdminUsersFacade`, `AdminOrdersFacade`, …) currently only ever talk to
local/mock gateways, so no real 401 has ever reached them. Their existing
generic `error` boolean signal + `common.errorTitle`/`common.errorDescription`
+ retry button (see "Generic list-page error UI" below) is the pattern a
real 401 would fall into **unless** the facades are updated to branch on
status — they don't today.
#### 403 — Forbidden (wrong role or tenant)
```json
{
"error": {
"code": "FORBIDDEN",
"message": "Your account does not have permission to perform this action.",
"status": 403,
"requestId": "…"
}
}
```
**Frontend reaction today:** Ed25519 admin flow only. `authErrorCodeFromStatus(403)`
`'forbidden'``AuthErrorPageComponent` copy "Forbidden… Back to
dashboard", button `router.navigateByUrl('/backoffice')`. No tenant-scoping
distinction exists in this code path — a 403 caused by wrong role and a 403
caused by wrong tenant render identical copy today. **Requires backend
decision**: if tenant-mismatch should be visually distinct from
role-mismatch, it needs its own `error.code` (e.g. `TENANT_FORBIDDEN` vs
`ROLE_FORBIDDEN`) since the frontend has no other signal to key off besides
status today.
#### 404 — Not found
```json
{
"error": {
"code": "NOT_FOUND",
"message": "The requested item could not be found.",
"status": 404,
"requestId": "…"
}
}
```
**Frontend reaction today:** No code path distinguishes 404 from any other
failure. `catalog-container.component.ts` and
`product-details-container.component.ts` both catch *any* load error and
render the same generic `catalog.errorTitle`/`productDetails.errorTitle`
empty-state (`en.ts:176,277`) — a real 404 (product deleted) and a 500
(server crash) look identical to the user today. **Requires backend
decision**: whether the frontend should be enhanced to show a distinct
"this product no longer exists" message for 404 specifically (would need a
status/code check added to those two containers — not present now).
#### 409 — Conflict
```json
{
"error": {
"code": "CONFLICT",
"message": "A category with this slug already exists.",
"status": 409,
"requestId": "…"
}
}
```
**Frontend reaction today:** no code catches or branches on 409 anywhere.
The one related concept in the codebase is `AdminCategoriesGateway
.isSlugTaken(slug, excludingId)` (BACKEND-AUDIT.md §14) — a **proactive**
pre-check call the frontend makes *before* submitting, not a reaction to a
409 conflict response. **Requires backend decision**: whether create/update
endpoints should also return 409 on the same slug/uniqueness conflict as a
race-condition backstop, and whether the frontend should add a 409 handler
that surfaces `error.details` inline (there is no such handler today —
`isSlugTaken` is the only existing conflict-avoidance mechanism, and it is
best-effort/TOCTOU-prone).
#### 422 — Validation failure
```json
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"status": 422,
"requestId": "…",
"details": [
{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." },
{ "field": "price", "code": "OUT_OF_RANGE", "message": "Price must be greater than 0." }
]
}
}
```
**Frontend reaction today:** no admin form currently parses a backend
validation-error body — all admin CRUD is local/mock (BACKEND-AUDIT.md §14),
so there has never been a real 422 to react to. The frontend **does** have
an established field-error UI convention worth preserving: `ProjectEditorFacade
.fieldError(fieldKey): string | null`
(`src/app/features/project-editor/facade/project-editor.facade.ts:371-374`)
reads from `issuesByField` (a `Map<fieldKey, ProjectValidationIssue[]>`) and
returns the first issue's `message`, for inline per-field template binding.
That mechanism is entirely client-side validation today (`ProjectValidator`
service), not backend-driven. **Requires backend decision**: adopting
`details[].field` as the join key would let a future `fieldError()`-style
adapter merge backend 422 errors into the same inline-error UI pattern
without inventing a second one — but the adapter itself does not exist yet
and would need to be built.
#### 429 — Rate limited
```json
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Please slow down.",
"status": 429,
"requestId": "…",
"retryAfterSeconds": 30
}
}
```
**Frontend reaction today: none whatsoever.** No interceptor, facade, or
component in the codebase references `429` or "rate limit" in any form (grepped
across `src/`). **Requires backend decision** on every aspect:
- Whether the backend sends a `Retry-After` HTTP header, a body field
(`retryAfterSeconds` above), or both.
- Whether the frontend should retry automatically (with backoff) or only
show the user a "please wait Ns" message. Recommend: since no retry
interceptor exists today, add one is a new build item, not a config
change.
#### 500 — Server error
```json
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred. Please try again.",
"status": 500,
"requestId": "…"
}
}
```
**Frontend reaction today:** falls into whichever generic catch-all a given
caller has:
- Ed25519 admin flow: `authErrorCodeFromStatus()` default branch → `status
>= 500 ? 'backend-unavailable' : 'unauthorized'` → same
"Backend unavailable… Retry" screen as a network-down 503 (see below) —
the frontend does not distinguish "server is up but this request 500'd"
from "server is completely unreachable."
- Admin list pages (`AdminUsersFacade` and siblings): generic `error`
boolean signal set to `true` in the RxJS `error:` callback, rendering
`common.errorTitle`/`common.errorDescription` + a retry button that
re-invokes the same load call. No status differentiation.
- Storefront catalog/product pages: same generic empty-state pattern as 404
above.
- `LocationService.getRegions()`-equivalent: falls back silently to 6
hardcoded regions on *any* error (including 500), no user-visible error at
all (`src/app/services/location.service.ts`).
#### 503 — Maintenance / unavailable
```json
{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "The service is temporarily unavailable. Please try again shortly.",
"status": 503,
"requestId": "…"
}
}
```
**Frontend reaction today:** Ed25519 admin flow only, via the same
`status >= 500` branch as 500 above → `'backend-unavailable'`
`AuthErrorPageComponent` "Backend unavailable… Retry." No other code path
reacts to 503 specifically today (marketplace API calls that 503 would just
fall into each caller's generic error handling, same as 500 above).
**Distinguishing signal from Maintenance mode (see next section):** use
`error.code`, not the HTTP status. A plain infra 503 (database down,
overload) should send `"code": "SERVICE_UNAVAILABLE"`; a deliberate
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.
#### Maintenance mode
Same HTTP status as above (503), distinguished purely by `error.code`:
```json
{
"error": {
"code": "MAINTENANCE_MODE",
"message": "This marketplace is temporarily down for maintenance.",
"status": 503,
"requestId": "…",
"maintenanceUntil": "2026-07-26T04:00:00Z"
}
}
```
`maintenanceUntil` (ISO 8601, optional) lets the maintenance-mode UX (sibling
doc) show an ETA if the backend has one. **Requires backend decision:**
whether `maintenanceUntil` is populated reliably enough to promise in UI, or
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
should treat it as building from scratch, not preserving anything.
#### Tenant disabled
```json
{
"error": {
"code": "TENANT_DISABLED",
"message": "This marketplace is not currently active.",
"status": 403,
"requestId": "…"
}
}
```
**Frontend reaction today: none.** Tenant resolution
(`TenantResolverService`, `src/app/core/config/tenant-resolver.service.ts`)
only ever resolves *which* tenant a request targets (via host/subdomain); no
code path in the audited surface handles a backend telling the frontend
"this tenant exists but is disabled." **Requires backend decision** end to
end: status code (403 recommended, to reuse the existing `forbidden`
auth-error screen plumbing, vs. a dedicated status), and whether this should
route to a dedicated "tenant disabled" screen or reuse
`AuthErrorPageComponent`'s `forbidden` copy (which currently says "Your
account role does not have permission" — wrong wording for a
tenant-disabled scenario, would need a new `AuthErrorCode` entry and copy if
reused).
#### Rate limit
See **429** above — same contract, called out separately here only because
the task list asked for it as its own row. No additional distinguishing
signal needed beyond the 429 status + `RATE_LIMITED` code.
#### Expired token
```json
{
"error": {
"code": "TOKEN_EXPIRED",
"message": "Your session has expired. Please sign in again.",
"status": 401,
"requestId": "…"
}
}
```
**Frontend reaction today — known gap, read carefully:** `AuthService
.refresh()` (`src/app/core/auth/services/auth.service.ts:65-76`) has a
client-side `AuthErrorCode` value `'session-expired'` and passes it as
`fallbackCode` into `handleAuthError()`. **However**, `toAuthErrorShape()`
(lines 110-118) only uses `fallbackCode` when the caught error is **not** an
`HttpErrorResponse` — for an actual HTTP error it always calls
`authErrorCodeFromStatus(error.status)`, which maps 401 → `'unauthorized'`,
never `'session-expired'`, regardless of `fallbackCode`. So today, a real
backend 401 on `/refresh` renders the **generic "Unauthorized" screen**, not
"Session expired" — the "Session expired" screen is only ever reached via
the *no-refresh-token-present* client-side branch (line 67-70), never from a
real HTTP response. **Requires backend decision + frontend fix**: for a
distinct "your session expired, please sign in again" screen to actually
render on a real backend 401, either (a) the backend returns a body
`error.code: "TOKEN_EXPIRED"` and the frontend is updated to read it instead
of relying solely on `authErrorCodeFromStatus(status)`, or (b) this
distinction is accepted as unreachable today and left as future work. Flag
this gap explicitly to whoever picks up the fix — it is a pre-existing bug,
not something this contract can silently paper over.
#### Invalid signature
```json
{
"error": {
"code": "INVALID_SIGNATURE",
"message": "The signed challenge could not be verified.",
"status": 401,
"requestId": "…"
}
}
```
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.
**Frontend reaction today:** same gap as "Expired token" above.
`AuthService.login()` passes `fallbackCode: 'invalid-signature'` into
`handleAuthError()`, but `toAuthErrorShape()` discards it for any real
`HttpErrorResponse` and maps a 401 from `/verify` to the generic
`'unauthorized'` screen via `authErrorCodeFromStatus()`. The dedicated
"Invalid signature… Try again" screen
(`src/app/core/auth/pages/auth-error-page.component.ts:21-25`) exists in the
copy table but is **currently unreachable from a real backend response** for
the same reason as `session-expired` above. **Requires backend decision +
frontend fix**: backend must send a body-level `error.code:
"INVALID_SIGNATURE"` and the frontend's `toAuthErrorShape()` must be updated
to prefer a body code over the status-only mapping, or this screen stays
dead code reachable only via non-HTTP error paths.
---
### Generic list-page error UI (for reference)
Every admin backoffice list page (`AdminUsersFacade`, `AdminOrdersFacade`,
`AdminMonitoringFacade`, `AdminModerationFacade`, `AdminTransactionsFacade`,
`AdminProductsFacade`, `AdminCategoriesFacade`, `AdminAnalyticsFacade`,
`AdminCustomersFacade`, `AdminDashboardFacade`) follows the same shape,
added by RC-02 (`e153a67 fix(backoffice): add error+retry states to Users,
Monitoring, Analytics, Reports`):
```ts
readonly error = signal(false);
// on load:
error: () => { this.items.set([]); this.loading.set(false); this.error.set(true); }
```
```html
@else if (facade.error()) {
<app-empty-state [title]="'common.errorTitle' | translate" [description]="'common.errorDescription' | translate">
<span slot="actions">
<app-button variant="primary" (click)="facade.loadAll()">{{ 'common.retry' | translate }}</app-button>
</span>
</app-empty-state>
}
```
This is a **boolean** error flag — it does not branch on HTTP status or
`error.code` today. Every status in this contract (401/403/404/409/422/429/
500/503) would currently collapse into the same generic "Something went
wrong / retry" UI on these pages **unless** the facades are individually
updated to inspect `error.code`/`status` and branch — none do today. Wiring
that up is out of scope for this document (it defines the wire contract);
flagging it here so whoever wires real backends into these facades knows
the current ceiling of frontend error UX is "generic retry," not
per-status handling, except in the Ed25519 admin-auth module.
---
### Summary: "Requires backend decision" items
- **Envelope adoption** — the `{ error: { code, message, status, requestId,
details? } }` shape itself; no frontend code parses any envelope today.
- **401 on customer-facing marketplace calls** (`/cart`, `/orders`, etc.) —
no unified "session expired, please re-auth" UX exists for the customer
Telegram-session flow.
- **403 tenant-mismatch vs role-mismatch** distinct copy/code.
- **404 vs generic-error distinct UX** on catalog/product pages (currently
identical).
- **409 conflict handling on submit** (today only a proactive
`isSlugTaken` pre-check exists; no reactive 409 handler).
- **422 `details[]` → inline field-error adapter** for admin forms (the
client-side `fieldError()` convention exists but nothing feeds it from a
backend response yet).
- **429 rate-limit contract end to end** — header vs body, retry-after
value, and whether the frontend auto-retries (nothing exists today).
- **Maintenance-mode `maintenanceUntil` reliability** — advisory only, or
can the frontend promise an ETA.
- **Tenant-disabled status code and screen** — reuse `forbidden` copy (wrong
wording today) vs. add a dedicated `AuthErrorCode`.
- **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
from HTTP status only and ignores the `fallbackCode` for real HTTP errors,
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.
---
## 7. Uploads
Full media-upload lifecycle contract, derived from the actual media stack:
`MediaRepository` (abstract token), `MockMediaRepository` (the only impl),
`MediaLibraryFacade`, `MediaPickerComponent`, `MediaUsageService`, and the
`MediaAsset` model. **Structural status:** media is MOCK-SWAPPABLE — bound via
`{ provide: MediaRepository, useClass: MockMediaRepository }` in `app.config.ts`,
so a real HTTP repository drops in behind the same abstract class with no UI
change. **No real HTTP media backend exists yet**, and **no literal media
endpoint path exists anywhere in code** — every path below is **PROPOSED** to
satisfy the existing `MediaRepository` method shapes. The admin-auth interceptor
already attaches `AdminWebSessionID` + `Authorization: Bearer` to any `/media/`
URL (`docs/context/BACKEND-AUDIT.md` §3), so the backend can expect those headers.
### 7.1 The `MediaAsset` contract (real interface)
`src/app/core/media/models/media-asset.model.ts`:
```ts
export type MediaAssetKind = 'image' | 'svg' | 'pdf' | 'other';
export interface MediaAsset {
id: string;
url: string;
thumbnailUrl?: string;
filename: string;
mimeType: string;
size: number; // bytes
width?: number;
height?: number;
altText?: Record<string, string>; // locale -> alt text
caption?: string; // plain, non-localized
description?: string;
decorative?: boolean; // suppresses missing-alt-text warning
tags?: string[];
folder?: string;
createdAt: string; // ISO 8601
}
export interface MediaListResult { items: MediaAsset[]; total: number; }
export interface MediaListParams {
page?: number; pageSize?: number; search?: string;
folder?: string; tag?: string; kind?: MediaAssetKind; sort?: MediaSort;
}
export type MediaSort = 'recent' | 'name' | 'size';
export interface MediaUploadOptions { folder?: string; tags?: string[]; }
```
The abstract `MediaRepository` (the exact backend contract) is:
```ts
list(params?: MediaListParams): Promise<MediaListResult>;
upload(file: File, options?: MediaUploadOptions): Promise<MediaAsset>;
remove(id: string): Promise<void>;
update(id, patch: Partial<Pick<MediaAsset,
'altText'|'tags'|'folder'|'caption'|'description'|'decorative'>>): Promise<MediaAsset>;
listFolders(): Promise<string[]>;
```
### 7.2 Media upload
- **Method / path (PROPOSED):** `POST /media` (or `/media/upload`).
- **Content type:** `multipart/form-data` — the frontend holds a raw `File`
(`MediaLibraryFacade.upload(file, options)` → `repository.upload(file,
{ folder, ...options })`). The natural wire form is a multipart body with the
binary file plus `folder` and `tags[]` fields from `MediaUploadOptions`.
- **Headers:** `AdminWebSessionID` + `Authorization: Bearer <token>` (auto-added
for `/media/` by `adminAuthHeadersInterceptor`), plus multipart boundary.
- **Request:** the file blob + optional `folder: string` and `tags: string[]`.
The current folder is injected by the facade when none is passed.
- **Response:** a single fully-populated `MediaAsset` (§7.1). The backend
assigns `id`, canonical `url`, `thumbnailUrl`, `size` (post-processing),
`width`/`height`, and `createdAt`.
- **Client pre-processing to be aware of:** the mock downscales images to
2000 px and re-encodes at quality 0.85 before "upload." A real backend
receives whatever the browser sends; it should **not** assume the client
compressed anything (§5.11) and must do its own validation and any canonical
resizing.
- **Retry:** the facade keeps the last failed `File` and exposes
`retryUpload()` / `canRetry()`, and `cancelUpload()` uses an upload-token
guard to ignore a stale in-flight result. So the backend should make upload
**idempotent-safe** on retry (a retried upload after a client-perceived
failure may duplicate) — **Requires backend decision** on dedup strategy.
### 7.3 Replace
**No distinct "replace" operation exists.** `MediaRepository` has no `replace`
method; `update()` only patches metadata (`altText, tags, folder, caption,
description, decorative`), never the binary. Changing an asset's actual file =
`remove(id)` + `upload(newFile)`, producing a **new `id` and new `url`**.
**Backend implication:** references to the old asset URL (tracked by
`MediaUsageService`, §7.5) are **not** auto-updated on replace — a true
in-place replace that preserves `id`/`url` would be new frontend+backend work.
**Requires backend decision** if in-place replace is wanted.
### 7.4 Delete (single + bulk)
- **Single:** `remove(id): Promise<void>`**PROPOSED** `DELETE /media/{id}`.
`MediaLibraryFacade.remove(id)` also deselects and reloads.
- **Bulk:** `MediaLibraryFacade.bulkDelete(ids[])` loops `repository.remove(id)`
**one call per id** (no batch endpoint) — so bulk delete today is N ×
`DELETE /media/{id}`. The confirm-dialog / selection flow (RC-02) gates this
in the UI; the backend just sees serial deletes. **Requires backend decision:**
whether to add a batch `DELETE /media` (body `{ ids: [] }`) to avoid N
round-trips — the facade would need a matching `bulkRemove` method (not
present today).
- There is also `bulkMoveToFolder(ids, folder)` = N × `update(id, { folder })`.
- **Hard vs soft delete: Requires backend decision** — the frontend calls
`remove` and expects the asset gone from `list()`; it has no restore/trash
concept for media (unlike admin orders/categories which do have
archive/restore).
### 7.5 Metadata
`MediaAsset` carries this metadata (all editable via `update()` except the
system-assigned fields):
| Field | Editable by frontend? | Notes |
|---|---|---|
| `altText: Record<string,string>` | yes | **Per-locale** alt text (locale key → string). |
| `caption` | yes | Plain, non-localized. |
| `description` | yes | Plain. |
| `decorative: boolean` | yes | Suppresses the missing-alt warning. |
| `tags: string[]` | yes | Free-form; drives the tag filter. |
| `folder: string` | yes | Single folder; `listFolders()` derives the folder list. |
| `width` / `height` | no (system) | Set from image dimensions at upload. |
| `size` | no (system) | Bytes, post-processing. |
| `mimeType` | no (system) | |
| `filename` | no (system) | Original filename. |
| `createdAt` | no (system) | ISO 8601 upload timestamp. |
There is **no `uploader`/`updatedAt`/usage-count field** on `MediaAsset`.
Usage is computed **client-side on demand** by `MediaUsageService.findUsages(bootstrap, url)`
(`src/app/core/media/media-usage.service.ts`) — it walks the in-memory
`BootstrapConfig` looking for the asset's `url` as a string value and reports
where it is referenced (branding, theme, header, footer, static pages, etc.).
So "usage tracking" is a read-time scan of the config, **not** a stored
reverse-index. **Requires backend decision:** whether the backend should track
uploader identity and a stored usage/reference count (the frontend does not
require either today, but `metadata`-export and audit (§5.14) would benefit).
**Metadata update endpoint (PROPOSED):** `PATCH /media/{id}` with the
`update()` patch body, returning the updated `MediaAsset`.
### 7.6 Image variants
The frontend **does not use responsive image variants**. Zero `srcset`/`sizes`
usage in Angular templates (the only `srcset`-ish hit is unrelated). Images are
consumed as a **single `url`**, with an optional single `thumbnailUrl`. In the
mock, `thumbnailUrl === url` (same object URL) — there is no distinct thumbnail
rendition today. **Backend implication:** the contract is single-`url` +
optional single-`thumbnailUrl`; the frontend will happily consume a
backend-generated thumbnail if `thumbnailUrl` differs from `url`, but it never
requests a specific size or a `srcset` set. Adding true responsive variants
would be new frontend work.
### 7.7 Thumbnail generation
**Requires backend decision** on strategy — nothing in the frontend prescribes
sizes or formats. But the frontend gives a concrete **display target** so the
backend has a real number to aim at:
- The media library / picker renders a **grid** whose cells are ~150 px wide
(`MediaPickerComponent.onGridKeydown` computes columns as
`clientWidth / 150`), page size **24** items.
- A `thumbnailUrl` sized around **~150300 px** (accounting for 2× DPI) would
serve the grid without shipping full-resolution originals.
If the backend populates `thumbnailUrl` with such a rendition, the UI uses it
automatically (falls back to `url` when absent). No other thumbnail sizes are
consumed anywhere.
### 7.8 Limits
- **Per-file size:** 10 MB, client-enforced (§5.12) — the backend must
re-enforce (§5.11).
- **MIME allowlist:** the 6 types in §5.13.
- **Upload count / batch size:** no client cap — `bulkDelete`/`bulkMoveToFolder`
loop over arbitrary-length id arrays; single-file upload only (the picker's
`<input>` takes `files?.[0]`, so **one file per upload call**).
- **Storage quota (per tenant / per account):** none client-side —
**Requires backend decision**.
- **Pagination:** `pageSize` default **24** (`DEFAULT_PAGE_SIZE`/`PAGE_SIZE`);
`list()` returns `{ items, total }` so the backend must return an accurate
`total` for the pager.
### 7.9 Future S3 compatibility
The `MediaAsset`/`MediaRepository` contract is deliberately **storage-agnostic**
and this is a **design constraint for the backend, not a frontend requirement**:
the frontend only ever sees opaque `url` / `thumbnailUrl` strings and an opaque
`id`. It never constructs storage paths, never assumes a host, and never signs
or negotiates storage directly. Therefore the backend is free to back media
with S3, a CDN, local disk, or anything else — including returning
pre-signed/CDN URLs in `url`/`thumbnailUrl` — with **no frontend change**, as
long as:
- `upload()` still returns a complete `MediaAsset` synchronously with a usable
`url` (a direct-to-S3 pre-signed **upload** flow, where the client PUTs to S3
itself, is **not** what the current `repository.upload(file)` shape expects —
it passes the `File` to the repository and awaits the finished asset;
switching to browser-direct-to-S3 would be new frontend work). **Requires
backend decision** if a pre-signed direct-upload flow is desired.
- Served asset URLs are treated as untrusted-origin content (§5.11) —
isolated origin, `nosniff`, correct `Content-Type`/`Content-Disposition`.
---
## 8. Real Backend Implementation Guide
This section is the practical, top-to-bottom guide a backend engineer follows to replace the
frontend's mock/local data layer with real endpoints. It is derived entirely from
`docs/context/BACKEND-AUDIT.md` (the exhaustive source audit) — no business logic is invented
beyond what the frontend gateway interfaces already fix. Where the frontend has no seam or no
mapper today, that gap is stated explicitly rather than papered over.
The core principle, already proven for Categories, is: **the gateway/provider _interface_ is the
contract; a real implementation only has to satisfy that interface.** You never rewrite the
interface, and — if the domain already has a DI-token seam — you never touch the facade or the
UI. The only structural surprise, called out repeatedly below, is that **most admin domains do
not have that seam yet** (audit §1, §14, §25 #1): their facade injects the concrete
`Admin*LocalGateway` class directly, so a token must be introduced before a real gateway can be
bound at all.
### 8.1 Which frontend gateways must be replaced
Every data boundary the audit found, with its swap status. "DI token status" is the single most
important column — it tells you whether you can drop in a real class (`already token-bound`) or
must first introduce a seam (`needs token added first`).
| Gateway / provider interface | Current mock/local impl file | DI token status | Real impl class to write | Consuming facade(s) |
|---|---|---|---|---|
| `ConfigProvider` | `core/bootstrap/providers/mock-bootstrap.provider.ts` | already token-bound (`CONFIG_PROVIDER`) | **already exists & LIVE**`ApiBootstrapProvider` (`GET /bootstrap`) | UiRuntime, WebsiteRuntime, ProjectEditor, ContentManagement, Diagnostics (via `ConfigService`) |
| `ProductDataProvider` | none bound (mock via `mockDataInterceptor` only) | already token-bound (`PRODUCT_DATA_PROVIDER`) | **already exists & LIVE**`ApiProductDataProvider` | `ProductFacade` |
| `CategoryRepository` | none bound (mock via interceptor) | already token-bound (`CATEGORY_REPOSITORY`) | **already exists & LIVE**`ApiCategoryRepository` (`GET /category`) | `CategoryFacade` |
| `BackofficeDataProvider` | `core/backoffice/providers/mock-backoffice-data.provider.ts` | already token-bound (`BACKOFFICE_DATA_PROVIDER`) | **already exists & LIVE**`ApiBackofficeDataProvider` (`/api/backoffice/*`) | storefront cards |
| `AdminCategoriesGateway` | `features/admin/categories/services/admin-categories-local.gateway.ts` | already token-bound (`ADMIN_CATEGORIES_GATEWAY`) | **already exists & LIVE**`AdminCategoriesApiGateway` | `AdminCategoriesFacade`, `AdminAnalyticsFacade` |
| `AdminDashboardMetricsGateway` | `features/admin/dashboard/services/admin-dashboard-metrics.local.gateway.ts` | already token-bound (`ADMIN_DASHBOARD_METRICS_GATEWAY`) | `AdminDashboardMetricsApiGateway` (new) | `AdminDashboardFacade` |
| `MediaRepository` (abstract class) | `core/media/mock-media-repository.service.ts` | already token-bound (abstract-class token in `app.config.ts`) | `ApiMediaRepository` (new) | `MediaLibraryFacade` |
| `UserExperienceRepository` | `core/user-experience/repositories/local-user-experience.repository.ts` | already token-bound (`USER_EXPERIENCE_REPOSITORY`) | `AuthenticatedUserExperienceRepository` (new; see §8.3 note on redesign) | `UserExperienceFacade` |
| `AdminOrdersGateway` | `features/admin/orders/services/admin-orders-local.gateway.ts` | **needs token added first** | `AdminOrdersApiGateway` (new) | `AdminOrdersFacade`, `AdminCustomersFacade`, `AdminAnalyticsFacade` |
| `AdminProductsGateway` | `features/admin/products/services/admin-products-local.gateway.ts` | **needs token added first** | `AdminProductsApiGateway` (new) | `AdminProductsFacade`, `AdminAnalyticsFacade` |
| `AdminUsersGateway` | `features/admin/users/services/admin-users-local.gateway.ts` | **needs token added first** | `AdminUsersApiGateway` (new) | `AdminUsersFacade` |
| `AdminTransactionsGateway` | `features/admin/transactions/services/admin-transactions-local.gateway.ts` | **needs token added first** | `AdminTransactionsApiGateway` (new) | `AdminTransactionsFacade` |
| `AdminMonitoringGateway` | `features/admin/monitoring/services/admin-monitoring-local.gateway.ts` | **needs token added first** | `AdminMonitoringApiGateway` (new) | `AdminMonitoringFacade` |
| `AdminModerationGateway` | `features/admin/moderation/services/admin-moderation-local.gateway.ts` | **needs token added first** | `AdminModerationApiGateway` (new) | `AdminModerationFacade`, `AdminAnalyticsFacade` |
| (Customers) | reuses `AdminOrdersLocalGateway` (derived) | **needs token added first** (inherits Orders seam) | none of its own — derive from `AdminOrdersApiGateway`, or add a `/customers` source | `AdminCustomersFacade` |
| (Analytics) | reuses orders/products/moderation local + `ADMIN_CATEGORIES_GATEWAY` + `AdminDashboardFacade` | partial (categories/dashboard token-bound; rest not) | no aggregation endpoint exists — see §8.3 | `AdminAnalyticsFacade` |
| Auth session (`TelegramSessionApiService`) | mock via `mockDataInterceptor` | n/a (concrete service, LIVE) | **already exists & LIVE** — no swap; implement server endpoints only | `AuthService`, `AdminAuthService`, `AuthFacade` |
| Ed25519 admin auth (`AuthApiService`) | none | n/a (concrete service, LIVE wiring) | **client wiring exists; backend absent** — implement `/api/admin/auth/*` server-side | `AuthService` (Ed25519 flow) |
| `SearchHistoryRepository` | localStorage impl (concrete) | injected concretely — LOCAL-ONLY by design | — (leave local; no backend) | `SearchFacade` (via `SearchHistoryService`) |
Read this table as three tiers:
1. **Done / LIVE (no gateway work):** ConfigProvider, ProductDataProvider, CategoryRepository,
BackofficeDataProvider, AdminCategories. These already talk to real HTTP through a real
`*Api*` class. The backend job here is only to _stand up the server endpoints_ these clients
already call — not to touch frontend code.
2. **Token-bound, real impl missing:** DashboardMetrics, Media, UserExperience. The seam exists;
write the `*Api*` class and bind it. No facade edit.
3. **No seam yet (the bulk of admin CRUD):** Orders, Products, Users, Transactions, Monitoring,
Moderation (plus derived Customers/Analytics). Each needs a token introduced _and_ a facade
injection change _before_ a real gateway can even be bound.
### 8.2 Interfaces that stay unchanged (do not touch)
The gateway/provider interface files _are_ the frozen seam. A real implementation satisfies the
existing TypeScript interface; changing an interface is a frontend contract change and forces UI
churn. **Do not edit any of these** while wiring a backend:
- `core/config/config-provider.interface.ts` (`ConfigProvider`)
- `core/products/providers/product-data-provider.interface.ts` (`ProductDataProvider`)
- `core/categories/repositories/category.repository.ts` (`CategoryRepository`)
- `core/backoffice/providers/backoffice-data-provider.interface.ts` (`BackofficeDataProvider`)
- `core/media/media-repository.ts` (abstract class `MediaRepository`)
- `core/user-experience/repositories/user-experience.repository.ts` (`UserExperienceRepository`)
- `features/admin/categories/services/admin-categories-gateway.interface.ts` (`AdminCategoriesGateway`)
- `features/admin/dashboard/services/admin-dashboard-metrics.gateway.interface.ts` (`AdminDashboardMetricsGateway`)
- `features/admin/orders/services/admin-orders-gateway.interface.ts` (`AdminOrdersGateway`)
- `features/admin/products/services/admin-products-gateway.interface.ts` (`AdminProductsGateway`)
- `features/admin/users/services/admin-users-gateway.interface.ts` (`AdminUsersGateway`)
- `features/admin/transactions/services/admin-transactions-gateway.interface.ts` (`AdminTransactionsGateway`)
- `features/admin/monitoring/services/admin-monitoring-gateway.interface.ts` (`AdminMonitoringGateway`)
- `features/admin/moderation/services/admin-moderation-gateway.interface.ts` (`AdminModerationGateway`)
The interface method lists (the shapes the backend must satisfy) are enumerated in audit §14 and
must not drift. If the backend genuinely cannot meet a method's shape, that is a conversation to
have with the frontend owner and change the interface deliberately — not something to work around
inside a mapper.
### 8.3 Facades: which stay unchanged, which need a small adjustment
The facade is the abstraction the components consume. **If a domain already has a DI-token seam
and the real gateway honors the interface, its facade needs zero changes.** That covers all of
tier 1 and tier 2 above (ConfigProvider/Product/Category/Backoffice/AdminCategories/Dashboard/
Media/UserExperience facades are all injection-by-token already).
Facades that **do** need a (small, mechanical) change:
- **`AdminOrdersFacade`, `AdminProductsFacade`, `AdminUsersFacade`, `AdminTransactionsFacade`,
`AdminMonitoringFacade`, `AdminModerationFacade`** — each currently injects the concrete
`Admin*LocalGateway` class directly (audit §14, §21). The one-line change is to inject the new
DI token instead of the concrete class. This is unavoidable for these six because the seam does
not exist yet; it is the "add a token seam" half of the migration, not a behavior change.
Facades whose current behavior is **mock-shaped** and needs more than a wiring swap:
- **`AdminCustomersFacade`** (`features/admin/customers/facade/…`) derives its customer list by
reading `AdminOrdersLocalGateway` in memory (audit §14, §21). Once orders are real, it can keep
deriving customers from the Orders _token_, but if the backend exposes a first-class customers
source, prefer that. Either way the "derive from local orders" assumption is mock-specific and
should be revisited when Orders goes real.
- **`AdminAnalyticsFacade`** (`features/admin/analytics/facade/…`) composes orders + products +
moderation local gateways plus the categories/dashboard tokens (audit §14, §21). There is **no
analytics aggregation endpoint and no tracking pipeline today** (audit §14; remaining-work
#13/#17). Real analytics is not a gateway swap — it needs a data source that does not exist yet.
Treat it as the last domain, dependent on orders/products/moderation all being real first.
- **`UserExperienceFacade`** (`facades/platform/user-experience.facade.ts`) stores fully
denormalized wishlist/compare objects synchronously in localStorage (audit §19). The backend
decision (remaining-work #16) is id-only sync + a `GET /items/batch?ids=` hydration endpoint,
which requires redesigning the repository to store id-arrays and hydrate from a local product
cache — a real refactor, not a drop-in `*Api*` class. Flagged so nobody assumes the token swap
alone finishes wishlist sync.
- **`ContentManagementFacade`** and **`ProjectEditorFacade`** have **no backend call at all**
today (audit §16, §17). They read/mutate the in-memory `BootstrapConfig` and persist drafts to
localStorage; "publishing a marketplace" = writing bootstrap back, for which **no client write
call exists**. Wiring these to a real backend means _adding_ a draft/publish HTTP path
(`ProjectEditorIoService` currently only does JSON.stringify/parse; drafts live in
`ProjectEditorDraftStorageService` / localStorage), not swapping a gateway.
- **Cart** (`CartService`) is local-only by design (localStorage `marketplace_cart`); checkout
already emits real `POST /cart` / `POST /orders` calls (audit §10). No facade change — just be
aware cart _contents_ never round-trip to a backend.
Also note the mock gateways commonly apply an **artificial delay** and keep state in
localStorage/in-memory overlays (audit §14). A real gateway should drop the fake latency; the
facades tolerate real async already (they're signal/Observable-based), so no facade edit is needed
for that specifically.
### 8.4 DTO mapping examples (worked)
The frontend already carries adapters for its LIVE domains; a backend engineer should treat those
adapters as the **tolerance contract** for the wire shape. Three representative cases:
#### Example A — Categories (clean stack, mapper already exists)
Real backend JSON (`GET /category`, shape the client already tolerates):
```json
[
{
"categoryID": 12,
"names": [ { "lang": "ru", "name": "Электроника" }, { "lang": "en", "name": "Electronics" } ],
"subcategories": [ { "categoryID": 34, "names": [ { "lang": "en", "name": "Phones" } ] } ]
}
]
```
Frontend wire DTO: `CategoryDto`, `CategoryNameDto` (`core/categories/dto/category.dto.ts`).
Domain model: `Category`, `CategoryTranslation` (`core/categories/models/category-domain.model.ts`).
Bridge: **`CategoryMapper`** (`core/categories/mappers/category.mapper.ts`) — flattens the
subcategory tree, dedupes by id, and normalizes the language code `am → hy`. **Already written.**
Backend action: match the `CategoryDto` field names; no new mapper.
#### Example B — Products (largest existing mapper; match its tolerance)
Real backend JSON (`GET /items/{id}`) → frontend `Item` (`src/app/models/item.model.ts`).
Bridge: **`ApiService.normalizeItem()`** — the single largest inline adapter in the codebase
(audit §7). It reconciles two historical shapes and normalizes, among others:
- `id` (string) ↔ `itemID` (numeric)
- `imgs[]``photos[]` (→ `ProductMedia`)
- `names[]``translations`
- `description` as key/value array ↔ string
- `comments``callbacks` (reviews)
- color `0xRRGGBB``#RRGGBB`; `remaining` count → stock band
Backend action: **do not "clean up" these dual shapes**`normalizeItem`/`normalizeCategory`
define exactly what the client accepts. Producing a payload inside that tolerance envelope is the
contract; a stricter/renamed shape breaks the storefront. No new mapper needed — the adapter
exists and is LIVE.
#### Example C — Admin Orders (no mapper exists yet — one must be written)
Unlike A and B, the admin domains have **no wire DTO and no mapper** — the local gateways
construct `AdminOrder` view models directly in memory and never touch a URL (audit §14, §24).
So this is the shape you have _freedom_ to design on the backend, as long as the new
`AdminOrdersApiGateway` maps it into the existing view model.
Proposed backend JSON (`GET /backoffice/orders/:id`):
```json
{
"id": "ord_1042",
"status": "processing",
"paymentStatus": "paid",
"customer": { "id": "cus_88", "name": "…", "email": "…" },
"items": [ { "productId": "…", "title": "…", "qty": 2, "unitPrice": 1990 } ],
"shipping": { "method": "…", "address": "…" },
"timeline": [ { "event": "created", "at": "2026-07-01T10:00:00Z" } ]
}
```
Frontend view model: `AdminOrder` + `AdminOrderCustomer`, `AdminOrderPayment`,
`AdminOrderShipping`, `AdminOrderItem`, `AdminOrderTimelineEntry`, `AdminOrderStatus`,
`AdminOrderPaymentStatus` (`features/admin/orders/models/admin-order.model.ts`, audit §23).
Bridge: **a new `AdminOrdersApiGateway` must contain the mapping** JSON → `AdminOrder`, honoring
the `AdminOrdersGateway` interface methods (`loadOrders`, `loadOrder`, `updateStatus`,
`requestRefund`, `addNote`, `archiveOrder`, `restoreOrder`, `deleteOrder`). The `status` field
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
must respect the order state machine (see the Orders CRUD contract / archive/BACKEND_API.md §8.1).
The same "no mapper exists, write one inside the new `*ApiGateway`" note applies to Products,
Users, Transactions, Monitoring, and Moderation.
### 8.5 Per-domain migration checklist (the pattern, then the table)
**Generic pattern to flip one domain from mock to real.** Steps 13 are only needed for the
"no seam yet" domains; token-bound domains start at step 4.
1. **Ensure the gateway interface exists** (all admin domains already have one — audit §14). If a
domain truly has none (e.g. Customers, Analytics derive from others), decide whether to add one
or keep deriving.
2. **Add a DI token**`InjectionToken` + factory that selects mock vs. real off
`RuntimeProviderStrategyService`, mirroring `admin-categories-gateway.token.ts`.
3. **Update the facade injection** from the concrete `Admin*LocalGateway` class to the new token.
4. **Implement the `*ApiGateway`** class satisfying the existing interface (contains the DTO→view
mapper, per §8.4 Example C).
5. **Bind the token to the real impl** in the providers (factory returns the api gateway for
`api` mode).
6. **Retire the `*LocalGateway`** — either delete it or keep it behind the existing
`useMockData` / mode flag as a dev fixture (the factory already lets both coexist).
This is the exact pattern Categories already follows (`admin-categories-api.gateway.ts` +
`admin-categories-gateway.token.ts`); replicate it verbatim.
| Domain | Needs steps 13 (add seam)? | Interface exists? | Real impl exists? | Net work |
|---|---|---|---|---|
| Categories (admin) | no (token-bound) | yes | **yes** | server endpoints only |
| Dashboard metrics | no (token-bound) | yes | no | steps 46 |
| Media | no (token-bound, class token) | yes (abstract class) | no | steps 46 |
| User experience | no (token-bound) | yes | no | steps 46 **+ repo redesign** (id-only + batch hydrate) |
| Orders | **yes** | yes | no | steps 16 (skip 1) |
| Products (admin) | **yes** | yes | no | steps 16 (skip 1) |
| Users / roles | **yes** | yes | no | steps 16 (skip 1) |
| Transactions | **yes** | yes | no | steps 16 (skip 1) |
| Monitoring | **yes** | yes | no | steps 16 (skip 1) |
| Moderation | **yes** | yes | no | steps 16 (skip 1) |
| Customers | inherits Orders seam | no (derived) | no | derive from Orders token, or add source |
| Analytics | partial | no (derived) | no | needs data source first (last) |
| Content management | n/a (no gateway) | n/a | no | **add** draft/publish HTTP path |
| Project editor / builder | n/a (no gateway) | n/a | no | **add** draft/publish HTTP path |
### 8.6 Recommended backend implementation order (all domains)
Ordered by the frontend's actual dependency structure (audit §5§18), not by convenience:
1. **Auth + session first.** `TelegramSessionApiService` is LIVE and every admin path is gated by
the `adminAuthHeadersInterceptor` (`AdminWebSessionID` / `Bearer`, audit §3). Nothing
role-gated works until session issuance and (if used) the Ed25519 `/api/admin/auth/*` flow are
real. Blocks everything admin.
2. **Bootstrap / tenant resolution.** `GET /bootstrap` transport is done, but its **content**
(branding/theme/nav/seo) is still stubbed (remaining-work #1). Tenant resolution and
`ApiConfigService.getBaseUrl()` depend on it; every tenant-scoped call resolves through it.
3. **Categories.** Already LIVE for both storefront (`GET /category`) and admin
(`AdminCategoriesApiGateway`). Products reference categories, so categories must be real and
populated before products are meaningful. Mostly "confirm server endpoints" work.
4. **Products / catalog.** Storefront reads (`GET /items`, `/searchitems`, `/randomitems`) are
LIVE; the **admin Products CRUD** is the first no-seam domain to build (remaining-work #3, P0),
and it depends on categories.
5. **Media.** Products/categories editors reference media assets; a real `ApiMediaRepository`
(remaining-work #5, P0) should land alongside/just after admin Products.
6. **Cart / Orders / Transactions.** Checkout `POST /cart` + `POST /orders` are LIVE; admin
**Orders CRUD** (remaining-work #6) then **Transactions** (tied to orders, #7) are the P1 core
commerce domains. Orders before Transactions/Customers/Analytics (they derive from it).
7. **Reviews / Moderation.** Customer review/question writes are LIVE; admin **Moderation**
(#10) gates them. Depends on products existing.
8. **Users / roles / invitations** (#9) — admin governance; independent of commerce but needs
auth (step 1).
9. **Dashboard metrics** (#11) then **Monitoring** (#12) — operational visibility over the
domains above.
10. **Analytics** (#13/#17) — **last**: needs orders/products/moderation real _and_ a tracking
pipeline that does not exist yet.
11. **Builder draft/publish + Content/CMS** (#2 P0 for builder transport, #14 for CMS) — these are
net-new write paths (no client call exists, audit §16/§17). Builder publish is high priority
for the builder product but is orthogonal to the storefront/commerce chain, so it can proceed
in parallel once bootstrap content (step 2) is real.
12. **User-experience sync** (#16) and **search suggestions/filters** (#15) — P2/P3 enhancements
over already-working local features.
### 8.7 Estimated effort (frontend-informed)
These are **frontend-informed** sizes only — they reflect how much of the contract is already
seamed/mapped on the client. Backend has its own unknowns (schema design, storage, infra, the
tracking pipeline) that are **out of scope for this document**; a domain marked S here can still be
L on the server.
| Domain | Size | Why |
|---|---|---|
| Categories (storefront + admin) | **S** | LIVE both sides; `CategoryMapper` + `AdminCategoriesApiGateway` exist. Server endpoints only. |
| Products storefront reads | **S** | LIVE; `normalizeItem` absorbs shape variance. |
| Bootstrap content | **M** | Transport done; producing real branding/theme/nav/seo values is the work (#1). |
| Media | **M** | Token seam + abstract interface ready; write `ApiMediaRepository` + upload pipeline (#5). |
| Admin Orders | **M** | Interface + rich view models exist, but no seam and no mapper — steps 16 + mapper. |
| Admin Products CRUD + variants | **L** | No seam, no mapper; largest admin model set (variants/attributes/translations/seo). |
| Admin Users/roles/invitations | **M** | No seam; also reconcile the duplicate `AdminRole` naming (audit §14/§25 #3). |
| Admin Transactions | **M** | No seam; tied to Orders shape. |
| Admin Moderation | **M** | No seam; two state machines (reviews + reports). |
| Admin Monitoring | **M** | No seam; three sub-resources (events/queues/webhooks). |
| Dashboard metrics | **SM** | Token-bound already; single `loadMetrics` shape. |
| Customers | **S** | Derives from Orders once Orders is real. |
| Analytics | **XL** | No data source; needs tracking pipeline + aggregation, gated on everything above. |
| User-experience sync | **L** | Requires client repository redesign (id-only + batch hydrate), not a drop-in. |
| Builder draft/publish | **L** | Net-new write path; no client call exists today. |
| Content management / CMS | **ML** | Net-new write path; adapter (`ContentPageService`) exists but no HTTP. |
| Search suggestions/filters | **M** | Client orchestration exists; backend suggestion source is new. |
---
## 9. Backend Checklist
A literal, top-to-bottom checklist. Work the phases in order; within a phase, items are roughly
independent. Section references point to the assembled backend-integration document (this doc's
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
§8, the CRUD-contracts sections, and `docs/archive/BACKEND_API.md` where a full shape already lives).
### Phase 1 — Foundation (nothing role-gated works until these land)
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Implement session issuance/check/logout: `POST /users/sessions`, `GET /users/sessions/:id`, `DELETE /users/sessions/:id` — per Auth contract (§5a; archive/BACKEND_API.md `/users/sessions/*`, already client-LIVE).
- [ ] Implement the Ed25519 admin-auth flow `GET /api/admin/auth/challenge`, `POST /verify`, `POST /refresh`, `POST /logout` — client wiring is LIVE and 404s today (§5b). Return `AuthChallenge` / `AuthTokenPair` shapes exactly.
- [ ] Honor the admin auth headers on every gated path: `AdminWebSessionID` + `Authorization: Bearer` for URLs containing `/admin/`, `/backoffice/`, `/builder/`, `/media/` (§3 interceptor pipeline).
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Serve real `GET /bootstrap` **content** (branding, theme, navigation, seo — not just the transport) — per Bootstrap contract (§6; archive/BACKEND_API.md §4). This is a P0 blocker.
- [ ] Populate `bootstrap.apiEndpoints.{website,builder,backoffice}` records so tenant-scoped paths resolve at runtime (§6; audit §24 — no path literals exist in client code).
- [ ] Confirm tenant resolution inputs (host/slug/code) match `TenantConfig` so `ApiConfigService.getBaseUrl()` resolves the right base (§2, §6).
- [ ] Adopt a consistent error envelope; the client maps failures to a `backend-unavailable` screen for admin auth — keep error bodies non-leaky (§5b; security guidance).
- [ ] Set the header contract: accept `X-Region`, `X-Language` (RU/EN/AM), `Currency` (default RUB), `WebSessionID` on marketplace requests (§3).
### Phase 2 — Read-heavy domains (mostly confirm; already client-LIVE)
- [ ] Stand up `GET /category` returning the `CategoryDto` shape `CategoryMapper` tolerates (§8.4 Example A; audit §8) — already LIVE client-side.
- [ ] Stand up `GET /items/:id`, `GET /category/:id`, `GET /items/randomitems`, `GET /searchitems` within the `normalizeItem` tolerance envelope (§8.4 Example B; audit §4, §7).
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Serve admin categories CRUD via the existing `AdminCategoriesApiGateway` contract: `loadCategories`, `loadCategory`, `create/update/delete/restore`, `isSlugTaken` — per Categories CRUD contract (archive/BACKEND_API.md §6.9). **Already wired client-side (DONE).**
- [ ] Serve `GET /api/backoffice/products` and `GET /api/backoffice/categories` (storefront cards) — `ApiBackofficeDataProvider` is LIVE (audit §9).
- [ ] Stand up `GET /regions``Region[]` (feeds the `X-Region` header; client falls back to 6 hardcoded regions) (audit §12).
### Phase 3 — Write-heavy customer domains
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Keep `POST /cart` (`CartPaymentRequest``QrCreateResponse`) and the frozen QR/card payment polling working unchanged (§10; payments frozen per archive/BACKEND_API.md §2.8).
- [ ] Implement `POST /orders` (`CreateOrderRequest``CreateOrderResponse`) — client call is LIVE, fire-and-forget after payment (§10; archive/BACKEND_API.md §16.9, marked DONE client-side).
- [ ] Implement `POST /purchase-email` (email receipt) (audit §4).
- [ ] Accept review/question writes `POST /items/:id/callback` and `POST /items/:id/questiion` (**preserve the `questiion` typo** — it matches the client literal) (§11; audit §4).
### Phase 4 — Admin domains (each needs the token seam added first — §8.5)
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Add `AdminOrdersGateway` token + `AdminOrdersApiGateway`, switch `AdminOrdersFacade` to the token; implement `GET/POST /backoffice/orders*` incl. `POST /backoffice/orders/:id/status` respecting the order state machine — per Orders CRUD contract (§8.4 Example C, §8.5; archive/BACKEND_API.md §6.11/§8.1).
- [ ] Add `AdminProductsGateway` token + `AdminProductsApiGateway`, switch `AdminProductsFacade`; implement Products CRUD + variants — per Products CRUD contract (§8.5; archive/BACKEND_API.md §6.10/§7.2).
- [ ] Add `AdminTransactionsGateway` token + api gateway, switch `AdminTransactionsFacade`; implement transactions list/detail + `retryFailed` + `setFraudFlag` (tied to orders) — per Transactions contract (§8.5; archive/BACKEND_API.md §6.12).
- [ ] Add `AdminUsersGateway` token + api gateway, switch `AdminUsersFacade`; implement users/roles/invitations/sessions/audit — per Users contract (§8.5; archive/BACKEND_API.md §6.13). Reconcile the duplicate `AdminRole` naming (audit §25 #3).
- [ ] Add `AdminModerationGateway` token + api gateway, switch `AdminModerationFacade`; implement review + report status transitions — per Moderation contract (§8.5; archive/BACKEND_API.md §6.14/§8.4/§8.5).
- [ ] Add `AdminMonitoringGateway` token + api gateway, switch `AdminMonitoringFacade`; implement events/queues/webhooks reads — per Monitoring contract (§8.5; archive/BACKEND_API.md §6.16).
- [ ] Wire `AdminDashboardMetricsApiGateway` to the existing `ADMIN_DASHBOARD_METRICS_GATEWAY` token; implement `loadMetrics` — per Dashboard contract (§8.5; archive/BACKEND_API.md §6.15).
- [ ] Resolve Customers: derive from the real Orders token (`AdminCustomersFacade`) or add a first-class customers source (§8.3).
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Defer Analytics until orders/products/moderation are real and a tracking pipeline exists; then implement the analytics summary source (§8.3, §8.6 step 10; archive/BACKEND_API.md §6.17).
### Phase 5 — Builder / CMS (net-new write paths — no client call exists today)
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Implement builder bootstrap draft/publish/validate: `GET/PUT /builder/bootstrap/draft`, `POST /builder/bootstrap/publish`, `POST /builder/bootstrap/validate` — and add the client-side write call in `ProjectEditorFacade`/`ProjectEditorIoService` (§8.3, §17; archive/BACKEND_API.md §6.7). P0 for the builder.
- [ ] Implement content pages / CMS write path and wire `ContentManagementFacade` beyond in-memory bootstrap (§8.3, §16; archive/BACKEND_API.md §6.8).
- [ ] Implement the media upload/delete/replace pipeline behind `ApiMediaRepository` bound to the `MediaRepository` token (§8.5; archive/BACKEND_API.md §6.18/§10).
### Phase 6 — Hardening
- [ ] Add rate limiting on all write endpoints (orders, reviews, admin CRUD, media upload) (security guidance).
- [ ] Set CSP / security headers for the frontend origin (frontend + API share a domain by decision — remaining-work "Explicitly not in this list").
- [ ] Add audit logging for admin mutations (order status, role changes, moderation actions, publish) — the client already models `*AuditEntry` / timeline shapes (audit §23).
- [ ] Implement maintenance-mode / graceful `backend-unavailable` responses the client can surface (§5b).
- [ ] Implement `GET /items/batch?ids=` to unblock the user-experience id-only sync redesign (remaining-work #16; §8.3).
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
- [ ] Add search suggestions/catalog-filter source if pursuing #15 (§8.6 step 12; archive/BACKEND_API.md §6.6).
- [ ] Plan dynamic sitemap generation (server-side, no frontend action) (remaining-work #18).
docs: final project closeout - classify TODO, backend spec, status Classified every TODO.md item into one of DONE/BACKEND/PRODUCT DECISION/FUTURE VERSION/BUG, verified against source, not against prior docs: - BACKEND items (bootstrap content, builder draft/publish, 6 admin CRUD domains, media pipeline) confirmed already covered by BACKEND_INTEGRATION.md; appended a mapping appendix rather than duplicating raw bullets. Fixed 22 stale internal BACKEND_API.md cross-references left over from before that file was archived. - PRODUCT DECISION items (dark mode, brand-color WCAG contrast, stars.component token gap, footer Contacts content, advanced analytics, payment providers) moved to new docs/PRODUCT_BACKLOG.md. - FUTURE VERSION items (Angular 22, bundle splitting, cart-modal composition cleanup, hero-spacing investigation) moved to new docs/FUTURE_FEATURES.md. - BUG: rewrote docs/KNOWN-ISSUES.md down to the one real, verified, currently-reproducible frontend bug (Ed25519 admin-auth error codes session-expired/invalid-signature are unreachable - toAuthErrorShape() never reads a body error code, only maps HTTP status, and no status ever produces those two codes - confirmed by reading auth.service.ts + auth-error.model.ts). Condensed the "Fixed" history instead of carrying full verbose repro text forward. - DONE items removed outright (dead-code deletion, dashboard false positive, RC-02 fixes, stale "dynamic-renderer unwired"/"178 missing keys" claims already disproven by source). docs/TODO.md rewritten to the exact "no blockers" template - nothing left qualifies as a release blocker. New docs/PROJECT_STATUS.md: honest per-area status (frontend/backend/ docs/auth/builder/storefront/admin), known limitations, and explicit production/backend/demo readiness calls - including correcting an initial draft's unpushed-commit count (53, not 10, per git log origin/B2B..HEAD). New docs/NEXT_PHASE.md: work that can only start once a real backend exists (gateway swap-in, mock removal, dormant-auth activation, role enforcement, integration/E2E tests, perf profiling, monitoring, maintenance-mode UI). docs/PROJECT_INDEX.md (the stated entry point) updated to link the new doc set and stop pointing at the now-archived BACKEND_API.md/AUTH.md. docs/FRONTEND-ROADMAP.md's "Known open items" replaced with pointers to the new category-split docs instead of a duplicated mixed list. Not swept: a handful of low-traffic docs (architecture ADRs, FRONTEND.md, EDITOR.md, ARCHITECTURE.md, PROJECT-STRUCTURE.md, StaticPages.md, ADMIN.md) still reference the old BACKEND_API.md/ AUTH.md filenames - noted as a known gap in PROJECT_STATUS.md rather than touched blindly, since they're historical-context docs, not the navigation entry point.
2026-07-26 12:35:26 +04:00
---
## Appendix: `docs/TODO.md` items merged into this document (2026-07-26)
Final Project Closeout moved every backend-shaped item out of `docs/TODO.md` into this
document. None were duplicated as raw new bullets — each is already covered by an
existing section above:
| TODO item | Covered by |
|---|---|
| `bootstrap.json` real content (branding/theme/nav/seo) | §1 Bootstrap |
| Builder bootstrap draft/publish/validate | §1 (Draft vs Published), §8, §9 Phase 5 |
| Backoffice Products CRUD | §3 Products, §8, §9 Phase 4 |
| Media upload/delete/replace pipeline | §7 Uploads, §9 Phase 5 |
| Backoffice Orders CRUD + status transitions | §3 Orders, §8, §9 Phase 4 |
| Backoffice Transactions | §3 Transactions, §9 Phase 4 |
| Backoffice Users/roles/invitations | §3 Users/Roles, §9 Phase 4 |
| Backoffice Moderation (reviews/reports) | §3 Reviews/Reports, §9 Phase 4 |
| Backend Ready sprint / no real API contract | This entire document |
`docs/TODO.md` is now empty of blockers — see that file.