mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-30 14:58:08 +00:00
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.
60 lines
2.5 KiB
TypeScript
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>
|
|
)
|
|
}
|