101 lines
3.2 KiB
TypeScript
101 lines
3.2 KiB
TypeScript
import { redirect } from "next/navigation"
|
|
import { AlertCircle, ShieldCheck } from "lucide-react"
|
|
|
|
import { applyHandoff } from "@/modules/auth/auth.actions"
|
|
|
|
type SearchParams = { ws?: string; api?: string }
|
|
|
|
export default async function ActivatePage(props: {
|
|
params: Promise<{ token: string }>
|
|
searchParams: Promise<SearchParams>
|
|
}) {
|
|
const { token } = await props.params
|
|
const { ws, api } = await props.searchParams
|
|
|
|
if (!ws || !api) {
|
|
return (
|
|
<ActivationError
|
|
title="Activation link is incomplete"
|
|
detail="The link is missing the workspace identifier or backend URL. Please open the garage again from the SaaS dashboard."
|
|
/>
|
|
)
|
|
}
|
|
|
|
let parsed: URL
|
|
try {
|
|
parsed = new URL(api)
|
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
throw new Error("Backend URL must be http(s).")
|
|
}
|
|
} catch {
|
|
return (
|
|
<ActivationError
|
|
title="Activation link is invalid"
|
|
detail="The backend URL provided in the activation link is malformed."
|
|
/>
|
|
)
|
|
}
|
|
|
|
const endpoint = `${parsed.toString().replace(/\/$/, "")}/api/saas/handoff/exchange`
|
|
|
|
let response: Response
|
|
try {
|
|
response = await fetch(endpoint, {
|
|
method: "POST",
|
|
cache: "no-store",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
"X-Workspace-UUID": ws,
|
|
},
|
|
body: JSON.stringify({
|
|
handoff_token: token,
|
|
saas_workspace_uuid: ws,
|
|
}),
|
|
})
|
|
} catch (err) {
|
|
return (
|
|
<ActivationError
|
|
title="Cannot reach the garage backend"
|
|
detail={(err as Error).message}
|
|
/>
|
|
)
|
|
}
|
|
|
|
let payload: { token?: string; user?: any; message?: string } = {}
|
|
try {
|
|
payload = await response.json()
|
|
} catch {
|
|
// ignore — handled below
|
|
}
|
|
|
|
if (!response.ok || !payload.token || !payload.user) {
|
|
return (
|
|
<ActivationError
|
|
title="Activation failed"
|
|
detail={payload.message ?? `HTTP ${response.status}`}
|
|
/>
|
|
)
|
|
}
|
|
|
|
await applyHandoff(payload.token, payload.user, parsed.toString().replace(/\/$/, ""), ws)
|
|
|
|
redirect("/")
|
|
}
|
|
|
|
function ActivationError({ title, detail }: { title: string; detail: string }) {
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100 p-6">
|
|
<div className="max-w-md w-full text-center space-y-3 bg-white rounded-xl shadow-sm border p-8">
|
|
<AlertCircle className="size-12 mx-auto text-rose-500" />
|
|
<h1 className="text-lg font-semibold">{title}</h1>
|
|
<p className="text-sm text-muted-foreground">{detail}</p>
|
|
<p className="text-xs text-muted-foreground pt-2 flex items-center justify-center gap-1.5">
|
|
<ShieldCheck className="size-3.5" />
|
|
Re-open the garage from the SaaS dashboard to get a fresh link.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|