Files
external-app/app/api/callback/route.ts
2026-07-14 15:21:28 +08:00

139 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { NextResponse } from "next/server";
/**
* SSO 回调接口
*
* OAuth2 授权码流程:用一次性授权 code 向真实后端换取 access_token
* 然后将 token / 用户信息写入 Cookie 建立本地会话。
*
* ⚠️ client_secret 仅在服务端 envSSO_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_infobasis 习惯用 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 },
);
}
}