Repository navigation
M19.2: Auth hardening — graceful 419/session-expiry → login with return-to-page - #97
Conversation
Wait for mysqladmin ping before launching schedule:work and queue:work, and switch their depends_on to condition: service_healthy, so a cold `sail up` no longer crash-loops the workers before the DB is ready. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cope Only errors/403.blade.php is branded today; 404/500/503/429 fall through to Laravel defaults. Flag as candidate addition to the Visual Identity Polish phase, to confirm at flesh-out. 419 excluded (Phase 2 handles it). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ders §1.4 = return-to-page (asymmetric recovery cost). Survey captured: Blade+Alpine stack, form-POST vs AJAX modality split, existing partial 419 handler baseline. §2.2 return target via hidden _return_to field (survives POST body when session dies) → Referer → dashboard, all same-origin GET validated. §2.3 AJAX unifies to /login?expired with scoped localStorage preserve for draft + payment-note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every 419 now routes through a session-expiry handler instead of falling through to Laravel's default page. Full-page posts redirect to /login?expired; AJAX gets a JSON redirect hint that a shared axios interceptor + fetch wrapper act on — one recovery story for both. Return target resolves _return_to (POST body, survives a dead session) → Referer → dashboard, validated same-origin GET via ReturnToResolver, so the mutation is never replayed. A global submit-listener stamps _return_to onto every POST form. url.intended wins over first-login persona routing so users land back where they were. Unsaved text in the two input-bearing AJAX spots (invoice draft, payment note) is preserved across the bounce via localStorage. Open: GET-idle-expiry banner (§3.2 decision), localStorage eyeball (§4.6). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET-idle expiry stays on Laravel's auth-middleware redirect (returns to intended page, no expired banner); banner reserved for the 419 path. Only §4.6 localStorage eyeball QA remains open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A GET to a protected page after idle expiry was redirecting to a plain /login with no signal — an invisible logout. Now redirectGuestsTo sends returning users (those carrying the cz_returning marker dropped at login, forgotten at logout) to /login?expired so they get the banner, while never-authenticated first visits still land on a plain /login. The marker is a non-sensitive boolean, exempted from cookie encryption so it reads as plain text. Return-to-page already worked here via Laravel's stashed url.intended. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§4.7 localStorage restore eyeball-verified. Rename strategy doc to x19.2_, mark Phase 2 done in the milestone rollup + exit criteria. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
§4.7 localStorage restore eyeball-verified; doc Status → Complete; Phase 2 checked off in the milestone rollup, Current Focus, and exit criteria. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 113e69cc5b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| image: 'sail-8.4/app' | ||
| command: > | ||
| sh -c "php artisan schedule:work" | ||
| sh -c "until mysqladmin ping -hmysql -usail -ppassword --silent; do echo 'Waiting for MySQL for scheduler...'; sleep 2; done; echo 'MySQL is ready for scheduler.'; exec php artisan schedule:work" |
There was a problem hiding this comment.
Use configured DB credentials in wait loops
When DB_PASSWORD is anything other than the literal password—including the checked-in .env.example, where it is empty—this new wait loop never exits even after depends_on: condition: service_healthy passes, so the scheduler stays stuck instead of starting; the queue worker has the same hard-coded -ppassword on line 55. The mysqladmin docs define -p[password] as the password value sent to the server, so this needs to use the Compose-provided DB password (or skip-password for the empty-password case) rather than a fixed value.
Useful? React with 👍 / 👎.
CyberCreek LLC (parent) Articles filed 2026-06-08. Rework Phase 3 to form the parent first, then CryptoZing LLC with CyberCreek as sole member — building the parent-subsidiary structure from the start to skip a later membership transfer. Two formations / EINs / publications; operating account stays in CryptoZing LLC's name. Timing watch-item: CryptoZing name-lock vs MS21 (2026-07-15) — name-lock is Articles-approved, not full publication, and parallel tracks insulate MS19, so not pre-slipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
MS19 Phase 2 (Auth/Password Policy Hardening). Replaces Laravel's raw 419 page with a graceful session-expiry → login flow that returns the user to the page they were on after re-auth. Spec:
x19.2_AUTH_HARDENING.md.What changed
$renderSessionExpiredinbootstrap/app.php): full-page requests redirect to/login?expired; AJAX requests get a JSON redirect hint that a shared axios interceptor +fetchwrapper act on — one recovery story for both modalities.App\Support\ReturnToResolverresolves the target as_return_to(POST body) →Referer→ dashboard, validated as a same-origin GET route, so a POST/PATCH/DELETE is never the target and the mutation is never replayed. A global submit-listener stamps_return_toonto every POST form.url.intendedwins over first-login persona routing so users land back where they were.redirectGuestsTosends returning users (those carrying thecz_returningmarker cookie, dropped at login / cleared at logout) to/login?expiredfor the banner, while never-authenticated first visits get a plain/login.localStorageacross the bounce and restore it on return./loginonly when?expired.Testing
SessionExpiryRedirectTest— 10 cases: form + AJAX 419, off-origin / POST-only fallbacks, guest-form, GET-idle marker behavior, login/logout cookie lifecycle, banner gate, return-to-page landing.localStoragerestore eyeball-verified in-browser (outside feature-test reach).Also in this branch
scheduler+queueworkers wait for healthy MySQL before starting (no cold-boot crash-loop).19.4strategy doc: cross-reference to Custom branded error pages (404, 500, 503, 429) — only 403 exists today #96 (custom branded error pages — gap surfaced during this work).🤖 Generated with Claude Code