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
|
//: One door. Accounts are made at the identity provider, so there is no
|
||||||
//: second mode for this to be in.
|
//: second mode for this to be in.
|
||||||
const [authOpen, setAuthOpen] = useState(false)
|
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 (
|
return (
|
||||||
<div className="lp-page">
|
<div className="lp-page">
|
||||||
|
|
||||||
{authOpen && <AuthModal onClose={() => setAuthOpen(false)} />}
|
{authOpen && !ssoOnly && <AuthModal onClose={() => setAuthOpen(false)} />}
|
||||||
|
|
||||||
<Navbar onSignIn={() => setAuthOpen(true)} />
|
<Navbar onSignIn={signIn} />
|
||||||
|
|
||||||
{/* ── Hero ───────────────────────────────────────────────────────────── */}
|
{/* ── Hero ───────────────────────────────────────────────────────────── */}
|
||||||
<section className="lp-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
|
{/* One button. "Create an account" led to a form that no longer
|
||||||
exists — an account is made by following an invitation from
|
exists — an account is made by following an invitation from
|
||||||
the identity provider, not from here. */}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -716,7 +734,7 @@ export default function LandingPage() {
|
||||||
🏥 PedsHub<span>© {new Date().getFullYear()}</span>
|
🏥 PedsHub<span>© {new Date().getFullYear()}</span>
|
||||||
</span>
|
</span>
|
||||||
<div className="lp-footer-links">
|
<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>
|
<a href="https://app.pedshub.com" target="_blank" rel="noopener noreferrer">Clinical Tools</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -141,9 +141,16 @@ describe('the auth modal', () => {
|
||||||
expect(screen.queryByRole('form', { name: 'Sign in' })).not.toBeInTheDocument()
|
expect(screen.queryByRole('form', { name: 'Sign in' })).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shows the provider and nothing else on an SSO-only site', async () => {
|
it('goes straight to the provider on an SSO-only site, with no modal', async () => {
|
||||||
// And nothing at all before the answer arrives: drawing the email form
|
// A box whose entire content is one link is a step that exists to be
|
||||||
// and then replacing it is a flash of a way in that does not exist.
|
// 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({
|
api.get.mockImplementation(url => Promise.resolve({
|
||||||
data: url === '/public/stats' ? STATS
|
data: url === '/public/stats' ? STATS
|
||||||
: url === '/auth/sso/config'
|
: url === '/auth/sso/config'
|
||||||
|
|
@ -151,10 +158,13 @@ describe('the auth modal', () => {
|
||||||
: {},
|
: {},
|
||||||
}))
|
}))
|
||||||
mount()
|
mount()
|
||||||
await userEvent.click(screen.getByRole('button', { name: 'Open login' }))
|
const open = await screen.findByRole('button', { name: 'Open login' })
|
||||||
expect(await screen.findByRole('link', { name: /Sign in with PedsHub SSO/ }))
|
// Given a tick for the config to arrive before the click is judged.
|
||||||
.toHaveAttribute('href', '/api/auth/sso/login')
|
await waitFor(() => expect(api.get).toHaveBeenCalledWith('/auth/sso/config'))
|
||||||
expect(screen.queryByRole('form', { name: 'Sign in' })).toBeNull()
|
await userEvent.click(open)
|
||||||
expect(screen.queryByText(/signs in through/)).toBeNull()
|
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