feat(sprint18): editor autosave/reset, admin auth, QR reuse, Ed25519 prep
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- 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.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Marketplace Project Editor - Sprint 13
|
||||
# Marketplace Project Editor - Sprint 13 (updated Sprint 18)
|
||||
|
||||
## Scope
|
||||
|
||||
@@ -128,22 +128,57 @@ Current widget editor supports explicit fields for:
|
||||
|
||||
Other widgets use JSON props fallback until dedicated editors are added.
|
||||
|
||||
## Draft / Publish (Sprint 16)
|
||||
## 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" (no
|
||||
network call yet).
|
||||
- `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` and marks
|
||||
status `published`.
|
||||
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
|
||||
@@ -159,6 +194,83 @@ Static page HTML is edited via `MarketplaceHtmlEditorComponent`
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user