- Project editor: persist draft to localStorage, restore on reload, last-saved/draft-restored status indicators, section/whole-draft reset with confirmation. - Extract shared QR/polling/expiry engine from TelegramLoginComponent (shared/qr-login) and reuse it for a new admin login flow. - Admin authentication kept fully separate from customer session: own cookie/localStorage keys, signals, guard, and header interceptor (core/admin-auth). - ?login=true / ?adminLogin=true open the respective login dialog for manual testing. - Ed25519 challenge/verify interfaces (fail-closed no-op binding) ready for backend delivery. - Document autosave/reset/admin-auth/QR-reuse/Ed25519 model and the remaining full-field-coverage gap in docs/Project-Editor.md.
11 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
- 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'anddirty(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()runsProjectValidator, and if there are no issues, applies the bootstrap viaPlatformRuntimeService.reloadFromBootstrap, marks statuspublished, and becomes the neworiginalBootstrapsnapshot 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
lastSavedAtis 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 tooriginalBootstrap(the last loaded/published snapshot). Confirmation required. - Reset draft - button in the save bar. Reverts the entire bootstrap to
originalBootstrapand 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)
The Telegram QR-login flow (QR image, polling, expiry, "return from app"
recovery via visibilitychange/focus/pageshow) was extracted from
TelegramLoginComponent into shared/qr-login/qr-login.engine.ts
(QrLoginEngine<TSession>) plus an adapter interface
(shared/qr-login/qr-login.model.ts, QrLoginAdapter<TSession>). The engine
is not a DI singleton - each login surface instantiates its own
new QrLoginEngine(adapter) and drives it from an effect() watching its own
dialog-visibility signal. TelegramLoginComponent was refactored onto this
engine with no behavior change. AdminLoginComponent
(core/admin-auth/admin-login.component.ts) reuses the same engine against a
separate adapter backed by AdminAuthService, so QR/polling/timeout logic is
not duplicated between customer and admin login.
Admin Authentication (Sprint 18)
Admin authentication is completely separate from the customer/storefront
session (AuthService), by design - one must never authenticate the other:
Customer (AuthService) |
Admin (AdminAuthService, core/admin-auth/) |
|
|---|---|---|
| Cookie | webSessionID |
adminSessionID (SameSite=Strict) |
| Anonymous/local id | web_session_id (localStorage, API attribution only) |
adminToken / adminRefreshToken (localStorage, reserved for future JWT pair) |
| Signals | session, status, showLoginDialog on AuthService |
session, status, showLoginDialog, role 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 URL, sets AdminWebSessionID + Authorization: Bearer <adminToken> when present |
| Login UI | TelegramLoginComponent (mounted per-page, e.g. cart) |
AdminLoginComponent (mounted once, globally, in app.html) |
Backend gap: environment.adminAuthApiUrl (https://users.vitanova.network:456/admin)
is a placeholder path under the existing auth host - there is no real admin
session/login backend yet. AdminAuthService.createWebSession() /
checkSessionOnce() / logout() call POST|GET|DELETE {adminAuthApiUrl}/sessions...
following the same shape as the customer session API; confirm/repoint this
once the backend ships dedicated admin endpoints.
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. Note TelegramLoginComponent is
currently mounted only on the cart page, so ?login=true only shows a dialog
there; AdminLoginComponent 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