feat: implement workspace session management and enhance API proxy handling

This commit is contained in:
Najjar\NajjarV02
2026-05-20 16:23:19 +04:00
parent 4f0a2f790f
commit ccb9eacd86
8 changed files with 410 additions and 71 deletions
@@ -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>
)
}
@@ -0,0 +1,97 @@
import { cookies } from "next/headers"
import { NextRequest, NextResponse } from "next/server"
const HOP_BY_HOP = new Set([
"host",
"connection",
"content-length",
"keep-alive",
"transfer-encoding",
"upgrade",
"te",
"trailer",
"proxy-authorization",
"proxy-authenticate",
])
async function forward(req: NextRequest, ctx: { params: Promise<{ path: string[] }> }) {
const cookieStore = await cookies()
const apiBase = cookieStore.get("api_base_url")?.value
const wsUuid = cookieStore.get("workspace_uuid")?.value
const token = cookieStore.get("auth_token")?.value
if (!apiBase || !wsUuid) {
return NextResponse.json(
{
message: "Workspace session not found. Please open your garage from the SaaS dashboard.",
code: "workspace_session_missing",
},
{ status: 412 },
)
}
const { path } = await ctx.params
const url = new URL(req.url)
// The OpenAPI clients keep `/api/...` in their route patterns, so when the
// client uses baseUrl=`/api/proxy` the resulting URL is
// `/api/proxy/api/customers`. Forward the path verbatim and rejoin so the
// upstream becomes `{apiBase}/api/customers`.
const target = `${apiBase.replace(/\/$/, "")}/${(path ?? []).join("/")}${url.search}`
const headers = new Headers()
req.headers.forEach((value, key) => {
if (!HOP_BY_HOP.has(key.toLowerCase()) && key.toLowerCase() !== "cookie") {
headers.set(key, value)
}
})
headers.set("Accept", "application/json")
headers.set("X-Workspace-UUID", wsUuid)
if (token) {
headers.set("Authorization", `Bearer ${token}`)
}
const init: RequestInit = {
method: req.method,
headers,
redirect: "manual",
}
if (req.method !== "GET" && req.method !== "HEAD") {
init.body = await req.arrayBuffer()
}
let upstream: Response
try {
upstream = await fetch(target, init)
} catch (err) {
return NextResponse.json(
{
message: "Failed to reach garage backend.",
code: "backend_unreachable",
error: (err as Error).message,
},
{ status: 502 },
)
}
const respHeaders = new Headers()
upstream.headers.forEach((value, key) => {
const k = key.toLowerCase()
if (HOP_BY_HOP.has(k) || k === "set-cookie") return
respHeaders.set(key, value)
})
return new NextResponse(upstream.body, {
status: upstream.status,
statusText: upstream.statusText,
headers: respHeaders,
})
}
export const GET = forward
export const POST = forward
export const PUT = forward
export const PATCH = forward
export const DELETE = forward
export const OPTIONS = forward
export const HEAD = forward