# Marketplace Project Editor - Sprint 13 (updated Sprint 18) ## Scope Sprint 13 introduces first version of Marketplace Project Editor. Editor only manages bootstrap configuration. Out of scope: - products - categories - orders - analytics management ## Architecture ```text src/app/features/project-editor/ pages/ components/ sections/ models/ services/ facade/ ``` Editor uses strongly typed bootstrap models and isolated feature state. Components do not call APIs directly. ## Facade API `ProjectEditorFacade` exposes: - `loadBootstrap()` - `updateBootstrap()` - `exportBootstrap()` - `importBootstrap()` - `preview()` Current source of truth: - existing bootstrap provider via `ConfigService` Future backend integration: - replace local import/export/save flow with builder endpoints - keep component contracts unchanged ## Supported Sections - General - marketplace name - domain - description - default language - supported languages - Branding - logo - small logo - favicon - marketplace title - Theme - palette tokens via color pickers - Header - logo/search/categories/languages/cart/profile/wishlist/compare/region toggles - Footer - company information - address - phone - email - copyright - payment icons - social links - static pages - Homepage - home page sections list - visibility - order - layout - drag-and-drop ordering - Widgets - homepage widget configuration - typed shortcuts for hero/categories/product collection widgets - JSON fallback for other widget props - Marketplace Features - feature flags - search suggestions/history - recently viewed - reviews/questions/recommendations - Preview - export JSON - import JSON - runtime preview without full browser refresh - Languages (Sprint 16) - add/remove supported locale - set default locale - generically syncs translation keys across static pages and navigation labels (`LocaleSyncService`) - Navigation (Sprint 16) - header navigation: add/remove/reorder/edit label/URL/visibility - flat footer navigation: same actions - grouped footer navigation (column-based) is read-only in this tab for now ## Preview Strategy Preview updates bootstrap snapshot in memory and re-applies: - theme - branding - runtime state - route-based page rendering on next Angular navigation This keeps bootstrap engine intact while enabling fast local preview. ## Widget Configuration Current widget editor supports explicit fields for: - Hero - layout - height - overlay - autoplay - Categories - layout - columns - Product Collection - layout - cards per row - filters - badges - rating - price Other widgets use JSON props fallback until dedicated editors are added. ## Draft / Publish (Sprint 16, autosave added Sprint 18) There is still no backend draft/publish API. This sprint models it client-side in `ProjectEditorFacade`: - `status: 'draft' | 'published'` and `dirty` (diffed against the last-saved snapshot) live in facade state. - `save()` snapshots the current in-memory bootstrap as "last saved" and timestamps it (`lastSavedAt`). - `publish()` runs `ProjectValidator`, and if there are no issues, applies the bootstrap via `PlatformRuntimeService.reloadFromBootstrap`, marks status `published`, and becomes the new `originalBootstrap` snapshot used by reset. **Backend gap, not yet implemented:** real persistence needs `PUT /builder/bootstrap/draft` and `POST /builder/bootstrap/publish` endpoints so drafts/publishes survive a reload and are shared across editors. ### Autosave (Sprint 18) `ProjectEditorDraftStorageService` (`services/project-editor-draft-storage.service.ts`) persists the full bootstrap draft to `localStorage` (key `projectEditor.draftBootstrap.v1`, scoped by `tenant.id`) on every `updateBootstrap()` call, `save()`, and `publish()`. On `loadBootstrap()`, if a stored draft exists for the same tenant it is loaded instead of the freshly-fetched bootstrap and `draftRestored` is set true (surfaced in the save bar as a dismissible notice). The published/loaded bootstrap is never overwritten automatically — only explicit `publish()` calls change what the runtime actually serves; the localStorage draft is a separate, purely local concern that survives refreshes and browser restarts. Status indicators in `ProjectEditorSaveBarComponent`: - **Unsaved changes** - shown while `dirty()` is true. - **Last saved: HH:MM:SS** - shown once not dirty and `lastSavedAt` is set. - **Draft restored** banner - shown once after a local draft is loaded from a previous session, dismissible. ### Reset (Sprint 18) - **Reset section** - button above the active section (shown only for sections with a bootstrap-key mapping in `EDITOR_SECTION_BOOTSTRAP_KEYS`, `models/project-editor.model.ts`). Reverts that section's bootstrap keys to `originalBootstrap` (the last loaded/published snapshot). Confirmation required. - **Reset draft** - button in the save bar. Reverts the entire bootstrap to `originalBootstrap` and clears the persisted local draft. Confirmation required. - Per-field reset is **not implemented** - the bootstrap schema has no registry of per-field defaults, so only section- and project-level reset exist. Adding field-level reset would require either a default-value registry per field or storing per-field undo history; deferred. ## Validation `ProjectValidator` (`services/project-validator.service.ts`) runs on every render of the save bar: missing logo, no languages, invalid marketplace URL, duplicate static-page slugs, empty homepage, a homepage widget with no `type`, duplicate header navigation links, invalid theme colors. Publish is blocked while any issue is present; Save is not. ## Rich HTML editing Static page HTML is edited via `MarketplaceHtmlEditorComponent` (`components/html-editor/`), a `contentEditable` + toolbar component with no external dependency. It emits raw HTML on every change and never sanitizes — sanitization remains a storefront-render concern. ## QR Login Reuse (Sprint 18, corrected) There is exactly **one** Telegram QR/session backend (`{authApiUrl}/users/sessions`) and exactly **one** QR login component/UI. Nothing about the QR flow is duplicated for admin: - `TelegramSessionApiService` (`services/telegram-session-api.service.ts`) is the single place that calls `POST/GET/DELETE {authApiUrl}/users/sessions...` and normalizes the response into `AuthSession`. It holds no state and writes no cookies - it's a pure API wrapper. - `QrLoginEngine` (`shared/qr-login/qr-login.engine.ts`) is the QR/polling/expiry/"return from Telegram app" state machine (extracted from the original `TelegramLoginComponent`), driven by a small `QrLoginAdapter` (`shared/qr-login/qr-login.model.ts`). - `TelegramLoginComponent` (`components/telegram-login/`) is **the same component for both customer and admin login** - not two components. It takes a `mode: 'customer' | 'admin'` input; `ngOnInit` picks `AuthService` or `AdminAuthService` accordingly and builds the `QrLoginAdapter` from whichever one, but the QR image, polling loop, timeouts, and dialog markup are identical either way. Customer usage is unchanged (`` on the cart page, `mode` defaults to `'customer'`); admin usage is ``, mounted once globally in `app.html`. An earlier version of this sprint's work built a separate `AdminAuthService`/`AdminLoginComponent` pair that called its own `adminAuthApiUrl` placeholder endpoint. That was wrong: there is no separate admin backend, and inventing one client-side would have meant testing against an endpoint that doesn't exist. It was replaced with the shared-API approach described above. ## Admin Authentication (Sprint 18, corrected) Only the **storage** is separate between customer and admin - the QR/session API and UI component are shared (see above), by design, since one Telegram QR/session backend serves both. What stays separate is everything needed so that scanning the admin QR can never authenticate the customer session (or vice versa): | | Customer (`AuthService`) | Admin (`AdminAuthService`, `core/admin-auth/`) | |---|---|---| | Cookie | `webSessionID` (`SameSite=Lax`) | `adminSessionID` (`SameSite=Strict`) | | Token storage | `web_session_id` (localStorage, anonymous API attribution only, unrelated to auth) | `adminToken` / `adminRefreshToken` (localStorage, reserved for a future JWT pair - unused today) | | Signals | `session`, `status`, `showLoginDialog` on `AuthService` | `session`, `status`, `showLoginDialog` on `AdminAuthService` | | Guard | none yet for customer routes | `adminAuthGuard` (`core/admin-auth/admin-auth.guard.ts`) | | Interceptor | `apiHeadersInterceptor` | `adminAuthHeadersInterceptor` (`core/admin-auth/admin-auth-headers.interceptor.ts`), self-guards on `/admin/` in the request URL, sets `AdminWebSessionID` + `Authorization: Bearer ` when present | | Session/QR API | `TelegramSessionApiService` | same `TelegramSessionApiService` instance/endpoint | | Login UI | `TelegramLoginComponent` (`mode="customer"`, default) | same `TelegramLoginComponent` (`mode="admin"`) | **Backend gap this creates, and why it matters:** because admin login goes through the exact same Telegram session API as customer login, the backend has **no concept of "this is an admin session"** at the point the QR is scanned - it's just a regular Telegram user session, identical in shape to a customer's. The frontend only decides *where to store* the resulting session id (admin cookie vs. customer cookie); it cannot and does not decide whether that Telegram user is actually allowed to act as an admin. **Real admin authorization must be enforced server-side**, at the point admin API calls are made with the `AdminWebSessionID` header - the backend must check the authenticated user against an admin/role list and reject non-admins, since nothing on the frontend prevents any Telegram user from completing the QR flow while `mode="admin"` is showing. This needs a backend decision (role check keyed off the session id, or a dedicated admin-scoped token issuance) before admin login can be considered secure, not just "separate storage." ### Login test mode `?login=true` and `?adminLogin=true` query params (handled once in `App.ngOnInit` via `openLoginDialogsFromTestModeQueryParams()`, `app.ts`) call `AuthService.requestLogin()` / `AdminAuthService.requestLogin()` respectively, for manual testing. This only sets the same signal a normal "please log in" action would set - it does not bypass authentication or change any other behavior, so it is safe in all environments. `TelegramLoginComponent` in customer mode is currently mounted only on the cart page, so `?login=true` only shows a dialog there; the admin-mode instance is mounted globally so `?adminLogin=true` works from any route. ### Ed25519 prep `core/admin-auth/ed25519-verification.model.ts` defines `Ed25519VerificationService` (abstract, injectable) with `requestChallenge()` / `verify(signedResponse)` and the `Ed25519Challenge` / `Ed25519SignedResponse` / `Ed25519VerificationResult` shapes (nonce, timestamp, payload, public key, signature). No crypto is implemented. The current DI binding, `NoopEd25519VerificationService` (registered in `app.config.ts`), fails closed (throws) rather than silently accepting anything, so it's safe to wire into a real login path today - it will error loudly instead of pretending to verify a signature. Swap the DI binding for a real implementation once the backend ships challenge/verify endpoints; nothing else needs to change. ## Known gaps / deferred (Sprint 18) Full field-by-field coverage of every supported bootstrap property (with bilingual EN/RU labels, description, and validation state per field) was not completed in this pass - the bootstrap schema is large (theme typography/ spacing/shadows, full company address, per-locale footer copyright, grouped footer navigation editing, sidebar navigation, per-page SEO map, catalog/ product-page/user-experience sub-fields, permissions, API endpoints) and several concepts named in the sprint brief (payments, delivery/shipping, checkout, unified search config) have **no corresponding model in `shared/models/config` at all** - they would need new bootstrap schema before an editor could expose them. See the section-by-section gap list gathered during Sprint 18 investigation for the full inventory; treat as a follow-up sprint rather than something silently skipped. ## Constraints - runtime bootstrap engine not replaced - configuration stays source of truth - no hardcoded marketplace values - no business domain management mixed into editor