feat: implement workspace session management and enhance API proxy handling
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user