197 lines
5.6 KiB
JavaScript
197 lines
5.6 KiB
JavaScript
import { ref } from "vue";
|
|
import {
|
|
createRequestController,
|
|
isRequestCancelled
|
|
} from "@/services/api/request-controller.js";
|
|
import { authApi } from "@/services/api/auth-service.js";
|
|
import { createAuthSmsCooldown } from "@/utils/auth/sms-cooldown.js";
|
|
import {
|
|
createTacRenderContext,
|
|
isAuthPhone,
|
|
isSmsDeliveryOutcomeUnknown,
|
|
normalizeCaptchaRequirement,
|
|
normalizeTacSuccess,
|
|
} from "@/utils/auth/verification.js";
|
|
import { runtimeConfig } from "@/utils/runtime-config.js";
|
|
|
|
export const useSmsVerification = ({
|
|
operationCode,
|
|
requestIdPrefix,
|
|
phone,
|
|
isActive,
|
|
showFeedback,
|
|
getErrorMessage = (error, fallback) => error?.message || fallback,
|
|
}) => {
|
|
if (!phone || typeof phone !== "object" || !("value" in phone)) {
|
|
throw new TypeError("phone must be a Vue ref");
|
|
}
|
|
if (
|
|
typeof isActive !== "function" ||
|
|
typeof showFeedback !== "function" ||
|
|
typeof getErrorMessage !== "function"
|
|
) {
|
|
throw new TypeError("isActive, showFeedback and getErrorMessage must be functions");
|
|
}
|
|
|
|
const tacVisible = ref(false);
|
|
const tacContext = ref(null);
|
|
const sendingCode = ref(false);
|
|
const cooldownSeconds = ref(0);
|
|
const sentPhone = ref("");
|
|
const captchaRequirementRequestController = createRequestController();
|
|
const smsDeliveryRequestController = createRequestController();
|
|
const smsCooldown = createAuthSmsCooldown({
|
|
operationCode,
|
|
onChange: (seconds) => {
|
|
cooldownSeconds.value = seconds;
|
|
},
|
|
});
|
|
let requestSequence = 0;
|
|
let disposed = false;
|
|
|
|
const canCommit = () => !disposed && isActive();
|
|
|
|
const closeTac = () => {
|
|
tacVisible.value = false;
|
|
tacContext.value = null;
|
|
};
|
|
|
|
const markSmsSent = (requestedPhone, message = "验证码已发送") => {
|
|
if (!canCommit()) return;
|
|
sentPhone.value = requestedPhone;
|
|
smsCooldown.start();
|
|
showFeedback(message);
|
|
};
|
|
|
|
const requestCode = async () => {
|
|
if (sendingCode.value || cooldownSeconds.value > 0) return false;
|
|
if (!isAuthPhone(phone.value)) return false;
|
|
|
|
const requestedPhone = phone.value;
|
|
let smsDeliveryStarted = false;
|
|
sendingCode.value = true;
|
|
try {
|
|
const requirementResponse = await authApi.getCaptchaRequirement(
|
|
{ operationCode, subject: requestedPhone },
|
|
{ requestController: captchaRequirementRequestController },
|
|
);
|
|
if (!canCommit()) return false;
|
|
if (phone.value !== requestedPhone) {
|
|
throw new Error("手机号已变化,请重新获取验证码");
|
|
}
|
|
const requirement = normalizeCaptchaRequirement(requirementResponse);
|
|
if (!requirement.required) {
|
|
smsDeliveryStarted = true;
|
|
await authApi.sendSmsCode(
|
|
{ operationCode, phone: requestedPhone },
|
|
{ requestController: smsDeliveryRequestController },
|
|
);
|
|
markSmsSent(requestedPhone);
|
|
return true;
|
|
}
|
|
|
|
requestSequence += 1;
|
|
tacContext.value = createTacRenderContext({
|
|
requestId: `${requestIdPrefix}-${requestSequence}`,
|
|
baseUrl: runtimeConfig.baseUrl,
|
|
clientId: runtimeConfig.clientId,
|
|
tenantId: runtimeConfig.tenantId,
|
|
operationCode,
|
|
subject: requestedPhone,
|
|
requirement,
|
|
});
|
|
tacVisible.value = true;
|
|
return true;
|
|
} catch (error) {
|
|
if (canCommit()) {
|
|
if (smsDeliveryStarted && isSmsDeliveryOutcomeUnknown(error)) {
|
|
markSmsSent(
|
|
requestedPhone,
|
|
"发送结果未知,如收到短信可直接填写;60 秒后可重试",
|
|
);
|
|
} else if (!isRequestCancelled(error)) {
|
|
showFeedback(getErrorMessage(error, "安全验证暂不可用"));
|
|
}
|
|
}
|
|
return false;
|
|
} finally {
|
|
if (canCommit()) sendingCode.value = false;
|
|
}
|
|
};
|
|
|
|
const completeTac = async (result) => {
|
|
const expectedContext = tacContext.value;
|
|
if (!expectedContext) return false;
|
|
try {
|
|
const ticket = normalizeTacSuccess(result, expectedContext.requestId);
|
|
if (phone.value !== expectedContext.subject) {
|
|
throw new Error("手机号已变化,请重新验证");
|
|
}
|
|
closeTac();
|
|
sendingCode.value = true;
|
|
await authApi.sendSmsCode(
|
|
{
|
|
operationCode,
|
|
phone: expectedContext.subject,
|
|
validToken: ticket.validToken,
|
|
},
|
|
{ requestController: smsDeliveryRequestController },
|
|
);
|
|
markSmsSent(expectedContext.subject);
|
|
return true;
|
|
} catch (error) {
|
|
closeTac();
|
|
if (canCommit()) {
|
|
if (isSmsDeliveryOutcomeUnknown(error)) {
|
|
markSmsSent(
|
|
expectedContext.subject,
|
|
"发送结果未知,如收到短信可直接填写;60 秒后可重试",
|
|
);
|
|
} else if (!isRequestCancelled(error)) {
|
|
showFeedback(getErrorMessage(error, "验证码发送失败"));
|
|
}
|
|
}
|
|
return false;
|
|
} finally {
|
|
if (canCommit()) sendingCode.value = false;
|
|
}
|
|
};
|
|
|
|
const handleTacFailure = ({ message } = {}) =>
|
|
showFeedback(message || "行为验证未通过,请重试");
|
|
|
|
const handleTacError = ({ message } = {}) => {
|
|
closeTac();
|
|
showFeedback(message || "安全验证暂不可用");
|
|
};
|
|
|
|
const cancel = () => {
|
|
captchaRequirementRequestController.abort();
|
|
smsDeliveryRequestController.abort();
|
|
sendingCode.value = false;
|
|
};
|
|
|
|
const dispose = () => {
|
|
disposed = true;
|
|
cancel();
|
|
closeTac();
|
|
smsCooldown.dispose();
|
|
};
|
|
|
|
return {
|
|
tacVisible,
|
|
tacContext,
|
|
sendingCode,
|
|
cooldownSeconds,
|
|
sentPhone,
|
|
requestCode,
|
|
completeTac,
|
|
closeTac,
|
|
handleTacFailure,
|
|
handleTacError,
|
|
cancel,
|
|
dispose,
|
|
syncCooldown: smsCooldown.sync,
|
|
};
|
|
};
|