- 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.
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
- 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, 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 callsPOST/GET/DELETE {authApiUrl}/users/sessions...and normalizes the response intoAuthSession. 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 originalTelegramLoginComponent), driven by a smallQrLoginAdapter<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 amode: 'customer' | 'admin'input;ngOnInitpicksAuthServiceorAdminAuthServiceaccordingly and builds theQrLoginAdapterfrom 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,modedefaults to'customer'); admin usage is<app-telegram-login mode="admin" />, mounted once globally inapp.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