Files
hanzo.ai/app/(marketing)/auth/initiate/page.tsx
zeekay bda16252a7 auth, auto, base, blog, modules: the words go through @hanzo/i18n
The sign-in callback, initiate and logout pages, authz, auto, the
connectors page, base, the blog index and its three posts, and the Go
module page wrap their copy: server pages with <T>, client pages with
t(), and copy held in data with phrase(). Figures in the posts' prose
are values; product names, code and IAM's own codes stay as written.
Models says its two sentences whole ("{n}+ models", "every model on
the gateway"); the noun prop no caller passed is gone.
2026-10-22 18:13:04 -07:00

60 lines
2.5 KiB
TypeScript

'use client'
/**
* Third-party-initiated login (OIDC Core §4): `/auth/initiate?iss=…&login_hint=…`.
*
* IAM's support flow sends a SuperAdmin here with a one-time `login_hint`
* (hanzoai/iam internal/oidc/impersonate.go). It is a support sign-in or nothing:
* `iss` must be the issuer this site signs in at and the hint an impersonation
* hint (`initiation`). The sign-in started is a fresh one — the SDK's own state and
* PKCE, as the client this site signs in as — carrying the hint unchanged, through
* the same helper /login uses. The session in this browser is replaced only by a
* successful callback, so a link can never sign anybody out; the callback lands a
* support session on `/`.
*
* The hint is never drawn and never logged: it leaves the address in a layout
* effect, before any effect that reports the page reads it, and the edge sends
* `/auth/*` with `Referrer-Policy: no-referrer` (lib/edge.ts `NOREFERRER`), so the
* page's own stylesheet and script requests, made before that, do not carry it.
*/
import { useLayoutEffect, useRef, useState } from 'react'
import { useIam } from '@hanzo/iam/react'
import { phrase } from '@hanzo/i18n'
import { useI18n } from '@hanzo/i18n/react'
import { Action } from '@hanzo/ui/marketing'
import { Focus, LARGE, Wait } from '@/components/shell/focus'
import { useSignin } from '@/lib/auth/signin'
import { initiation, issuer } from '@/lib/auth/impersonation'
export default function Initiate() {
const { config } = useIam()
const { t } = useI18n()
const signin = useSignin()
const [refused, setRefused] = useState<string | null>(null)
// Once per page: the address is read and emptied by the first run.
const started = useRef(false)
useLayoutEffect(() => {
if (started.current) return
started.current = true
const search = window.location.search
window.history.replaceState(null, '', window.location.pathname)
issuer(config.serverUrl)
.then((iss) => {
const asked = initiation(search, iss)
if ('refused' in asked) return setRefused(asked.refused)
return signin({ additionalParams: { login_hint: asked.hint } })
})
.catch(() => setRefused(phrase('This sign-in could not be started.')))
}, [config.serverUrl, signin])
if (!refused) return <Wait title={t('Signing in')} />
return (
<Focus id="refused" title={t(refused)}>
<Action render="button" type="button" fill onClick={() => window.location.assign('/')} {...LARGE}>
{t('Go to Hanzo')}
</Action>
</Focus>
)
}