Files
jiapuapp/composables/auth/use-sms-verification.js
T

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,
};
};