139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
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 },
|
||
);
|
||
}
|
||
}
|