Files
marketplaces/docs/Project-Editor.md
sdarbinyan 6aec2ebcb2
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
fix(admin-auth): reuse exact same QR/session API and component for admin login
- Removed invented adminAuthApiUrl endpoint and separate AdminLoginComponent.
  Admin login now uses the exact same Telegram session backend
  (TelegramSessionApiService, {authApiUrl}/users/sessions) and the exact
  same TelegramLoginComponent (mode="customer" | "admin" input) as customer
  login - only the storage (cookie/localStorage/signals) stays separate.
- Extracted the shared HTTP+normalization logic from AuthService into
  TelegramSessionApiService so both AuthService and AdminAuthService call it
  instead of duplicating request/parsing code.
- Documented the resulting backend gap in docs/Project-Editor.md: since the
  session API has no concept of "admin", server-side role enforcement is
  required when admin API calls are made - the frontend only decides where
  to store the session, not whether the user is actually an admin.
2026-07-14 10:13:59 +04:00

12 KiB

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

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<TSession> (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<TSession> (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 (<app-telegram-login /> on the cart page, mode defaults to 'customer'); admin usage is <app-telegram-login mode="admin" />, 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 <adminToken> 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