mirror of
https://github.com/hanzoai/bootnode.git
synced 2026-10-25 02:31:30 +00:00
Names the two halves for what they are. The repo already holds both (12407e9 merged bootnode/bootnode into this history); this makes the frontend directory say so, so the layout reads the way the owner describes the system: backend and frontend separate, one repo. git mv throughout — 187 renames, history preserved. Every path reference follows: hanzo.yml build context and dockerfile, compose.yml, compose.prod.yml, start.sh, LLM.md, infra/k8s/web-deployments.yaml, infra/cloud/deploy.sh, and the Dockerfile's own COPY paths. Image and container names (bootnode-web, the -web tag suffix) are deliberately NOT renamed here: those are a deploy contract the App CR and ingress bind to on port 3001, and breaking them is a separate, staged change. Console builds green after the move; the Go tree builds. Co-authored-by: Hanzo Dev <[email protected]>
77 lines
2.4 KiB
TypeScript
77 lines
2.4 KiB
TypeScript
// OAuth Callback Handler for Hanzo IAM
|
|
"use client"
|
|
|
|
import { Suspense, useEffect, useState } from "react"
|
|
import { useRouter } from "next/navigation"
|
|
import { useAuth } from "@/lib/auth"
|
|
import { getIam, takeReturnUrl } from "@/lib/iam"
|
|
import { EmptyState, ErrorState, Loading } from "@/components/ui/states"
|
|
|
|
function CallbackContent() {
|
|
const [status, setStatus] = useState<"loading" | "success" | "error">("loading")
|
|
const [error, setError] = useState("")
|
|
const router = useRouter()
|
|
const { loginWithToken } = useAuth()
|
|
|
|
useEffect(() => {
|
|
handleCallback()
|
|
}, [])
|
|
|
|
async function handleCallback() {
|
|
try {
|
|
// Canonical PKCE exchange (HIP-0111): the IAM SDK validates state,
|
|
// exchanges the authorization code at the token endpoint with the stored
|
|
// PKCE verifier, and returns tokens. The resulting IAM access token is
|
|
// the session bearer the bootnode API verifies against IAM JWKS.
|
|
const tokens = await getIam().handleCallback()
|
|
await loginWithToken(tokens.accessToken)
|
|
|
|
setStatus("success")
|
|
|
|
// Redirect to the stashed destination after successful login.
|
|
const returnUrl = takeReturnUrl()
|
|
setTimeout(() => {
|
|
router.push(returnUrl)
|
|
}, 1500)
|
|
|
|
} catch (err) {
|
|
console.error("Auth callback error:", err)
|
|
setError(err instanceof Error ? err.message : "Authentication failed")
|
|
setStatus("error")
|
|
|
|
// Redirect to login after error
|
|
setTimeout(() => {
|
|
router.push("/login")
|
|
}, 3000)
|
|
}
|
|
}
|
|
|
|
// Three states, three shared primitives — no bespoke tick/cross SVGs, and no
|
|
// `bg-green-100` / `text-red-600` literals, which were the only two colours
|
|
// in the app that ignored the token layer and so stayed light-mode green and
|
|
// red on a black page.
|
|
if (status === "loading") return <Loading full label="Completing sign-in…" />
|
|
|
|
if (status === "error") {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<ErrorState title="Sign-in failed" description={`${error} Returning you to the login page…`} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center">
|
|
<EmptyState title="Signed in" description="Taking you to your dashboard…" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default function AuthCallback() {
|
|
return (
|
|
<Suspense fallback={<Loading full />}>
|
|
<CallbackContent />
|
|
</Suspense>
|
|
)
|
|
}
|