Files
jiapuapp/utils/auth-verification.js
T
2026-07-27 06:50:23 +08:00

141 lines
5.1 KiB
JavaScript

export const AUTH_VERIFICATION_OPERATION = Object.freeze({
PASSWORD_LOGIN: "password-login",
SMS_LOGIN: "sms-login",
REGISTER: "register",
FORGOT_PASSWORD: "forgot-password",
});
const SUPPORTED_TAC_TYPES = new Set([
"SLIDER",
"ROTATE",
"CONCAT",
"WORD_IMAGE_CLICK",
]);
const PHONE_PATTERN = /^1[3-9]\d{9}$/;
export const isAuthPhone = (value) =>
typeof value === "string" && PHONE_PATTERN.test(value);
const contractError = (message, code) => {
const error = new Error(message);
error.code = code;
return error;
};
const requireText = (value, label) => {
if (typeof value !== "string" || !value.trim()) {
throw contractError(`${label}不能为空`, "AUTH_TAC_CONTRACT_INVALID");
}
return value.trim();
};
export const assertSmsCode = (value) => {
if (typeof value !== "string" || !/^\d{4}$/.test(value)) {
throw contractError("请输入 4 位短信验证码", "SMS_CODE_INVALID");
}
return value;
};
const assertAuthVerificationOperation = (operationCode) => {
if (!Object.values(AUTH_VERIFICATION_OPERATION).includes(operationCode)) {
throw contractError("认证动作不属于当前认证合同", "AUTH_OPERATION_INVALID");
}
return operationCode;
};
export const normalizeCaptchaRequirement = (value) => {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw contractError("行为验证策略响应无效", "AUTH_TAC_REQUIREMENT_INVALID");
}
const sceneCode = requireText(value.sceneCode, "行为验证场景");
if (value.required === false) {
return { required: false, sceneCode };
}
if (value.required !== true) {
throw contractError(
"行为验证策略缺少 required 布尔值",
"AUTH_TAC_POLICY_INCOMPLETE",
);
}
const providerCode = requireText(value.providerCode, "行为验证服务商").toUpperCase();
if (providerCode !== "TIANAI") {
throw contractError("当前仅支持 TIANAI 行为验证服务", "AUTH_TAC_PROVIDER_UNSUPPORTED");
}
const captchaType = requireText(value.captchaType, "行为验证码类型").toUpperCase();
if (!SUPPORTED_TAC_TYPES.has(captchaType)) {
throw contractError("服务端返回了客户端不支持的验证码类型", "AUTH_TAC_TYPE_UNSUPPORTED");
}
const ttlSeconds = Number(value.ttlSeconds);
if (!Number.isInteger(ttlSeconds) || ttlSeconds < 1) {
throw contractError("行为验证策略缺少有效期", "AUTH_TAC_TTL_INVALID");
}
return { required: true, providerCode, captchaType, sceneCode, ttlSeconds };
};
export const createTacRenderContext = ({
requestId,
baseUrl,
clientId,
tenantId,
operationCode,
subject,
requirement,
}) => {
const normalizedRequirement = normalizeCaptchaRequirement(requirement);
if (!normalizedRequirement.required) {
throw contractError("当前认证动作无需行为验证", "AUTH_TAC_NOT_REQUIRED");
}
const normalizedBaseUrl = requireText(baseUrl, "后端地址").replace(/\/+$/, "");
if (!/^https:\/\/[^/]+/i.test(normalizedBaseUrl)) {
throw contractError("行为验证只允许使用 HTTPS 后端地址", "AUTH_TAC_HTTPS_REQUIRED");
}
const normalizedSubject = requireText(subject, "手机号");
if (!isAuthPhone(normalizedSubject)) {
throw contractError("请输入正确的手机号", "AUTH_PHONE_INVALID");
}
return {
requestId: requireText(requestId, "验证请求标识"),
baseUrl: normalizedBaseUrl,
challengeUrl: `${normalizedBaseUrl}/genealogy/app/auth/verification/${assertAuthVerificationOperation(operationCode)}/challenge`,
verifyUrl: `${normalizedBaseUrl}/genealogy/app/auth/verification/${assertAuthVerificationOperation(operationCode)}/verify`,
clientId: requireText(clientId, "客户端标识"),
tenantId: requireText(tenantId, "租户标识"),
operationCode,
subject: normalizedSubject,
providerCode: normalizedRequirement.providerCode,
captchaType: normalizedRequirement.captchaType,
};
};
export const normalizeTacSuccess = (value, expectedRequestId) => {
if (!value || typeof value !== "object" || value.requestId !== expectedRequestId) {
throw contractError("行为验证结果已过期或与当前请求不匹配", "AUTH_TAC_RESULT_STALE");
}
const validToken = requireText(value.validToken, "行为验证票据");
const expireSeconds = Number(value.expireSeconds);
if (!Number.isInteger(expireSeconds) || expireSeconds < 1) {
throw contractError("行为验证票据缺少有效期", "AUTH_TAC_RESULT_INVALID");
}
return { requestId: value.requestId, validToken, expireSeconds };
};
export const isSmsDeliveryOutcomeUnknown = (error) => {
if (!error || typeof error !== "object") return false;
if (
error.code === "REQUEST_TIMEOUT" ||
error.code === "NETWORK_ERROR" ||
error.code === "RESPONSE_INVALID" ||
error.code === "REQUEST_CANCELLED"
) {
return true;
}
if (error.code === "HTTP_ERROR" || error.code === "BUSINESS_ERROR") {
const status = Number(
error.code === "HTTP_ERROR" ? error.httpStatus : error.businessCode,
);
if (!Number.isInteger(status)) return true;
return !(status >= 400 && status < 500 && status !== 408);
}
return false;
};