fix: Sign in goes to the provider, not to a box containing one button
The landing page's Sign in opened a modal whose entire content was a single "Sign in with PedsHub SSO" link. That is a step that exists to be clicked through. Every sign-in control on the page — the header, the hero, the closing call to action — now goes straight to /api/auth/sso/login when the site is SSO-only. The modal is still built and still opens on a site that has a password door, which is the only thing it was ever for. /login is deliberately left as it is. It renders the one button rather than redirecting, because it is also where the provider sends somebody back when sign-in fails — ?error=sso_failed — and a page that redirected on sight would bounce them into the provider again, forever. Verified live: one click from the landing page lands on sso.pedshub.com's flow with the client id, callback, scope, state and nonce, and no modal is rendered on the way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
cdf4ab80b4
commit
a8cc0c40df
2 changed files with 40 additions and 12 deletions
|
|
@ -596,13 +596,31 @@ export default function LandingPage() {
|
|||
//: One door. Accounts are made at the identity provider, so there is no
|
||||
//: second mode for this to be in.
|
||||
const [authOpen, setAuthOpen] = useState(false)
|
||||
//: Where "Sign in" goes. On a site whose only way in is the provider there
|
||||
//: is nothing to ask first, so the button is the provider's door rather than
|
||||
//: a box containing one button — a modal whose entire content is a link is a
|
||||
//: step that exists to be clicked through.
|
||||
const [ssoOnly, setSsoOnly] = useState(false)
|
||||
useEffect(() => {
|
||||
let live = true
|
||||
api.get('/auth/sso/config')
|
||||
.then(res => {
|
||||
if (live) setSsoOnly(res.data?.sso_enabled === true && res.data?.sso_only === true)
|
||||
})
|
||||
.catch(() => {})
|
||||
return () => { live = false }
|
||||
}, [])
|
||||
const signIn = () => {
|
||||
if (ssoOnly) window.location.href = '/api/auth/sso/login'
|
||||
else setAuthOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lp-page">
|
||||
|
||||
{authOpen && <AuthModal onClose={() => setAuthOpen(false)} />}
|
||||
{authOpen && !ssoOnly && <AuthModal onClose={() => setAuthOpen(false)} />}
|
||||
|
||||
<Navbar onSignIn={() => setAuthOpen(true)} />
|
||||
<Navbar onSignIn={signIn} />
|
||||
|
||||
{/* ── Hero ───────────────────────────────────────────────────────────── */}
|
||||
<section className="lp-hero">
|
||||
|
|
@ -627,7 +645,7 @@ export default function LandingPage() {
|
|||
{/* One button. "Create an account" led to a form that no longer
|
||||
exists — an account is made by following an invitation from
|
||||
the identity provider, not from here. */}
|
||||
<button onClick={() => setAuthOpen(true)} className="btn btn-primary">Sign in</button>
|
||||
<button onClick={signIn} className="btn btn-primary">Sign in</button>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -716,7 +734,7 @@ export default function LandingPage() {
|
|||
🏥 PedsHub<span>© {new Date().getFullYear()}</span>
|
||||
</span>
|
||||
<div className="lp-footer-links">
|
||||
<button onClick={() => setAuthOpen(true)}>Sign In</button>
|
||||
<button onClick={signIn}>Sign In</button>
|
||||
<a href="https://app.pedshub.com" target="_blank" rel="noopener noreferrer">Clinical Tools</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -141,9 +141,16 @@ describe('the auth modal', () => {
|
|||
expect(screen.queryByRole('form', { name: 'Sign in' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows the provider and nothing else on an SSO-only site', async () => {
|
||||
// And nothing at all before the answer arrives: drawing the email form
|
||||
// and then replacing it is a flash of a way in that does not exist.
|
||||
it('goes straight to the provider on an SSO-only site, with no modal', async () => {
|
||||
// A box whose entire content is one link is a step that exists to be
|
||||
// clicked through. There is nothing to ask first when the provider is the
|
||||
// only way in.
|
||||
const go = vi.fn()
|
||||
const real = Object.getOwnPropertyDescriptor(window, 'location')
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
value: { get href() { return '' }, set href(v) { go(v) } },
|
||||
})
|
||||
api.get.mockImplementation(url => Promise.resolve({
|
||||
data: url === '/public/stats' ? STATS
|
||||
: url === '/auth/sso/config'
|
||||
|
|
@ -151,10 +158,13 @@ describe('the auth modal', () => {
|
|||
: {},
|
||||
}))
|
||||
mount()
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Open login' }))
|
||||
expect(await screen.findByRole('link', { name: /Sign in with PedsHub SSO/ }))
|
||||
.toHaveAttribute('href', '/api/auth/sso/login')
|
||||
expect(screen.queryByRole('form', { name: 'Sign in' })).toBeNull()
|
||||
expect(screen.queryByText(/signs in through/)).toBeNull()
|
||||
const open = await screen.findByRole('button', { name: 'Open login' })
|
||||
// Given a tick for the config to arrive before the click is judged.
|
||||
await waitFor(() => expect(api.get).toHaveBeenCalledWith('/auth/sso/config'))
|
||||
await userEvent.click(open)
|
||||
await waitFor(() => expect(go).toHaveBeenCalledWith('/api/auth/sso/login'))
|
||||
expect(screen.queryByRole('button', { name: 'Close' })).toBeNull()
|
||||
if (real) Object.defineProperty(window, 'location', real)
|
||||
})
|
||||
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue