Scert/frontend/src/views/SetupWizard/SetupWizard.js

826 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { useDatabaseSetup } from "./DatabaseSetup.js";
import { bootstrapPayload } from "./BootstrapPayload.js";
const defaultDNSVerificationTimeoutSeconds = 60;
const aliyunDNSVerificationTimeoutSeconds = 6;
const databaseRequiredMessage = "请先验证所选数据库的连接和写权限。";
const databaseExpiredMessage = "数据库验证结果无效或已过期,请重新验证。";
function dnsVerificationTimeoutFor(providerID) {
return String(providerID || "").trim().toLowerCase() === "aliyun"
? aliyunDNSVerificationTimeoutSeconds
: defaultDNSVerificationTimeoutSeconds;
}
const commonEmailDomains = [
{ label: "常用邮箱", value: "" },
{ label: "@163.com", value: "163.com" },
{ label: "@qq.com", value: "qq.com" },
{ label: "@126.com", value: "126.com" },
{ label: "@outlook.com", value: "outlook.com" },
{ label: "@gmail.com", value: "gmail.com" },
{ label: "@foxmail.com", value: "foxmail.com" },
{ label: "@aliyun.com", value: "aliyun.com" },
{ label: "@139.com", value: "139.com" },
{ label: "@189.cn", value: "189.cn" },
{ label: "自定义后缀", value: "custom" },
];
export function useSetupWizard(props, emit) {
const scertLogo = "/Scert.png";
const isBootstrap = props.status.mode === "bootstrap";
const apiBase = isBootstrap ? "/api/v1/bootstrap" : "/api/v1/setup";
const submitting = ref(false);
const error = ref("");
const resultMessage = ref("");
const dnsCatalogLoading = ref(true);
const dnsCatalogError = ref("");
const dnsTesting = ref(false);
const dnsTestError = ref("");
const dnsVerificationElapsedSeconds = ref(0);
const dnsVerificationTimeoutSeconds = ref(defaultDNSVerificationTimeoutSeconds);
const dnsProviders = ref([]);
const dnsCredentials = ref({});
const dnsCredentialsVisible = ref(true);
const dnsTestDomain = ref(props.status.web_hostname || "");
const dnsVerification = ref(null);
const verificationClock = ref(Date.now());
const database = useDatabaseSetup(verificationClock);
watch(database.databaseVerified, (verified) => {
if (verified && [databaseRequiredMessage, databaseExpiredMessage].includes(error.value)) {
error.value = "";
}
}, { flush: "sync" });
const completed = ref(false);
const currentStep = ref(1);
const notificationAdvancedVisible = ref(false);
const selectedEmailDomain = ref("");
const emailLocalPart = ref("");
const emailInputElement = ref(null);
const emailAutofillElement = ref(null);
let emailVisibleEdited = false;
let verificationTimer;
let dnsVerificationTimer;
let dnsRequestVersion = 0;
let emailAutofillTimer;
const targetType = ref(props.status.web_public_ip ? "ip" : "hostname");
const form = ref({
admin_username: "",
admin_email: "",
admin_password: "",
confirm_password: "",
server_port: props.status.mode === "bootstrap" ? 80 : (props.status.server_port || 80),
email_smtp_enabled: false,
email_smtp_host: "",
email_smtp_port: 587,
email_smtp_username: "",
email_smtp_password: "",
email_smtp_mode: "starttls",
email_smtp_allow_plaintext_auth: false,
email_event_mode: "detailed",
email_include_deployment_details: true,
email_include_certificate_serial: true,
email_include_management_url: true,
email_timeout_secs: 10,
email_retry_count: 3,
email_retry_intervals_secs: "30,120,600",
recipients: "",
dns_provider: props.status.dns_provider || "aliyun",
propagation_timeout_secs: props.status.propagation_timeout_secs || 300,
propagation_polling_interval_secs:
props.status.propagation_polling_interval_secs || 5,
certificate_root: props.status.certificate_root || "",
web_hostname: props.status.web_hostname || "",
web_public_ip: props.status.web_public_ip || "",
web_enabled: props.status.web_enabled !== false,
https_mode: props.status.https_mode || "direct",
proxy_https_port: props.status.proxy_https_port || 443,
acme_environment: props.status.acme_environment || "staging",
});
const targetLabel = computed(() =>
targetType.value === "hostname" ? "管理页面域名" : "公网 IP 地址",
);
const resetURLPreview = computed(() => {
const rawHost =
targetType.value === "hostname"
? form.value.web_hostname
: form.value.web_public_ip;
const host = String(rawHost || "")
.trim()
.replace(/^\[|\]$/g, "");
if (!host) return "完成域名或公网 IP 配置后自动生成";
const displayHost = host.includes(":") ? `[${host}]` : host;
return `https://${displayHost}/reset`;
});
const selectedDNSProvider = computed(
() =>
dnsProviders.value.find(
(provider) => provider.id === form.value.dns_provider,
) || null,
);
const dnsVerificationValid = computed(() => {
verificationClock.value;
if (
dnsVerification.value?.verified !== true || !dnsVerification.value?.verification_token ||
(!isBootstrap && !dnsVerification.value?.credential_ref)
)
return false;
return new Date(dnsVerification.value.expires_at).getTime() > Date.now();
});
function invalidateDNSVerification() {
dnsRequestVersion += 1;
stopDNSVerificationTimer();
dnsTesting.value = false;
dnsVerification.value = null;
dnsTestError.value = "";
}
function updateDNSVerificationTimeoutSeconds(value) {
const timeout = Number(value);
if (Number.isFinite(timeout) && timeout > 0) {
dnsVerificationTimeoutSeconds.value = Math.floor(timeout);
}
}
function stopDNSVerificationTimer(requestVersion) {
if (requestVersion !== undefined && requestVersion !== dnsRequestVersion) return;
if (dnsVerificationTimer) {
window.clearInterval(dnsVerificationTimer);
dnsVerificationTimer = undefined;
}
dnsVerificationElapsedSeconds.value = 0;
}
function startDNSVerificationTimer(requestVersion, providerID) {
stopDNSVerificationTimer();
dnsVerificationTimeoutSeconds.value = dnsVerificationTimeoutFor(providerID);
const startedAt = Date.now();
const updateElapsedSeconds = () => {
if (requestVersion !== dnsRequestVersion) return;
dnsVerificationElapsedSeconds.value = Math.max(
0,
Math.floor((Date.now() - startedAt) / 1000),
);
};
updateElapsedSeconds();
dnsVerificationTimer = window.setInterval(updateElapsedSeconds, 1000);
}
function formatVerificationExpiry(value) {
if (!value) return "";
return new Intl.DateTimeFormat("zh-CN", {
dateStyle: "medium",
timeStyle: "medium",
timeZone: "Asia/Shanghai",
}).format(new Date(value));
}
function formatVerificationStageStatus(status) {
switch (status) {
case "completed":
return "已完成";
case "warning":
return "警告";
case "failed":
return "失败";
default:
return "处理中";
}
}
function findCommonEmailDomain(domain) {
const normalizedDomain = String(domain || "").trim().toLowerCase();
return commonEmailDomains.find(
(option) =>
option.value &&
option.value !== "custom" &&
option.value === normalizedDomain,
);
}
function getEmailLocalPart(value) {
const text = String(value || "").trim();
const atIndex = text.indexOf("@");
return (atIndex >= 0 ? text.slice(0, atIndex) : text).trim();
}
function isCompleteEmail(localPart, domain) {
return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(
`${localPart}@${domain}`,
);
}
function updateEmailAutofillValue(value) {
if (!emailAutofillElement.value) return;
emailAutofillElement.value.value = String(value || "");
}
function handleEmailInput(value, clearError = true) {
emailVisibleEdited = true;
if (clearError) error.value = "";
const rawValue = String(value || "").trim();
if (!rawValue) {
emailLocalPart.value = "";
selectedEmailDomain.value = "";
form.value.admin_email = "";
updateEmailAutofillValue("");
return;
}
const atIndex = rawValue.indexOf("@");
if (atIndex < 0) {
emailLocalPart.value = rawValue;
form.value.admin_email = selectedEmailDomain.value && selectedEmailDomain.value !== "custom"
? `${rawValue}@${selectedEmailDomain.value}`
: rawValue;
updateEmailAutofillValue(form.value.admin_email);
return;
}
const localPart = rawValue.slice(0, atIndex).trim();
const domain = rawValue.slice(atIndex + 1).trim().toLowerCase();
if (!localPart || !domain) {
emailLocalPart.value = rawValue;
selectedEmailDomain.value = domain ? "custom" : "";
form.value.admin_email = rawValue;
updateEmailAutofillValue(form.value.admin_email);
return;
}
const commonDomain = findCommonEmailDomain(domain);
if (commonDomain) {
emailLocalPart.value = localPart;
selectedEmailDomain.value = commonDomain.value;
form.value.admin_email = `${localPart}@${commonDomain.value}`;
updateEmailAutofillValue(form.value.admin_email);
return;
}
emailLocalPart.value = rawValue;
selectedEmailDomain.value = "";
form.value.admin_email = rawValue;
updateEmailAutofillValue(form.value.admin_email);
}
function synchronizeEmailField(value) {
const visibleValue = emailInputElement.value?.value?.trim() || "";
const autofillValue = emailAutofillElement.value?.value?.trim() || "";
const inputValue =
value !== undefined
? value
: visibleValue.includes("@")
? visibleValue
: emailVisibleEdited
? visibleValue
: autofillValue.includes("@")
? autofillValue
: visibleValue;
const rawValue = String(inputValue || "").trim();
if (!rawValue) return;
if (rawValue.includes("@") || rawValue !== emailLocalPart.value) {
// 自动同步不应清除数据库、密码等其他字段的校验结果。
handleEmailInput(rawValue, false);
return;
}
if (
selectedEmailDomain.value &&
selectedEmailDomain.value !== "custom"
) {
form.value.admin_email = `${rawValue}@${selectedEmailDomain.value}`;
updateEmailAutofillValue(form.value.admin_email);
}
}
function handleEmailAutofill() {
synchronizeEmailField();
}
function handleEmailPaste(event) {
const pastedValue = event.clipboardData?.getData("text") || "";
if (!pastedValue.includes("@")) return;
event.preventDefault();
handleEmailInput(pastedValue);
handleEmailBlur();
}
function handleEmailBlur(value) {
synchronizeEmailField(value);
const rawValue = String(emailLocalPart.value || "").trim();
const atIndex = rawValue.indexOf("@");
if (atIndex < 0) {
handleEmailInput(rawValue);
return;
}
const localPart = rawValue.slice(0, atIndex).trim();
const domain = rawValue.slice(atIndex + 1).trim().toLowerCase();
if (localPart && isCompleteEmail(localPart, domain)) {
emailLocalPart.value = localPart;
selectedEmailDomain.value = findCommonEmailDomain(domain)?.value || "custom";
form.value.admin_email = `${localPart}@${domain}`;
updateEmailAutofillValue(form.value.admin_email);
}
}
function applyEmailDomain(domain) {
error.value = "";
if (!domain || domain === "custom") return;
const currentEmail = form.value.admin_email.trim();
const localPart =
getEmailLocalPart(emailLocalPart.value) || getEmailLocalPart(currentEmail);
if (!localPart) {
selectedEmailDomain.value = "";
error.value = "请先输入邮箱名称,再选择常用邮箱。";
return;
}
emailLocalPart.value = localPart;
form.value.admin_email = `${localPart}@${domain}`;
updateEmailAutofillValue(form.value.admin_email);
}
function toggleNotificationAdvanced() {
notificationAdvancedVisible.value = !notificationAdvancedVisible.value;
}
async function loadDNSProviders() {
dnsCatalogLoading.value = true;
dnsCatalogError.value = "";
try {
const response = await fetch(`${apiBase}/dns/providers`);
const result = await response.json().catch(() => ({}));
if (!response.ok)
throw new Error(result.error || "读取 DNS 服务商列表失败");
dnsProviders.value = Array.isArray(result.items) ? result.items : [];
if (
!dnsProviders.value.some(
(provider) => provider.id === form.value.dns_provider,
)
) {
form.value.dns_provider =
dnsProviders.value.find((provider) => provider.available)?.id || "";
}
} catch (err) {
dnsCatalogError.value = err.message || "读取 DNS 服务商列表失败";
} finally {
dnsCatalogLoading.value = false;
}
}
function credentialPayload() {
const payload = {};
for (const field of selectedDNSProvider.value?.credential_fields || []) {
const value = String(dnsCredentials.value[field.name] || "").trim();
if (value) payload[field.name] = value;
}
return payload;
}
function maskCredential(value) {
const text = String(value || "");
if (!text) return "";
return "•".repeat(Math.min(Math.max(text.length, 8), 64));
}
async function testDNS() {
dnsTestError.value = "";
resultMessage.value = "";
invalidateDNSVerification();
const provider = selectedDNSProvider.value;
if (!provider) {
dnsTestError.value = "请选择 DNS 服务商。";
return;
}
if (!provider.available) {
dnsTestError.value = "该 DNS 服务商在线验证适配器尚未开放。";
return;
}
const testDomain = dnsTestDomain.value.trim();
if (!testDomain) {
dnsTestError.value = "请填写 DNS 权限测试域名。";
return;
}
for (const field of provider.credential_fields || []) {
if (
field.required &&
!String(dnsCredentials.value[field.name] || "").trim()
) {
dnsTestError.value = `请填写${field.label}。`;
return;
}
}
const requestVersion = dnsRequestVersion;
dnsTesting.value = true;
startDNSVerificationTimer(requestVersion, provider.id);
try {
const response = await fetch(`${apiBase}/dns/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
provider: provider.id,
test_domain: testDomain,
credentials: credentialPayload(),
}),
});
const result = await response.json().catch(() => ({}));
if (requestVersion !== dnsRequestVersion) return;
updateDNSVerificationTimeoutSeconds(result.verification_timeout_secs);
if (result.test_report) {
dnsVerification.value = { test_report: result.test_report };
}
if (!response.ok) throw new Error(result.error || "DNS API 在线验证失败");
dnsVerification.value = result;
} catch (err) {
if (requestVersion !== dnsRequestVersion) return;
dnsTestError.value = err.message || "DNS API 在线验证失败";
} finally {
if (requestVersion === dnsRequestVersion) {
stopDNSVerificationTimer(requestVersion);
dnsTesting.value = false;
}
}
}
function validateCurrentStep() {
verificationClock.value = Date.now();
if (currentStep.value === 1) synchronizeEmailField();
error.value = "";
resultMessage.value = "";
if (currentStep.value === 1) {
if (!form.value.admin_username.trim()) {
error.value = "请输入超级管理员用户名。";
return false;
}
if (!form.value.admin_email.trim()) {
error.value = "请输入超级管理员绑定邮箱。";
return false;
}
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.value.admin_email.trim())) {
error.value = "请输入合法的超级管理员绑定邮箱。";
return false;
}
if (
form.value.admin_password.length < 8 ||
form.value.admin_password.length > 12
) {
error.value = "超级管理员密码长度必须为 8 至 12 个字符。";
return false;
}
if (form.value.admin_password !== form.value.confirm_password) {
error.value = "两次输入的超级管理员密码不一致。";
return false;
}
const port = Number(form.value.server_port);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
error.value = "请输入 1 至 65535 之间的主服务端口。";
return false;
}
const hostname =
targetType.value === "hostname" ? form.value.web_hostname.trim() : "";
const publicIP =
targetType.value === "ip" ? form.value.web_public_ip.trim() : "";
if (!hostname && !publicIP) {
error.value = `请填写${targetLabel.value}。`;
return false;
}
if (isBootstrap && !database.databaseVerified.value) {
error.value = databaseRequiredMessage;
return false;
}
}
if (currentStep.value === 2) {
if (form.value.email_smtp_enabled) {
if (!form.value.email_smtp_host.trim()) {
error.value = "启用 SMTP 通知后必须填写 SMTP 主机。";
return false;
}
const smtpPort = Number(form.value.email_smtp_port);
if (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535) {
error.value = "SMTP 端口必须在 1 至 65535 之间。";
return false;
}
if (!form.value.email_smtp_username.trim()) {
error.value = "启用 SMTP 通知后必须填写 SMTP 用户名。";
return false;
}
if (!form.value.email_smtp_password) {
error.value = "启用 SMTP 通知后必须填写 SMTP 密码。";
return false;
}
if (!["plain", "starttls", "implicit_tls"].includes(form.value.email_smtp_mode)) {
error.value = "SMTP 连接模式无效。";
return false;
}
if (form.value.email_smtp_mode === "plain" && !form.value.email_smtp_allow_plaintext_auth) {
error.value = "SMTP 明文模式必须明确允许明文认证,建议改用 STARTTLS 或隐式 TLS。";
return false;
}
}
}
if (currentStep.value === 3) {
const propagationTimeout = Number(form.value.propagation_timeout_secs);
const propagationInterval = Number(
form.value.propagation_polling_interval_secs,
);
if (
!Number.isInteger(propagationTimeout) ||
propagationTimeout < 1 ||
propagationTimeout > 86400
) {
error.value = "DNS 传播最大等待时间必须在 1 至 86400 秒之间。";
return false;
}
if (
!Number.isInteger(propagationInterval) ||
propagationInterval < 1 ||
propagationInterval > propagationTimeout
) {
error.value = "DNS 传播检测间隔必须大于 0 且不超过最大等待时间。";
return false;
}
const provider = selectedDNSProvider.value;
if (!provider) {
error.value = "请选择 DNS 服务商。";
return false;
}
if (!provider.available) {
error.value = "该 DNS 服务商在线验证适配器尚未开放。";
return false;
}
if (!dnsTestDomain.value.trim()) {
error.value = "请填写 DNS 权限测试域名。";
return false;
}
for (const field of provider.credential_fields || []) {
if (
field.required &&
!String(dnsCredentials.value[field.name] || "").trim()
) {
error.value = `请填写${field.label}。`;
return false;
}
}
if (!dnsVerificationValid.value) {
error.value = "请先完成 DNS API 在线验证。";
return false;
}
}
if (currentStep.value === 4) {
if (form.value.https_mode === "proxy") {
const port = Number(form.value.proxy_https_port);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
error.value = "代理公开 HTTPS 端口必须为 1 至 65535 的整数。";
return false;
}
if (isBootstrap && (form.value.acme_environment !== "production" || !form.value.web_hostname.trim())) {
error.value = "反向代理管理入口需要管理域名和正式环境证书。测试环境或 IP 自签名证书请使用直接 HTTPS 模式,避免初始化后出现 502。";
return false;
}
}
if (!form.value.certificate_root.trim()) {
error.value = "请填写证书部署根目录。";
return false;
}
}
return true;
}
function nextStep() {
if (!validateCurrentStep()) return;
if (currentStep.value < 4) currentStep.value += 1;
}
function previousStep() {
error.value = "";
resultMessage.value = "";
if (currentStep.value > 1) currentStep.value -= 1;
}
function handleSubmit() {
if (submitting.value || completed.value) return;
if (currentStep.value < 4) {
nextStep();
return;
}
return submit();
}
function recipients() {
return form.value.recipients
.split(/[\n,,]/)
.map((item) => item.trim())
.filter(Boolean);
}
function retryIntervals() {
return form.value.email_retry_intervals_secs
.split(/[,,]/)
.map((item) => Number(item.trim()))
.filter((item) => Number.isInteger(item) && item > 0);
}
async function submit() {
verificationClock.value = Date.now();
error.value = "";
resultMessage.value = "";
if (isBootstrap && !database.databaseVerified.value) {
error.value = databaseExpiredMessage;
return;
}
if (!dnsVerificationValid.value) {
error.value = "必须先完成 DNS API 在线验证;验证结果已过期时请重新测试。";
return;
}
// 提交前重新检查全部步骤,不能依赖之前点击“下一步”时的校验结果。
for (let step = 1; step <= 4; step += 1) {
currentStep.value = step;
if (!validateCurrentStep()) return;
}
const hostname =
targetType.value === "hostname" ? form.value.web_hostname.trim() : "";
const publicIP =
targetType.value === "ip" ? form.value.web_public_ip.trim() : "";
if (!hostname && !publicIP) {
error.value = `请填写${targetLabel.value}。`;
return;
}
if (isBootstrap && hostname && hostname.toLowerCase().replace(/\.$/, "") !== dnsTestDomain.value.trim().toLowerCase().replace(/\.$/, "")) {
error.value = "DNS 验证域名必须与管理页面域名一致,请重新验证。";
return;
}
submitting.value = true;
try {
const response = await fetch(isBootstrap ? `${apiBase}/complete` : apiBase, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify((isBootstrap ? (payload) => bootstrapPayload(payload, database.databaseVerification.value.verification_token, form.value.acme_environment) : (payload) => payload)({
admin_username: form.value.admin_username,
admin_email: form.value.admin_email,
admin_password: form.value.admin_password,
server_port: Number(form.value.server_port),
email_http_enabled: false,
email_smtp_enabled: form.value.email_smtp_enabled,
email_smtp_host: form.value.email_smtp_host,
email_smtp_port: Number(form.value.email_smtp_port),
email_smtp_username: form.value.email_smtp_username,
email_smtp_password: form.value.email_smtp_password,
email_smtp_mode: form.value.email_smtp_mode,
email_smtp_allow_plaintext_auth: form.value.email_smtp_allow_plaintext_auth,
email_event_mode: form.value.email_event_mode,
email_include_deployment_details:
form.value.email_include_deployment_details,
email_include_certificate_serial:
form.value.email_include_certificate_serial,
email_include_management_url: form.value.email_include_management_url,
email_timeout_secs: Number(form.value.email_timeout_secs),
email_retry_count: Number(form.value.email_retry_count),
email_retry_intervals_secs: retryIntervals(),
recipients: recipients(),
dns_provider: form.value.dns_provider,
dns_credential_ref: dnsVerification.value.credential_ref,
dns_verification_token: dnsVerification.value.verification_token,
dns_test_domain: dnsTestDomain.value.trim(),
propagation_timeout_secs: Number(
form.value.propagation_timeout_secs,
),
propagation_polling_interval_secs: Number(
form.value.propagation_polling_interval_secs,
),
certificate_root: form.value.certificate_root,
web_hostname: hostname,
web_public_ip: publicIP,
web_enabled: form.value.web_enabled,
https_mode: form.value.https_mode,
proxy_https_port: form.value.https_mode === "proxy" ? Number(form.value.proxy_https_port) : 0,
})),
});
const result = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(result.error || "保存初始化配置失败");
if (isBootstrap) {
let destination;
try {
destination = new URL(result?.management_url);
} catch {
throw new Error("初始化响应不完整,请检查服务器状态后再继续。");
}
if (result?.completed !== true || destination.protocol !== "https:" ||
destination.username || destination.password) {
throw new Error("初始化响应不完整,请检查服务器状态后再继续。");
}
}
completed.value = true;
resultMessage.value = isBootstrap ? `初始化配置已保存,正在切换至 ${result.management_url}。请稍候;IP 自签名证书或测试证书需要人工核对。` : result.message || "初始化配置已保存。";
if (isBootstrap && Array.isArray(result.warnings)) {
const warnings = result.warnings.filter((message) => typeof message === "string" && message.trim());
if (warnings.length) resultMessage.value += ` 注意:${warnings.join(";")}`;
}
emit("initialized", result);
} catch (err) {
error.value = err.message || "保存初始化配置失败";
if (isBootstrap) {
// 服务端可能已领取令牌;失败重试必须重新验证,不能复用已消费状态。
database.invalidateDatabase();
invalidateDNSVerification();
}
} finally {
submitting.value = false;
}
}
watch(
() => form.value.dns_provider,
() => {
dnsCredentials.value = {};
invalidateDNSVerification();
},
{ flush: "sync" },
);
watch(dnsCredentials, invalidateDNSVerification, { deep: true, flush: "sync" });
watch(dnsTestDomain, invalidateDNSVerification, { flush: "sync" });
watch(() => [targetType.value, form.value.web_hostname], ([target, hostname]) => {
if (isBootstrap && target === "hostname") dnsTestDomain.value = hostname;
}, { flush: "sync" });
onMounted(() => {
loadDNSProviders();
emailAutofillTimer = window.setInterval(() => {
if (currentStep.value === 1) synchronizeEmailField();
}, 250);
verificationTimer = window.setInterval(() => {
verificationClock.value = Date.now();
}, 30000);
});
onUnmounted(() => {
invalidateDNSVerification();
if (verificationTimer) window.clearInterval(verificationTimer);
if (emailAutofillTimer) window.clearInterval(emailAutofillTimer);
});
return {
isBootstrap,
completed,
...database,
scertLogo,
submitting,
error,
resultMessage,
dnsCatalogLoading,
dnsCatalogError,
dnsTesting,
dnsTestError,
dnsVerificationElapsedSeconds,
dnsVerificationTimeoutSeconds,
dnsProviders,
dnsCredentials,
dnsCredentialsVisible,
maskCredential,
dnsTestDomain,
dnsVerification,
currentStep,
notificationAdvancedVisible,
targetType,
form,
commonEmailDomains,
selectedEmailDomain,
emailLocalPart,
emailInputElement,
emailAutofillElement,
handleEmailAutofill,
targetLabel,
resetURLPreview,
selectedDNSProvider,
dnsVerificationValid,
formatVerificationExpiry,
formatVerificationStageStatus,
applyEmailDomain,
toggleNotificationAdvanced,
handleEmailInput,
synchronizeEmailField,
handleEmailPaste,
handleEmailBlur,
testDNS,
handleSubmit,
previousStep,
};
}