diff --git a/frontend/src/pages/LandingPage.jsx b/frontend/src/pages/LandingPage.jsx
index b5ebb96..a7070b3 100644
--- a/frontend/src/pages/LandingPage.jsx
+++ b/frontend/src/pages/LandingPage.jsx
@@ -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 (
- {authOpen && setAuthOpen(false)} />}
+ {authOpen && !ssoOnly && setAuthOpen(false)} />}
- setAuthOpen(true)} />
+
{/* ── 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. */}
-
+
>}
diff --git a/frontend/src/pages/LandingPage.test.jsx b/frontend/src/pages/LandingPage.test.jsx
index a16b0cc..ad724ac 100644
--- a/frontend/src/pages/LandingPage.test.jsx
+++ b/frontend/src/pages/LandingPage.test.jsx
@@ -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)
})
+
})