import { NextResponse } from "next/server"; /** * SSO 回调接口 * * OAuth2 授权码流程:用一次性授权 code 向真实后端换取 access_token, * 然后将 token / 用户信息写入 Cookie 建立本地会话。 * * ⚠️ client_secret 仅在服务端 env(SSO_CLIENT_SECRET),绝不下发浏览器。 */ const BACKEND = ( process.env.SSO_BACKEND_API || "http://172.16.115.31:6610" ).replace(/\/+$/, ""); export async function POST(req: Request) { try { const { code } = await req.json(); if (!code) { return NextResponse.json({ error: "缺少 code 参数" }, { status: 400 }); } const clientId = process.env.NEXT_PUBLIC_SSO_APP_ID; const clientSecret = process.env.SSO_CLIENT_SECRET; const tenant = process.env.SSO_TENANT || "cowarobot"; if (!clientId || !clientSecret) { console.error( "[external-app] 缺少 NEXT_PUBLIC_SSO_APP_ID 或 SSO_CLIENT_SECRET", ); return NextResponse.json( { error: "服务端 SSO 配置缺失" }, { status: 500 }, ); } // 用授权 code 换 access_token(标准 OAuth2) const tokenRes = await fetch(`${BACKEND}/api/v1/basis/sso/access_token`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code, grant_type: "authorization_code", tenant, }), }); const data = await tokenRes.json().catch(() => ({})); console.log( "[external-app] access_token 响应 status:", tokenRes.status, "| body:", JSON.stringify(data).slice(0, 500), ); if (!tokenRes.ok) { const message = data?.message || data?.msg || "换 token 失败"; console.error( "[external-app] access_token error:", tokenRes.status, message, ); return NextResponse.json({ error: message }, { status: 401 }); } // 兼容平铺 / 外壳两种响应;字段名兼容 user / user_info(basis 习惯用 user_info) const token = data?.access_token ?? data?.data?.access_token; const refreshToken = data?.refresh_token ?? data?.data?.refresh_token; if (!token) { console.error( "[external-app] access_token 响应中无 token:", JSON.stringify(data).slice(0, 300), ); return NextResponse.json( { error: "后端未返回 access_token" }, { status: 502 }, ); } const infoRes = await fetch( `${BACKEND}/api/v1/basis/sso/user_info?access_token=${token}`, { method: "GET", }, ); const infoData = await infoRes.json().catch(() => ({})); const user = infoData?.data; console.log( "[external-app] 提取 → token:", !!token, "| refresh_token:", !!refreshToken, "| user:", !!user, user ? `(${Object.keys(user).join(",")})` : "(无 — 登录态无法建立,首页会跳回 sso-portal)", ); // 写入 Cookie 建立会话 const response = NextResponse.json({ success: true }); response.cookies.set("token", token, { httpOnly: true, path: "/", maxAge: 86400, }); if (refreshToken) { response.cookies.set("refresh_token", refreshToken, { httpOnly: true, path: "/", maxAge: 86400 * 7, }); } if (user) { response.cookies.set("user", JSON.stringify(user), { httpOnly: false, // 客户端需要读取用于展示 path: "/", maxAge: 86400, }); } return response; } catch (err: any) { console.error("[external-app] callback error:", err?.message); return NextResponse.json( { error: err?.message || "回调处理失败" }, { status: 500 }, ); } }