项目改名 sso-mock → sso-portal: - 重命名仓库目录 sso-mock/ → sso-portal/ - package.json name 改 sso-portal - app/layout.tsx title/description 改 SSO Portal - 源码 [sso-mock] console.log 前缀、注释全部 → [sso-portal] - 文档(README、ADAPTATION_GUIDE)、env 同步 - 生产 svc 名 sso-mock-svc → sso-portal-svc(external-app 引用同步) SSO tenant 改环境变量驱动: - 新增 SSO_TENANT env(默认 cowarobot) - login/route.ts 顶部 const SSO_TENANT,ssoBody 显式使用 - callSsoApi 改回纯透传(不再硬编码覆盖 tenant) - external-app 同步用 SSO_TENANT,命名对齐 external-app 登录态改为基于 token: - page.tsx 读 token cookie 判定 authenticated(不再依赖 user cookie) - DashboardPage 用 authenticated 决定跳转,user 为 null 时优雅兜底展示 - 解决「200 后又跳回 SSO」bug(access_token 接口不返回 user) sso-portal 缺 SSO 参数体验优化: - 缺 app_id/app_url 时 Alert 提示 + 禁用所有输入和登录按钮 - components/login/index.tsx 加 missingSsoParams 检测 - Alert 样式两排完整显示 部署脚本(参考 uirefbase): - 新增 build.sh(git → pnpm install → build → docker build → tag → push) - 新增 .gitlab-ci.yml(main 分支触发 build.sh prod) - 新增 Dockerfile(standalone 模式,端口 5501) - package.json 加 env-cmd 依赖 + build:stage/build 脚本 - next.config.ts 保持 output: 'standalone' 代码清理: - 删除 utils/code-store.ts - 删除 app/api/auth/verify/(连同目录) - 移除旧的 codeStore / generateCode 逻辑 文档: - ADAPTATION_GUIDE.md:完整重写对齐真实后端三个 SSO 接口(/sso/account、/sso/phone/smscode、/sso/access_token) - 新增 INTEGRATION_GUIDE.md(从 external-app 迁移过来作为通用接入指南) - README.md 同步更新(sso-portal 仓库门面 + 文档索引) - external-app/README.md 加交叉链接指向 sso-portal 的接入文档
131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
import CryptoJS from "crypto-js";
|
||
import { NextResponse } from "next/server";
|
||
|
||
const CODE_CRYPTO_KEY = "k#8Mp$2Qr&9Tz@4W";
|
||
|
||
const SHARED_API = (
|
||
process.env.NEXT_PUBLIC_SHARED_API_PATH || "http://172.16.115.31:6610"
|
||
).replace(/\/+$/, "");
|
||
|
||
// SSO 接口的租户标识(account / phone-smscode / access_token 共用,由环境变量驱动)
|
||
const SSO_TENANT = process.env.SSO_TENANT || "cowarobot";
|
||
|
||
function decrypt(encrypted: string): string {
|
||
try {
|
||
const bytes = CryptoJS.AES.decrypt(encrypted, CODE_CRYPTO_KEY);
|
||
return bytes.toString(CryptoJS.enc.Utf8);
|
||
} catch {
|
||
return encrypted;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 调用真实后端 SSO 端点。
|
||
* 透传响应(含后端签发的一次性授权 code),不再自造 code。
|
||
*/
|
||
async function callSsoApi(path: string, body: object) {
|
||
const url = `${SHARED_API}${path}`;
|
||
console.log("[sso-portal] →", url, body);
|
||
|
||
const res = await fetch(url, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(body),
|
||
});
|
||
|
||
const text = await res.text();
|
||
let data: any;
|
||
try {
|
||
data = JSON.parse(text);
|
||
} catch {
|
||
throw new Error(`后端返回非 JSON [${res.status}]: ${text.slice(0, 200)}`);
|
||
}
|
||
|
||
return { data, status: res.status };
|
||
}
|
||
|
||
// ─── POST /api/login ───
|
||
// 与 uirefbase 保持一致:客户端发 JSON.stringify(encrypt(...))
|
||
// req.json() 去掉外层引号得到加密字符串,decrypt 解密得到原始 JSON
|
||
export async function POST(req: Request) {
|
||
try {
|
||
const decryptData = await req.json(); // JSON 字符串 → 去掉引号 → 加密原文
|
||
const body = JSON.parse(decrypt(decryptData));
|
||
|
||
const type = body?.type;
|
||
console.log(
|
||
"[sso-portal] login | type:",
|
||
type,
|
||
"| body keys:",
|
||
Object.keys(body || {}),
|
||
);
|
||
|
||
if (!type) {
|
||
return NextResponse.json({
|
||
error: "缺少 type 字段",
|
||
debug_type: typeof type,
|
||
});
|
||
}
|
||
|
||
const client_id = body.client_id;
|
||
const client_redirect_uri = body.client_redirect_uri;
|
||
|
||
// OAuth2 授权码流程:client_id / client_redirect_uri 为必填(授权 code 必须绑定应用)
|
||
if (!client_id || !client_redirect_uri) {
|
||
return NextResponse.json({
|
||
error: "缺少 client_id 或 client_redirect_uri,无法签发授权 code",
|
||
});
|
||
}
|
||
|
||
let path: string;
|
||
let ssoBody: Record<string, unknown>;
|
||
|
||
if (type === "account") {
|
||
path = "/api/v1/basis/sso/account";
|
||
ssoBody = {
|
||
account: body.account,
|
||
password: body.password,
|
||
tenant: SSO_TENANT,
|
||
client_id,
|
||
client_redirect_uri,
|
||
};
|
||
} else if (type === "phone") {
|
||
path = "/api/v1/basis/sso/phone/smscode";
|
||
ssoBody = {
|
||
phone: body.phone,
|
||
smscode: body.verify_code,
|
||
tenant: SSO_TENANT,
|
||
client_id,
|
||
client_redirect_uri,
|
||
};
|
||
} else {
|
||
console.error("[sso-portal] 未知 type:", type);
|
||
return NextResponse.json({ error: `不支持的登录方式: ${type}` });
|
||
}
|
||
|
||
const { data: ssoRes, status } = await callSsoApi(path, ssoBody);
|
||
console.log(
|
||
"[sso-portal] backend status:",
|
||
status,
|
||
"| res:",
|
||
JSON.stringify(ssoRes).slice(0, 300),
|
||
);
|
||
|
||
// ⚠️ code 在新流程里是「授权码字符串」,不再是业务状态码。
|
||
// 兼容平铺 { code } 与外壳 { code:200, data:{ code } } 两种响应。
|
||
const authCode = ssoRes?.data?.code ?? ssoRes?.code;
|
||
if (!authCode || typeof authCode !== "string") {
|
||
const message =
|
||
ssoRes?.message || ssoRes?.msg || "登录失败,后端未返回授权 code";
|
||
console.log("[sso-portal] no auth code:", message);
|
||
return NextResponse.json({ error: message });
|
||
}
|
||
|
||
console.log("[sso-portal] auth code →", authCode);
|
||
return NextResponse.json({ code: authCode });
|
||
} catch (err: any) {
|
||
console.error("[sso-portal] login error:", err.message);
|
||
return NextResponse.json({ error: err?.message || "登录失败" });
|
||
}
|
||
}
|