You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

94 lines
2.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { SignJWT, jwtVerify, type JWTPayload } from "jose";
import { JWT_EXPIRY_SECONDS } from "@/lib/constants/auth";
import { unauthorized } from "@/modules/auth/errors";
export const EMBED_DEMO_ROLE = "embed_demo" as const;
export const EMBED_DEMO_COOKIE = "embed_demo_session";
export const EMBED_DEMO_DEFAULT_PASSWORD = "123456";
/**
* 嵌入 Cookie 选项:生产环境 iframe跨站宿主如 CC须 SameSite=None + Secure
* 否则第三方 iframe 内登录态无法写入/回传。
*/
export function embedDemoCookieOptions(maxAgeSeconds?: number): {
httpOnly: boolean;
secure: boolean;
sameSite: "lax" | "none";
path: string;
maxAge?: number;
} {
const isProd = process.env.NODE_ENV === "production";
const forceNone =
process.env.EMBED_COOKIE_SAMESITE?.trim().toLowerCase() === "none";
const useNone = isProd || forceNone;
return {
httpOnly: true,
secure: useNone ? true : isProd,
sameSite: useNone ? "none" : "lax",
path: "/",
...(typeof maxAgeSeconds === "number" ? { maxAge: maxAgeSeconds } : {}),
};
}
/** 登录类型:账号密码 / API Key */
export type EmbedDemoLoginType = "password" | "api_key";
export type EmbedDemoJwtPayload = {
sub: string;
role: typeof EMBED_DEMO_ROLE;
loginType: EmbedDemoLoginType;
};
function normalizeLoginType(raw: unknown): EmbedDemoLoginType {
// 兼容旧 Cookie无 lt 声明):默认按账号密码处理
return raw === "api_key" ? "api_key" : "password";
}
function getJwtSecret(): Uint8Array {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("JWT_SECRET 未配置");
}
return new TextEncoder().encode(secret);
}
export function getEmbedDemoPassword(): string {
return process.env.EMBED_DEMO_PASSWORD?.trim() || EMBED_DEMO_DEFAULT_PASSWORD;
}
function toEmbedPayload(payload: JWTPayload): EmbedDemoJwtPayload {
if (typeof payload.sub !== "string" || payload.role !== EMBED_DEMO_ROLE) {
throw unauthorized("无效的演示会话");
}
return {
sub: payload.sub,
role: EMBED_DEMO_ROLE,
loginType: normalizeLoginType(payload.lt),
};
}
export async function signEmbedDemoToken(
customerId: string,
loginType: EmbedDemoLoginType = "password",
): Promise<string> {
return new SignJWT({ role: EMBED_DEMO_ROLE, lt: loginType })
.setProtectedHeader({ alg: "HS256" })
.setSubject(customerId)
.setIssuedAt()
.setExpirationTime(`${JWT_EXPIRY_SECONDS}s`)
.sign(getJwtSecret());
}
export async function verifyEmbedDemoToken(
token: string,
): Promise<EmbedDemoJwtPayload> {
try {
const { payload } = await jwtVerify(token, getJwtSecret(), {
algorithms: ["HS256"],
});
return toEmbedPayload(payload);
} catch {
throw unauthorized("演示会话无效或已过期");
}
}