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>
)
}