diff --git a/docs/Backend-Handoff-Sprint16.md b/docs/Backend-Handoff-Sprint16.md new file mode 100644 index 0000000..77d9ceb --- /dev/null +++ b/docs/Backend-Handoff-Sprint16.md @@ -0,0 +1,90 @@ +# Backend Handoff — Sprint 16 (Project Editor) + +For backend devs picking up work after the Sprint 16 frontend editor. Frontend +is done; this documents what backend still needs to build for the editor to +be real (not just an in-browser demo). + +## What exists today (frontend-only) + +The Project Editor (`/edit/:section`, or `/{lang}/edit/:section`) edits the +same `BootstrapConfig` the storefront consumes — no parallel model. Today: + +- **Load:** `GET /bootstrap` (existing, tenant resolved by request host). +- **Save:** in-memory only. `ProjectEditorFacade.save()` just snapshots the + current draft as "last saved" in the browser tab. Nothing is persisted. + Reload the page, or open the editor in another tab/browser, and it's gone. +- **Publish:** `ProjectEditorFacade.publish()` applies the bootstrap + in-memory via `PlatformRuntimeService.reloadFromBootstrap` (for live + preview) and flips a local `status` flag to `'published'`. It does not + call any backend endpoint. Nothing is persisted. + +This is fine for demoing the editor UI to one person in one browser tab. It +is not usable as a real per-tenant admin panel yet — that's this handoff. + +## Endpoints backend needs to add + +None of these exist yet. Suggested shapes (adjust to match your existing API +conventions — these are contracts, not prescriptions): + +``` +GET /builder/bootstrap/draft + -> returns the tenant's current draft BootstrapConfig (may differ from + the published one). 404/empty if no draft exists yet (draft = published). + +PUT /builder/bootstrap/draft + body: BootstrapConfig + -> persists the draft for this tenant. Does not affect what GET /bootstrap + (storefront-facing) returns. + +POST /builder/bootstrap/publish + body: BootstrapConfig (or no body, if publish always promotes the + current stored draft) + -> validates, then makes this BootstrapConfig the one GET /bootstrap + returns for this tenant. This is the only endpoint that affects the + live storefront. + +POST /builder/bootstrap/validate (optional — validation already runs + client-side via ProjectValidator, but a + server-side check prevents a stale/ + bypassed client from publishing garbage) + body: BootstrapConfig + -> returns the same shape as the client's ProjectValidationIssue[]: + { code: string, message: string }[] +``` + +Tenant identity: same as every other endpoint in this platform — resolved by +request host, not a `projectId` path param (see `docs/backend-platform/ +tenant-resolution.md`). There is no multi-project-per-domain concept; each +domain is one tenant with one draft and one published bootstrap. + +## What the frontend already validates (don't duplicate logic, just enforce it) + +`ProjectValidator` (`src/app/features/project-editor/services/ +project-validator.service.ts`) blocks Publish client-side on: +missing `branding.logoUrl`, empty `localization.supportedLocales`, invalid +`tenant.websiteBaseUrl` (must be `http(s)://...`), duplicate static-page +identifiers (slug, falling back to route), empty homepage sections, a +homepage widget with no `type`, duplicate header nav links, and any +non-hex-string `theme.palette` value. A malicious or buggy client could +bypass all of this — if `POST /builder/bootstrap/publish` is meant to be a +trust boundary, re-run equivalent checks server-side before accepting. + +## Static pages: `slug` vs `route` + +Heads up for whoever owns `StaticPageConfig`: the model requires `slug: +string`, but at least one real bootstrap in this repo +(`src/assets/mock/bootstrap/bootstrap.json`) only populates `route` (e.g. +`/about-us`) and leaves `slug` undefined. The frontend's duplicate-detection +was patched to fall back to `route` when `slug` is empty +(`project-validator.service.ts`), but the underlying data inconsistency is +still there. Worth deciding whether `slug` should be backend-required/ +auto-derived from `route` going forward, so both frontend and backend agree +on one source of truth. + +## Not in scope for this handoff (already tracked separately) + +- Separate admin app/deployment (`admin.` subdomain) — storefront and + editor still ship in one Angular build today. +- A pre-existing, unrelated crash in `ContentPageService.normalizeSlug` + against legacy-shaped static-page fixture data (frontend bug, not a + backend concern). diff --git a/nginx.conf b/nginx.conf index 95bdf33..cbaf5ee 100644 --- a/nginx.conf +++ b/nginx.conf @@ -94,3 +94,57 @@ server { add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; } + +# Template for onboarding a new marketplace tenant. +# Replace NEWMARKETPLACE.EXAMPLE.COM, /var/www/newmarketplace, and the +# api.newmarketplace.example.com:443 proxy target with the real values, +# then rename this block's server_name/root before deploying. +server { + listen 80; + server_name newmarketplace.example.com www.newmarketplace.example.com; + + root /var/www/newmarketplace/browser; + index index.html; + + # Angular routing - serve index.html for all routes (client-side router + # handles /edit, /:lang/edit/:section, etc. once index.html is served) + location / { + try_files $uri $uri/ /index.html =404; + } + + # Proxy API calls to backend + location /api { + proxy_pass https://api.newmarketplace.example.com:443; + proxy_set_header Host api.newmarketplace.example.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + rewrite ^/api(/.*)$ $1 break; + proxy_ssl_verify off; + } + + # Static assets caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Don't cache index.html + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Pragma "no-cache"; + add_header Expires "0"; + } + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; + gzip_min_length 1000; + + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; +}