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

515 lines
16 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";
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 submitting = ref(false);
const error = ref("");
const resultMessage = ref("");
const dnsCatalogLoading = ref(true);
const dnsCatalogError = ref("");
const dnsTesting = ref(false);
const dnsTestError = ref("");
const dnsProviders = ref([]);
const dnsCredentials = ref({});
const dnsTestDomain = ref(props.status.web_hostname || "");
const dnsVerification = ref(null);
const verificationClock = ref(Date.now());
const currentStep = ref(1);
const selectedEmailDomain = ref("");
const emailLocalPart = ref("");
let verificationTimer;
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.server_port || 8080,
email_http_enabled: false,
email_provider: "",
email_endpoint: "",
email_http_path: "/api/email/send",
email_http_login_path: "/api/login",
email_http_auth_mode: "none",
email_http_credential_ref: "",
email_http_headers: "",
email_event_mode: "detailed",
email_include_deployment_details: true,
email_include_certificate_serial: true,
email_include_management_url: true,
email_sender_name: "Scert",
email_sender_email: "",
email_timeout_secs: 10,
email_retry_count: 3,
email_retry_intervals_secs: "30,120,600",
recipients: "",
dns_provider: props.status.dns_provider || "cloudflare",
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",
});
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?.verification_token ||
!dnsVerification.value?.credential_ref
)
return false;
return new Date(dnsVerification.value.expires_at).getTime() > Date.now();
});
function invalidateDNSVerification() {
dnsVerification.value = null;
dnsTestError.value = "";
}
function formatVerificationExpiry(value) {
if (!value) return "";
return new Intl.DateTimeFormat("zh-CN", {
dateStyle: "medium",
timeStyle: "medium",
timeZone: "Asia/Shanghai",
}).format(new Date(value));
}
function handleEmailInput(value) {
error.value = "";
const rawValue = String(value || "").trim();
if (!rawValue) {
emailLocalPart.value = "";
selectedEmailDomain.value = "";
form.value.admin_email = "";
return;
}
const atIndex = rawValue.indexOf("@");
if (atIndex <= 0) {
emailLocalPart.value = rawValue;
form.value.admin_email = selectedEmailDomain.value && selectedEmailDomain.value !== "custom"
? `${rawValue}@${selectedEmailDomain.value}`
: rawValue;
return;
}
const localPart = rawValue.slice(0, atIndex).trim();
const domain = rawValue.slice(atIndex + 1).trim().toLowerCase();
emailLocalPart.value = localPart;
selectedEmailDomain.value = commonEmailDomains.some(
(option) => option.value === domain,
)
? domain
: "custom";
form.value.admin_email = `${localPart}@${domain}`;
}
function applyEmailDomain(domain) {
error.value = "";
if (!domain || domain === "custom") return;
const currentEmail = form.value.admin_email.trim();
const localPart = emailLocalPart.value.trim() || currentEmail.split("@")[0].trim();
if (!localPart) {
selectedEmailDomain.value = "";
error.value = "请先输入邮箱名称,再选择常用邮箱。";
return;
}
emailLocalPart.value = localPart;
form.value.admin_email = `${localPart}@${domain}`;
}
async function loadDNSProviders() {
dnsCatalogLoading.value = true;
dnsCatalogError.value = "";
try {
const response = await fetch("/api/v1/setup/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;
}
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;
}
}
dnsTesting.value = true;
try {
const response = await fetch("/api/v1/setup/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 (!response.ok) throw new Error(result.error || "DNS API 在线验证失败");
dnsVerification.value = result;
} catch (err) {
dnsTestError.value = err.message || "DNS API 在线验证失败";
} finally {
dnsTesting.value = false;
}
}
function validateCurrentStep() {
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) {
error.value = "超级管理员密码至少需要 8 个字符。";
return false;
}
if (form.value.admin_password !== form.value.confirm_password) {
error.value = "两次输入的超级管理员密码不一致。";
return false;
}
}
if (currentStep.value === 2) {
const port = Number(form.value.server_port);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
error.value = "请输入 1 至 65535 之间的主服务端口。";
return false;
}
if (form.value.email_http_enabled && !form.value.email_endpoint.trim()) {
error.value = "启用 HTTP 邮件通知后必须填写 API 地址。";
return false;
}
if (
form.value.email_http_enabled &&
form.value.email_http_auth_mode !== "none" &&
!form.value.email_http_credential_ref.trim()
) {
error.value = "启用 HTTP 通知认证后必须填写凭据引用。";
return false;
}
if (form.value.email_http_headers.trim()) {
try {
const headers = JSON.parse(form.value.email_http_headers);
if (!headers || Array.isArray(headers) || typeof headers !== "object") {
throw new Error("not-object");
}
} catch {
error.value = "HTTP 自定义请求头必须是 JSON 对象。";
return false;
}
}
}
if (currentStep.value === 3) {
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) {
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 (!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 (currentStep.value < 4) {
nextStep();
return;
}
submit();
}
function recipients() {
return form.value.recipients
.split(/[\n,,]/)
.map((item) => item.trim())
.filter(Boolean);
}
function emailHeaders() {
if (!form.value.email_http_headers.trim()) return {};
return JSON.parse(form.value.email_http_headers);
}
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() {
error.value = "";
resultMessage.value = "";
if (form.value.admin_password.length < 8) {
error.value = "超级管理员密码至少需要 8 个字符。";
return;
}
if (form.value.admin_password !== form.value.confirm_password) {
error.value = "两次输入的超级管理员密码不一致。";
return;
}
if (!dnsVerificationValid.value) {
error.value = "必须先完成 DNS API 在线验证;验证结果已过期时请重新测试。";
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;
}
submitting.value = true;
try {
const response = await fetch("/api/v1/setup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
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: form.value.email_http_enabled,
email_provider: form.value.email_provider,
email_endpoint: form.value.email_endpoint,
email_http_path: form.value.email_http_path,
email_http_login_path: form.value.email_http_login_path,
email_http_auth_mode: form.value.email_http_auth_mode,
email_http_credential_ref: form.value.email_http_credential_ref,
email_http_headers: emailHeaders(),
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_sender_name: form.value.email_sender_name,
email_sender_email: form.value.email_sender_email,
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(),
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,
}),
});
const result = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(result.error || "保存初始化配置失败");
resultMessage.value = result.message || "初始化配置已保存。";
emit("initialized", result);
} catch (err) {
error.value = err.message || "保存初始化配置失败";
} finally {
submitting.value = false;
}
}
watch(
() => form.value.dns_provider,
() => {
dnsCredentials.value = {};
invalidateDNSVerification();
},
);
watch(dnsCredentials, invalidateDNSVerification, { deep: true });
watch(dnsTestDomain, invalidateDNSVerification);
onMounted(() => {
loadDNSProviders();
verificationTimer = window.setInterval(() => {
verificationClock.value = Date.now();
}, 30000);
});
onUnmounted(() => {
if (verificationTimer) window.clearInterval(verificationTimer);
});
return {
scertLogo,
submitting,
error,
resultMessage,
dnsCatalogLoading,
dnsCatalogError,
dnsTesting,
dnsTestError,
dnsProviders,
dnsCredentials,
dnsTestDomain,
dnsVerification,
currentStep,
targetType,
form,
commonEmailDomains,
selectedEmailDomain,
emailLocalPart,
targetLabel,
resetURLPreview,
selectedDNSProvider,
dnsVerificationValid,
formatVerificationExpiry,
applyEmailDomain,
handleEmailInput,
testDNS,
handleSubmit,
previousStep,
};
}