-
Mahmoud Aglan authored
S6, S7, the staff half of S8, S9 and S10, plus the admin screens S3 and S5 were waiting on. PWA (S6) -------- The worker lives at /app/sw.js and is generated per deploy, because the precache list comes from public/build/manifest.json and the cache name is a hash of it — a deploy evicts the old cache instead of leaving a worker serving asset URLs that no longer exist. Its scope is /app/, not the root: a root worker would control /dashboard and /api too, serving admins a stale shell and leaving cached credentialed responses on a shared front-desk tablet. HTML is never precached. wire:navigate swaps <head> wholesale and prefetches on hover, so an HTML cache fills with unvisited pages and then injects @vite hashes from a build that no longer exists — a blank page with no error. /livewire/* is never cached at all: its snapshot checksum is bound to APP_KEY and the session, so a replayed one is a corrupt-snapshot error rather than a stale render. The only offline artifact is a static page with no session and no CSRF token in it. nginx gets exact-match locations for /app/sw.js and /app/manifest.webmanifest. Both end in an extension the static-asset regex claims, and that regex ends in try_files $uri =404 — so without these the worker 404s before reaching PHP. Push (S7) --------- FCM, not VAPID. kreait/firebase-php is installed, device_tokens exists, twelve listeners already funnel through PushNotificationService, every client has their own Firebase project, and FCM HTTP v1 delivers to Web Push endpoints with the same CloudMessage and the same token column. VAPID buys independence from Google — not a constraint here — for a second sender, table, log path and prune policy. So the change is: platform CHECK widened to include 'web', a user_agent column for sensible pruning, and a unique index on (device_token, user_id) — never on the token alone, which is what the deleted DeviceController keyed on, letting anyone claim anyone's token so the victim's phone received the attacker's notifications. Duplicates are cleared before the index, because a failed migration blocks every later one on that client forever. The check-in gate (S8) ---------------------- Staff-scan only. The printed-poster direction stays cut: a printed QR is a public, permanent, non-secret string, so rotation is impossible by construction — it proves the member once visited, or knows someone who did. The scanner screen works with a connected barcode reader by default and uses BarcodeDetector where the browser has it, because most reception desks have the reader and not the camera permission. A real bug the tests caught: participants.status is cast to an enum, so comparing it to the string 'active' was always false — the gate would have turned everyone away. The native shell (S9) --------------------- flutter_shell/ holds one long-lived Sanctum token in the Keychain or EncryptedSharedPreferences with the single ability portal:session, and exchanges it at /app/session-exchange for an ordinary web session in the WebView's own jar. The token never reaches JavaScript. /app/* is never exempted from CSRF — that shortcut is what turns a wrapper from safe into trivially exploitable. Every bridge is an exported native capability, so each is narrow and checked natively: the host allowlist is compared against the origin read from the controller, never from the page; biometrics gate a native action and return nothing the page can use as an authorisation decision; QR is decoded natively and only the string crosses. flutter_inappwebview rather than webview_flutter, because <input type="file"> is inert in a bare Android WebView without onShowFileChooser — and that single gap breaks the transfer-proof upload, which is the portal's whole money path. Two endpoints only, and they are the only routes on the sanctum guard. The deleted API minted tokens with mobile:* — every endpoint it would ever grow. E3's recommendation stands and the shell is not shipped this cycle. It exists so that shipping is a decision rather than a project. App content (S10) ----------------- One `channel` column on website_news and website_sections instead of the parallel CMS the plan called for. website_sections, website_news, website_menus, media, a page builder and website:blueprint export|import all already exist; a second CMS is a second migration surface, a second editor to keep in step, and a second place for content to go missing, forever. Admin screens ------------- Portal invitations, where the raw link exists for exactly one render and is never recoverable afterwards. The duplicate-account merge screen E4 asked for — the prerequisite for ever putting a unique index on users.phone, and the reason ambiguous phone logins can be refused rather than guessed. Both move references rather than deleting rows: a deleted user id in a financial record is worse than a duplicate account. Documentation that was actively wrong ------------------------------------- docs/agent-rules/05-financial-integrity.md described double-entry as two rows with a type of 'debit' or 'credit', and 16-enums-and-checks.md registered that vocabulary. That schema has never existed — 2024_01_01_000013 created the single-row shape with both account columns from the start. Anyone writing code from that text got a mass-assignment no-op and a row that silently said nothing. CLAUDE.md repeated the same claim, and also said Livewire 3 while composer.json says ^4.3 — a difference that decides whether a public property is an IDOR. The test suite is now symmetrical: tests that build their own tables skip off SQLite, tests that need a real tenant skip off Postgres, so the whole file runs clean under either connection instead of one of them being a lie. Suite: 76 pass on SQLite (24 skipped), and against the restored tenant PortalSmokeTest 3/3, PaymentProofTest 11/11, CheckInScanTest 10/10. portal.css is 4.98 kB gzipped against app.css at 31.10 kB. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
1819b943