Fortify sends every successful sign-in to config('fortify.home') — /dashboard.
That is why an operator signing in ended up in the customer portal, and once
the console has a hostname to itself it stops being merely confusing:
/dashboard does not exist on that host, so signing in would land on a 404.
Three outcomes now, decided by where the sign-in happened and who signed in:
the console for an operator on the console, the portal everywhere else, and for
a non-operator on the console the session is taken away again at the moment it
was created — a guard that merely refuses each page afterwards leaves that
session in the browser.
Four things the review caught, each of which would have left a hole:
- Fortify never reaches LoginResponse when a two-factor challenge was involved.
Binding only that one left exactly the accounts most likely to be operators
on the old behaviour, so both exits are bound and share one decision.
- The JSON branch ran before the authorization check, so a JSON client kept a
session a browser would have lost.
- The two exits do not share a JSON success contract — an ordinary sign-in
answers {"two_factor":false}, a completed challenge answers an empty 204 —
and merging them breaks a client keying off the status code.
- In shared mode everyone posts to /login, so the request never looks like the
console even when /admin is the destination. The intended URL is read too.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The marketing page was a generic centred hero in the system font stack: it
neither used the design system the console is built on nor looked like the
product it sells. Rebuilt around what CluPilot actually promises — security you
can produce evidence for — so the page reads as a controlled technical
document: a specification plate instead of a hero image, numbered sections, an
audit register with rhythm and proof per measure, and a price sheet rather than
floating cards. IBM Plex Serif joins Sans and Mono as the display voice, self
hosted as static files so the public page renders even without the Vite build.
The prices were written into the page by hand, and the page and the catalogue
had already drifted apart on three of four plans: the site advertised 249 € for
a plan the checkout charges 399 € for. The sheet now reads the catalogue, like
every other caller — including the currency, which is configurable and was also
hard-coded here.
The catalogue fails loudly on purpose; a public website must not. A broken or
overlapping catalogue is caught in the controller alone: the page still
renders, the sheet is replaced by "on request", and nobody is quoted a number
the checkout would not honour.
The sign-in and registration panels shared a copy-pasted orange gradient. They
now share one component and the same ink plate as the site, so the two halves
of the product no longer look like two companies.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>