Files
marketplaces/docs/BACKEND.md
sdarbinyan 6a8c4a549a feat(admin): complete category management
Sprint 20. Adds features/admin/categories/ (model, gateway interface +
local gateway, facade, list/editor pages), mirroring the admin/products
container/facade/service split.

- indented hierarchy view + native HTML5 drag-and-drop reorder
- visibility toggle, item counter, empty state, include-deleted filter
- editor: slug uniqueness validation, translations, SEO fields, breadcrumb
  preview, image via existing MediaPickerComponent
- soft delete/restore, blocked when a category has children or items
- draft/publish status + localStorage draft recovery (mirrors Project
  Editor autosave) + CanDeactivate unsaved-changes guard
- wired into app.routes.ts (replaces the categories coming-soon placeholder)
- docs/ADMIN.md + docs/BACKEND.md updated with the new gap detail

Not yet done: admin/products' category dropdown still reads from its own
AdminProductsGateway.loadCategories() rather than this gateway (Sprint 21).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 09:34:13 +04:00

16 KiB

BACKEND

This is the punch list for backend engineers. Every item below is a frontend behavior that is currently mocked or local-only, with the exact gap, the endpoint(s) needed, and which frontend files change once the endpoint exists. Read this file instead of diffing the whole repo against main.

General contract rules (apply to everything below): tenant is resolved by request host, never a path/query param (docs/ARCHITECTURE.md); bootstrap/config responses must never include secrets; existing auth/payment/authorization contracts are frozen (ADR-010) — nothing here changes them.

1. Auth — admin authorization gap (highest priority, security-relevant)

Current frontend behavior: Admin login and customer login share one Telegram QR/session backend ({authApiUrl}/users/sessions) and one UI component (TelegramLoginComponent, mode: 'customer' | 'admin'). The only client-side difference is where the resulting session id is stored: webSessionID cookie (SameSite=Lax) for customers vs. adminSessionID cookie (SameSite=Strict) for admin, via separate AuthService / AdminAuthService. Admin API calls attach AdminWebSessionID via adminAuthHeadersInterceptor (core/admin-auth/admin-auth-headers.interceptor.ts).

The gap: because both flows hit the identical Telegram session endpoint, the backend has no concept of "this is an admin session" at the moment the QR is scanned — it's an ordinary Telegram user session, indistinguishable from a customer's. The frontend only decides where to store the resulting id; it cannot and does not decide whether that Telegram user is actually allowed to act as an admin. Any Telegram user who completes the QR flow while the admin login UI is showing gets an adminSessionID.

What's needed: server-side authorization check keyed off the session id (or a dedicated admin-scoped token) at the point admin API calls arrive with AdminWebSessionID — reject non-admin users. This must be enforced server-side; nothing on the frontend can substitute for it.

Frontend files that would change: src/app/core/admin-auth/admin-auth.service.ts, admin-auth-headers.interceptor.ts, admin-auth.guard.ts — only if the auth response shape changes (e.g. a role claim to check client-side in addition to the server-side enforcement).

Also prepared, not wired: core/admin-auth/ed25519-verification.model.ts defines Ed25519VerificationService (challenge/verify) for a future non-Telegram admin auth path. Current binding (NoopEd25519VerificationService, in app.config.ts) throws rather than silently accepting — safe to leave wired until a real challenge/verify endpoint exists.

2. Bootstrap draft / publish persistence (second-highest priority)

Current frontend behavior: The Project Editor (docs/EDITOR.md) edits the same BootstrapConfig the storefront consumes.

  • Load: GET /bootstrap (existing, tenant by host).
  • Save: in-memory + localStorage only (ProjectEditorDraftStorageService, key projectEditor.draftBootstrap.v1, scoped by tenant.id). Reload in another browser/tab and it's gone.
  • Publish: applies the bootstrap in-memory via PlatformRuntimeService.reloadFromBootstrap() and flips a local status flag. No backend call.

Endpoints needed (none exist yet; shapes are contracts, adjust to house conventions):

GET  /builder/bootstrap/draft
  -> tenant's current draft BootstrapConfig. 404/empty if none (draft = published).

PUT  /builder/bootstrap/draft
  body: BootstrapConfig
  -> persists the draft for this tenant. Does not affect GET /bootstrap (storefront-facing).

POST /builder/bootstrap/publish
  body: BootstrapConfig (or none, if publish always promotes the stored draft)
  -> validates, then makes this BootstrapConfig what GET /bootstrap returns. Only endpoint
     that affects the live storefront.

POST /builder/bootstrap/validate   (optional)
  body: BootstrapConfig
  -> { code: string, message: string }[], mirroring ProjectValidationIssue shape client-side.

Tenant identity: resolved by request host, same as every other endpoint — no projectId path param. One domain = one tenant = one draft = one published bootstrap.

Don't duplicate client validation, enforce it server-side too. ProjectValidator (features/project-editor/services/project-validator.service.ts) blocks Publish client-side on: missing branding.logoUrl, empty localization.supportedLocales, invalid tenant.websiteBaseUrl (must be http(s)://...), duplicate static-page identifiers (slug, falling back to route), empty homepage sections, a homepage widget with no type, duplicate header nav links, non-hex theme.palette values, missing translations for a supported locale, and unknown layout/section-layout enum values. A malicious or stale client can bypass all of this — if POST /builder/bootstrap/publish is a trust boundary, re-run equivalent checks server-side.

Frontend files that would change: features/project-editor/facade/project-editor.facade.ts (replace local save/publish with the new endpoints, keep the same public method signatures), services/project-editor-draft-storage.service.ts (becomes a fallback/offline cache rather than primary store).

3. Static pages / slug vs route inconsistency

Gap: StaticPageConfig requires slug: string, but at least one real bootstrap fixture (src/assets/mock/bootstrap/bootstrap.json) only populates route (e.g. /about-us) and leaves slug undefined. The frontend's duplicate-detection was patched to fall back to route when slug is empty, but the underlying data inconsistency remains.

Needed: decide whether slug is backend-required or auto-derived from route, so both sides agree on one source of truth going forward.

Frontend files: shared/models/config/static-page.model.ts, features/content-management/services/* (ContentPageService normalization), features/project-editor/services/project-validator.service.ts.

4. Static pages / CMS persistence

Current frontend behavior: staticPages are edited client-side in the Project Editor (create/delete page, toggle footer/header/sitemap visibility, edit slug/icon/order/translations/HTML) and only ever written into the in-memory/localStorage bootstrap draft above (item 2) — no dedicated backend for CMS content exists.

Endpoints needed:

GET  /builder/content-pages
PUT  /builder/content-pages
POST /builder/content-pages/import
GET  /builder/content-pages/export
POST /builder/content-pages/validate

Backend must also support content moderation/validation on publish (disallow dangerous tags/attributes) and revision history — the frontend only sanitizes at render time, not at authoring time (MarketplaceHtmlEditorComponent emits raw HTML with no sanitization by design; sanitization is a storefront-render concern, not an authoring concern).

Frontend files: features/content-management/facade/*, services/*, components/html-editor/*, features/project-editor/sections/footer-section.component.ts (static page list editing today lives partly here too).

5. Categories

Current behavior (Sprint 20): GET /category (existing) still backs the public storefront via CategoryDto -> CategoryMapper -> Category -> CategoryFacade, unchanged. A full admin editor now exists at features/admin/categories/ (list + create/edit, hierarchy, drag-and-drop reorder, soft delete/restore, draft/publish, SEO/translations — see docs/ADMIN.md "Sprint 20") but it runs entirely against AdminCategoriesLocalGateway, an in-memory cache seeded once from BackofficeDataService.loadCategories() (CategoryCardConfig, no hierarchy) — nothing persists across a page reload.

Gap: no admin write path (create/update/delete/reorder categories) exists on the backend. AdminCategory also carries fields the current CategoryDto/CategoryCardConfig don't have yet: parentId (hierarchy), slug, icon, imageUrl, status (draft/published), deletedAt (soft delete), seo, per-locale translations.

Needed: category CRUD endpoints matching the AdminCategory shape (src/app/features/admin/categories/models/admin-category.model.ts) plus a bulk reorder endpoint (order field) and a slug-uniqueness check (GET /admin/categories/slug-taken?slug=..., mirrors AdminCategoriesGateway.isSlugTaken).

Frontend files: implement AdminCategoriesApiGateway against AdminCategoriesGateway (services/admin-categories-gateway.interface.ts) and rebind via an injection token (same swap pattern as AdminDashboardMetricsGateway/BACKOFFICE_DATA_PROVIDER) — the facade and pages don't change. Also still open: wire admin/products' category dropdown to AdminCategoriesGateway instead of its own AdminProductsGateway.loadCategories() (Sprint 21).

6. Products

Current behavior: features/admin/products/ is fully built (list + editor pages) against AdminProductsLocalGateway (swappable via an injection token, same pattern as everywhere else) — i.e. it's ready for a real API gateway, but one has never been implemented.

Needed: product CRUD endpoints matching the existing storefront product contract (itemID, name, price, currency, categoryID, visible, discount, images, badges, media, specificationGroups, variantOptions, relatedCollections — see the Product Engagement / Product Experience 2.0 fields folded from prior sprint reports).

Frontend files: implement AdminProductsApiGateway alongside the existing AdminProductsLocalGateway and rebind the injection token — features/admin/products/pages/* and the facade do not change.

7. Orders / revenue (does not exist at all)

Current behavior: no backend or local data model for orders or revenue exists anywhere in the codebase. features/backoffice/orders is an empty placeholder. The Admin Dashboard's Orders and Revenue cards intentionally render a pending-backend state ("Awaiting backend integration") rather than fabricated numbers or a generic empty state.

Needed: an order domain (creation, lifecycle, line items, totals) and revenue aggregation, plus endpoints to back a dashboard summary (see item 8) and an admin orders list/detail UI.

Frontend files: features/admin/dashboard/facade/admin-dashboard.facade.ts (card status computation), a new features/admin/orders/ module once the domain exists.

8. Dashboard metrics

Current behavior: AdminDashboardMetricsGateway (token ADMIN_DASHBOARD_METRICS_GATEWAY) defaults to AdminDashboardMetricsLocalGateway, which composes BackofficeDataService.loadCategories()/loadProducts() client-side into counts. Everything else on the dashboard (marketplace status, theme, languages, last publish/save, bootstrap version, active layout, enabled widgets, system health) is derived from ProjectEditorFacade state, not a metrics endpoint.

Needed: a /builder/dashboard/summary-style endpoint returning real-time counts and trend deltas.

Frontend files: implement AdminDashboardMetricsGateway (real API version) and rebind the token in DI config — AdminDashboardFacade and all dashboard card components are unaffected (docs/ADMIN.md has the full architecture).

9. Recent Activity (admin dashboard)

Current behavior: AdminDashboardHistoryService is localStorage-backed, scoped per tenant (adminDashboard.activityHistory.v1) — same limitation pattern as the draft storage in item 2. It will never show another editor's activity.

Needed: a real audit-log endpoint (who changed what, when) that multiple admin sessions can read.

Frontend files: features/admin/dashboard/services/admin-dashboard-history.service.ts, facade/admin-dashboard.facade.ts.

10. Translations

Current behavior: all UI strings are static, compiled into the three locale files (src/app/i18n/{en,ru,hy}.ts) via the Translations interface (translations.ts) and served by TranslateService/TranslatePipe. There is no backend-editable translation surface — bootstrap-level translatable content (nav labels, static-page translations, footer copyright) is edited per-tenant through the Project Editor and stored in the bootstrap itself (see item 2), which is separate from the compiled UI-chrome strings.

Gap: UI-chrome strings (button labels, section titles, validation messages) require a frontend deploy to change or add a locale — there's no backend-driven UI-string catalog. This may be acceptable (frontend chrome vs. tenant content are different concerns) but is worth an explicit decision if a backend team expects to control all copy.

Frontend files: src/app/i18n/* (if this becomes backend-driven, it's a new i18n loading mechanism, a larger change than any other item here).

Current behavior: in-memory products/categories/tags power autocomplete and suggestions (features/search/services/search-autocomplete.service.ts); trending returns null (UI hides gracefully); search history is LocalSearchHistoryRepository (guest) with a BackendSearchHistoryRepository placeholder already defined but unimplemented.

Endpoints that would let this go live without frontend changes:

GET /search/suggestions?q={term}
GET /catalog/filters?category={id}&q={term}

Plus, if authenticated history/wishlist/compare/saved-searches sync is wanted: GET/POST/DELETE /me/wishlist, /me/compare, /me/saved-searches, /me/recently-viewed.

Frontend files: features/search/services/search-autocomplete.service.ts, search-trending.service.ts, search-history.repository.ts (swap LocalSearchHistoryRepository for BackendSearchHistoryRepository) — facade/UI unaffected by design.

12. Product engagement (rating/reviews/questions) — partially real, confirm contract

Current behavior: frontend already expects these endpoints and renders against them: GET /products/{id}/rating, GET /products/{id}/reviews?page&pageSize, GET /products/{id}/questions?page&pageSize, POST /products/{id}/reviews (rating/title/text/anonymous), POST /products/{id}/questions (text/anonymous). If these already exist, this is a contract-confirmation item, not new work; if not, they block the Reviews/Questions UI (productPage.reviews/questions config, already toggleable per tenant).

Frontend files: none, if the contract matches what's documented in docs/BOOTSTRAP.md's Product Engagement notes.


Known reliability issues

Production 502/504 Bad Gateway on refresh / back-navigation

Symptom: production hits intermittent 502/504 Bad Gateway responses specifically on page refresh and browser back-navigation.

Root cause (investigation so far): environment.production.ts points the frontend at the backend API via absolute URLs directly (apiUrl: 'https://api.dexarmarket.ru:445', authApiUrl: 'https://users.vitanova.network:456'), bypassing this repo's nginx.conf entirely — that config only proxies /api for the lovero.store tenant, not dexarmarket.ru. So the 502/504 originates from that backend API's own reverse proxy (ports 445/456, a separate server not in this repo), not from anything this repo controls.

Refresh and back-navigation both re-fire session-check and bootstrap-load calls on mount (AdminAuthService.checkSession(), ConfigService.loadBootstrap(), TelegramSessionApiService), which is the likely trigger if that backend's app server or reverse proxy is crashing, overloaded, or misconfigured on those specific endpoints.

This is not fixable from this repo. It needs DevOps/backend investigation on the reverse proxy and app server fronting api.dexarmarket.ru:445 and users.vitanova.network:456 — check upstream health, timeout settings, and concurrent-connection handling around session-check and bootstrap endpoints.