import { NextRequest, NextResponse } from "next/server" /** * SaaS → garage-erp handoff landing. * * URL shape: * /activate/handoff/{handoff_token}?ws={workspace_uuid}&api={backend_url} * * Calls the backend's /api/saas/handoff/exchange with the token + workspace * UUID, then sets four cookies (auth_token, auth_user, workspace_uuid, * api_base_url) on the redirect response and lands the user on /. * * Implemented as a Route Handler (not a Page) because Next.js disallows * cookie writes inside Server Components — only Server Actions and Route * Handlers can mutate cookies. */ export async function GET( req: NextRequest, ctx: { params: Promise<{ token: string }> }, ) { const { token } = await ctx.params const url = new URL(req.url) const ws = url.searchParams.get("ws") const api = url.searchParams.get("api") if (!ws || !api) { return redirectToError(req, "Activation link is missing workspace or backend URL.") } 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 redirectToError(req, "Activation link has a malformed backend URL.") } const apiBase = parsed.toString().replace(/\/$/, "") const endpoint = `${apiBase}/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 redirectToError(req, `Cannot reach garage backend: ${(err as Error).message}`) } let payload: { token?: string; user?: any; message?: string } = {} try { payload = await response.json() } catch { // body is not JSON — fall through to error path } if (!response.ok || !payload.token || !payload.user) { return redirectToError(req, payload.message ?? `Exchange failed (HTTP ${response.status})`) } // Use a relative Location to keep the user on whichever host they came // in on (127.0.0.1 vs localhost). NextResponse.redirect(new URL("/", req.url)) // resolves to the Next dev server's canonical host which can differ from // the request's actual host, and cookies set here would not survive that // cross-host redirect. const res = new NextResponse(null, { status: 302, headers: { Location: "/" }, }) const isProd = process.env.NODE_ENV === "production" const sevenDays = 60 * 60 * 24 * 7 const cookieOptions = { path: "/", sameSite: "strict" as const, httpOnly: true, secure: isProd, maxAge: sevenDays, } res.cookies.set("auth_token", payload.token, cookieOptions) res.cookies.set("auth_user", JSON.stringify(payload.user), cookieOptions) res.cookies.set("workspace_uuid", ws, cookieOptions) res.cookies.set("api_base_url", apiBase, cookieOptions) return res } function redirectToError(req: NextRequest, reason: string) { const location = "/activate/error?reason=" + encodeURIComponent(reason) return new NextResponse(null, { status: 302, headers: { Location: location }, }) }