feat: implement server-side pricing calculation and add site settings management
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -27,12 +27,15 @@ Postgres' official entrypoint only processes `/docker-entrypoint-initdb.d/` on a
|
||||
### Post-boot (every time, idempotent)
|
||||
- `supabase/migrations/post-boot.sql` — seeds the admin `auth.users` row with `must_change_password=true`, creates the `vehicle-photos` bucket row if missing. Parameterized: `psql -v admin_email=... -v admin_password=...`.
|
||||
- `supabase/migrations/02-leads.sql` — leads + customers + RPCs + `supabase_realtime` publication. Uses `create if not exists`, `create or replace`, and `do $$ ... $$` with exists-checks so every object is idempotent.
|
||||
- `supabase/migrations/08-backend-pricing-and-security.sql` — `calculate_price` RPC (public, read-only pricing), refactored `create_lead` RPC (accepts no price params, computes server-side), unique partial indexes on `lead_attachments` (max 1 id_document + 1 income_proof per lead), hardened storage RLS (anon INSERT-only on customer-documents, admin SELECT+INSERT only).
|
||||
- `supabase/migrations/09-site-settings.sql` — `site_settings` key-value table + RLS (public SELECT, admin full CRUD) + seed row `hero_image_url = '/images/ferrari-main-car.png'`.
|
||||
|
||||
Both are run by the `post-init` service, which gates on `auth.users` and `storage.buckets` being queryable before touching anything.
|
||||
|
||||
**Adding a new migration:**
|
||||
- If it's schema the app can't start without on a fresh DB → extend `01-init.sql`.
|
||||
- If it's ongoing / idempotent → new file `03-*.sql`, mount it in `post-init`, append one more `psql -f` line to the entrypoint.
|
||||
- If it's ongoing / idempotent → new file `NN-*.sql`, mount it in `post-init`, append one more `psql -f` line to the entrypoint in `docker-compose.yml`.
|
||||
- Current numbering: `01`, `02`, `08`, `09`. Use the next available number for new migrations.
|
||||
|
||||
---
|
||||
|
||||
@@ -40,13 +43,15 @@ Both are run by the `post-init` service, which gates on `auth.users` and `storag
|
||||
|
||||
| Role | How obtained | Can do |
|
||||
| ----------------- | ---------------------------------- | ------------------------------------ |
|
||||
| `anon` | Anon JWT (shipped to browser) | `select` on active `vehicles`; `insert` on `leads` only |
|
||||
| `authenticated` | JWT from `signInWithPassword` | Full CRUD on vehicles/leads/customers, `execute` RPCs |
|
||||
| `anon` | Anon JWT (shipped to browser) | `select` on active `vehicles`; `insert` on `leads` only; `select` on `site_settings`; `insert` on `customer-documents` bucket; execute `calculate_price` and `create_lead` RPCs |
|
||||
| `authenticated` | JWT from `signInWithPassword` | Full CRUD on vehicles/leads/customers/sales_orders/site_settings, `execute` RPCs, storage uploads |
|
||||
| `service_role` | Service JWT (never in browser) | Bypasses RLS |
|
||||
|
||||
- Anon **cannot** read leads. The booking form is fire-and-forget by design. An abuser submitting with a stolen anon key sees only HTTP 201 with no row data.
|
||||
- Anon **cannot** read/delete from `customer-documents` bucket — only INSERT (upload). Admin can SELECT + INSERT but not DELETE (prevents evidence destruction).
|
||||
- RPCs are `SECURITY INVOKER`. They run as the caller, so `auth.uid()` returns the actual admin; RLS still applies.
|
||||
- `qualify_lead` is idempotent — calling it twice returns the existing customer row instead of raising.
|
||||
- `create_lead` computes prices server-side via `calculate_price` — no price params accepted from client (prevents price-tampering).
|
||||
|
||||
---
|
||||
|
||||
@@ -93,6 +98,10 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
|
||||
- The `web` service uses `nginx:1.27-alpine` directly (no `build:`). Static files and `nginx.conf` are bind-mounted. Updating the frontend is `git pull` + `docker compose up -d --force-recreate web`.
|
||||
- `frontend/app.js` (public) creates a Supabase client with `persistSession: false` — the public site never needs a session.
|
||||
- `frontend/admin.js` uses `persistSession: true, storageKey: "mccars.auth"` and a single realtime channel `mccars-admin` that subscribes to leads/customers/vehicles.
|
||||
- Admin tabs: Aktive Leads, Inaktive Leads, Kunden, Fahrzeuge, **Einstellungen**. The Einstellungen (Settings) tab manages site-wide configuration (currently: hero image upload).
|
||||
- `app.js` calls `calculate_price` RPC for sidebar pricing display — **no client-side price computation**.
|
||||
- `app.js` calls `create_lead` RPC for form submission — prices are computed server-side and stored on the lead.
|
||||
- `app.js` at boot loads `hero_image_url` from `site_settings` and applies it via CSS variable `--hero-bg` on `.hero`. The `styles.css` fallback is `url('images/ferrari-main-car.png')`.
|
||||
- `app.js` writes `vehicle_id` (uuid) **and** denormalized `vehicle_label` ("BMW M3") into the lead at submit time, so the admin UI renders even if the vehicle is later deleted.
|
||||
|
||||
---
|
||||
@@ -111,6 +120,9 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
|
||||
| `config.js` not generated, website uses fallback URL | Unraid doesn't preserve execute bits — entrypoint script skipped. Fixed: `config.js` is now written by an inline `entrypoint:` command in docker-compose, no file needed. |
|
||||
| Website loads but API calls fail ("Failed to fetch") | `config.js` cached by NPM or browser with old `localhost` URL. The `?v=timestamp` cache-buster on the `<script>` tag prevents this. Hard-refresh or check NPM "Cache Assets" toggle. |
|
||||
| Git pull fails with "Your local changes would be overwritten" | `.env` was modified on the NAS. Use `git checkout -- .env && git pull`, then re-apply the two URL lines with `sed`. |
|
||||
| `calculate_price` returns PGRST202 "Could not find the function" | Migration `08-backend-pricing-and-security.sql` not applied. Check `post-init` logs or run it manually. |
|
||||
| Hero image not changing after admin upload | Browser/CDN caching the old image. The URL includes a unique path (`site/hero.ext`) with upsert — hard-refresh the public page. |
|
||||
| `create_lead` fails with "function does not exist" | Same as calculate_price — migration 08 not applied. |
|
||||
|
||||
---
|
||||
|
||||
@@ -119,22 +131,29 @@ Password min length is enforced server-side by `GOTRUE_PASSWORD_MIN_LENGTH=10`.
|
||||
```bash
|
||||
docker compose ps # all services up (post-init exited 0)
|
||||
curl http://localhost:55521/rest/v1/vehicles?select=brand -H "apikey: $ANON" # 6 demo cars
|
||||
curl -X POST http://localhost:55521/rest/v1/rpc/calculate_price \
|
||||
-H "apikey: $ANON" -H "Content-Type: application/json" \
|
||||
-d '{"p_vehicle_id":"<uuid>","p_date_from":"2025-01-06","p_date_to":"2025-01-10"}' # pricing JSON
|
||||
curl http://localhost:55521/rest/v1/site_settings?select=value&key=eq.hero_image_url \
|
||||
-H "apikey: $ANON" # returns hero image URL
|
||||
```
|
||||
|
||||
In the browser:
|
||||
1. Open http://\<host\>:55580, submit the booking form.
|
||||
2. Open http://\<host\>:55580/admin.html, log in with `.env` bootstrap creds.
|
||||
3. Rotate the password when prompted.
|
||||
4. The lead you submitted appears in "Aktive Leads" — in real time.
|
||||
5. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed.
|
||||
6. Open Supabase Studio at http://\<host\>:55530 and confirm the `customers.lead_id` FK matches.
|
||||
1. Open http://\<host\>:55580, verify the hero image loads (dynamic from settings).
|
||||
2. Submit the booking form — sidebar shows server-computed pricing.
|
||||
3. Open http://\<host\>:55580/admin.html, log in with `.env` bootstrap creds.
|
||||
4. Rotate the password when prompted.
|
||||
5. The lead you submitted appears in "Aktive Leads" — in real time, with pricing columns.
|
||||
6. Click **Qualifizieren**. Row disappears from active, a new row appears in **Kunden** with the `lead_id` displayed.
|
||||
7. Go to **Einstellungen** tab. Upload a new hero image. Verify it appears on the public site after refresh.
|
||||
8. Open Supabase Studio at http://\<host\>:55530 and confirm `site_settings`, `leads` pricing columns, `customers.lead_id` FK.
|
||||
|
||||
---
|
||||
|
||||
## 10. Things explicitly NOT in this stack
|
||||
|
||||
- No n8n. Email automation is out of scope here; do it downstream.
|
||||
- No Google Analytics / Google Reviews widget.
|
||||
- No "Anmelden/Registrieren" on the public site. Admin access is intentionally unlinked.
|
||||
- No logflare / analytics.
|
||||
- No edge-functions container — RPCs live in Postgres.
|
||||
- No client-side price calculation — all pricing is server-side via `calculate_price` RPC.
|
||||
|
||||
Reference in New Issue
Block a user