Scert/frontend/src/views/SetupWizard/SetupWizard.vue

422 lines
14 KiB
Vue
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.

<script setup>
import { useSetupWizard } from "./SetupWizard.js";
import AccessShell from "../components/AccessShell/AccessShell.vue";
const props = defineProps({
status: {
type: Object,
required: true,
},
});
const emit = defineEmits(["initialized"]);
const {
scertLogo,
submitting,
error,
resultMessage,
dnsCatalogLoading,
dnsCatalogError,
dnsTesting,
dnsTestError,
dnsProviders,
dnsCredentials,
dnsTestDomain,
dnsVerification,
currentStep,
targetType,
form,
targetLabel,
resetURLPreview,
selectedDNSProvider,
dnsVerificationValid,
formatVerificationExpiry,
testDNS,
handleSubmit,
previousStep,
} = useSetupWizard(props, emit);
</script>
<template>
<AccessShell variant="setup" aria-label="Scert 初始化向导">
<template #brand>
<img class="access-logo" :src="scertLogo" alt="Scert 标志" />
<p class="access-eyebrow">SCERT 首次运行</p>
<h1 class="access-brand-title">配置证书管理服务</h1>
<p class="access-brand-description">
Scert 与 Scert-admin 共享本次配置。完成初始化后,即可通过管理页面统一管理证书。
</p>
<div class="warning-card">
<strong>初始化提示</strong>
<span
>请先完成 DNS API 在线验证。证书签发和 HTTPS
切换将在后续任务中执行,本页不会伪报申请成功。</span
>
</div>
</template>
<section class="setup-card">
<header>
<div>
<p class="setup-eyebrow">初始化向导</p>
<h2>基础配置</h2>
</div>
<span class="local-badge">仅限本机</span>
</header>
<nav class="setup-progress" aria-label="初始化步骤">
<span
class="progress-step"
:class="{ active: currentStep === 1, complete: currentStep > 1 }"
><b>1</b><strong>管理员</strong><small>账号安全</small></span
>
<i aria-hidden="true"></i>
<span
class="progress-step"
:class="{ active: currentStep === 2, complete: currentStep > 2 }"
><b>2</b><strong>服务通知</strong><small>端口与邮件</small></span
>
<i aria-hidden="true"></i>
<span
class="progress-step"
:class="{ active: currentStep === 3, complete: currentStep > 3 }"
><b>3</b><strong>DNS 验证</strong><small>权限测试</small></span
>
<i aria-hidden="true"></i>
<span class="progress-step" :class="{ active: currentStep === 4 }"
><b>4</b><strong>管理页面证书</strong><small>访问方式</small></span
>
</nav>
<form
id="setup-form"
name="setup_form"
class="setup-form"
@submit.prevent="handleSubmit"
>
<div class="setup-step-grid">
<fieldset v-if="currentStep === 1" class="setup-step-card">
<legend><span>01</span>超级管理员</legend>
<div class="setup-grid two-columns">
<label
><span>用户名</span
><input
id="admin-username"
name="admin_username"
v-model.trim="form.admin_username"
required
autocomplete="username"
placeholder="例如 scert-admin"
/></label>
<label
><span>绑定邮箱</span
><input
id="admin-email"
name="admin_email"
v-model.trim="form.admin_email"
required
type="email"
autocomplete="email"
placeholder="用于登录和安全通知"
/></label>
<label
><span>密码</span
><input
id="admin-password"
name="admin_password"
v-model="form.admin_password"
required
minlength="8"
type="password"
autocomplete="new-password"
placeholder="至少 8 个字符"
/></label>
<label
><span>确认密码</span
><input
id="confirm-password"
name="confirm_password"
v-model="form.confirm_password"
required
minlength="8"
type="password"
autocomplete="new-password"
placeholder="再次输入密码"
/></label>
</div>
</fieldset>
<fieldset v-if="currentStep === 2" class="setup-step-card">
<legend><span>02</span>服务与通知</legend>
<div class="setup-grid two-columns">
<label
><span>Scert 主服务端口</span
><input
id="server-port"
name="server_port"
v-model.number="form.server_port"
required
min="1"
max="65535"
type="number"
/></label>
<label
><span>第三方邮箱服务标识</span
><input
id="email-provider"
name="email_provider"
v-model.trim="form.email_provider"
placeholder="例如 company-mail-api"
/></label>
<label
><span>邮件 API 地址</span
><input
id="email-endpoint"
name="email_endpoint"
v-model.trim="form.email_endpoint"
type="url"
placeholder="https://mail.example.com/send"
/></label>
</div>
<div class="provider-description reset-preview">
<strong>密码重置地址</strong><span>{{ resetURLPreview }}</span
><span
>无需配置独立端口;只有手动启动 Scert-admin
后,此地址才会临时可用。</span
>
</div>
<label
><span>通知收件人</span
><input
id="recipients"
name="recipients"
v-model="form.recipients"
placeholder="多个地址使用逗号分隔"
/></label>
</fieldset>
<fieldset v-if="currentStep === 3" class="setup-step-card">
<legend><span>03</span>DNS 验证</legend>
<div class="setup-grid two-columns">
<label
><span>DNS 服务商</span
><select
id="dns-provider"
name="dns_provider"
v-model="form.dns_provider"
required
:disabled="dnsCatalogLoading"
>
<option v-if="dnsCatalogLoading" value="">
正在读取服务商……
</option>
<option
v-for="provider in dnsProviders"
:key="provider.id"
:value="provider.id"
>
{{ provider.name
}}{{ provider.available ? "" : "(适配器待开放)" }}
</option>
</select></label
>
<label
><span>DNS 权限测试域名</span
><input
id="dns-test-domain"
name="dns_test_domain"
v-model.trim="dnsTestDomain"
required
placeholder="例如 example.com"
/><small
>使用域名时可填写管理页面域名;使用公网 IP
时必须另填一个由当前 DNS 账号管理的域名。</small
></label
>
</div>
<p v-if="dnsCatalogError" class="inline-error">
{{ dnsCatalogError }}
</p>
<div
v-if="selectedDNSProvider"
class="provider-description"
:class="{ unavailable: !selectedDNSProvider.available }"
>
<strong>{{
selectedDNSProvider.available
? "在线验证可用"
: "当前仅提供字段模型"
}}</strong>
<span>{{ selectedDNSProvider.description }}</span>
</div>
<div
v-if="selectedDNSProvider"
class="setup-grid two-columns credential-grid"
>
<label
v-for="field in selectedDNSProvider.credential_fields"
:key="field.name"
>
<span
>{{ field.label
}}{{ field.required ? "(必填)" : "(可选)" }}</span
>
<textarea
v-if="field.name.includes('json')"
:id="`dns-credential-${field.name}`"
:name="`dns_credential_${field.name}`"
v-model="dnsCredentials[field.name]"
:required="field.required"
:placeholder="field.placeholder"
rows="4"
autocomplete="off"
></textarea>
<input
v-else
:id="`dns-credential-${field.name}`"
:name="`dns_credential_${field.name}`"
v-model="dnsCredentials[field.name]"
:required="field.required"
:type="field.secret ? 'password' : 'text'"
:placeholder="field.placeholder"
autocomplete="off"
/>
</label>
</div>
<div class="dns-test-actions">
<button
class="dns-test-button"
type="button"
:disabled="
dnsTesting ||
dnsCatalogLoading ||
!selectedDNSProvider?.available
"
@click="testDNS"
>
{{ dnsTesting ? "正在创建并清理测试记录……" : "测试 DNS API" }}
</button>
<span
>测试会临时创建 DNS-01 TXT
记录并自动清理,明文凭据不会写入普通配置文件。</span
>
</div>
<p v-if="dnsTestError" class="inline-error">{{ dnsTestError }}</p>
<div v-if="dnsVerificationValid" class="dns-verification-success">
<strong>DNS API 在线验证通过</strong>
<span
>凭据已加密保存,本次验证结果有效至
{{
formatVerificationExpiry(dnsVerification.expires_at)
}}。修改服务商、测试域名或凭据后需要重新测试。</span
>
</div>
</fieldset>
<fieldset v-if="currentStep === 4" class="setup-step-card">
<legend><span>04</span>管理页面与证书目录</legend>
<div class="target-switch">
<button
type="button"
:class="{ active: targetType === 'hostname' }"
@click="targetType = 'hostname'"
>
使用域名</button
><button
type="button"
:class="{ active: targetType === 'ip' }"
@click="targetType = 'ip'"
>
使用公网 IP
</button>
</div>
<div class="setup-grid two-columns">
<label v-if="targetType === 'hostname'"
><span>管理页面域名</span
><input
id="web-hostname"
name="web_hostname"
v-model.trim="form.web_hostname"
required
placeholder="scert.example.com"
/></label>
<label v-else
><span>公网 IP 地址</span
><input
id="web-public-ip"
name="web_public_ip"
v-model.trim="form.web_public_ip"
required
placeholder="203.0.113.10"
/></label>
<label
><span>证书部署根目录</span
><input
id="certificate-root"
name="certificate_root"
v-model.trim="form.certificate_root"
required
/></label>
<label
><span>HTTPS 模式</span
><select
id="https-mode"
name="https_mode"
v-model="form.https_mode"
>
<option value="direct">默认:核心服务直接终止 HTTPS</option>
<option value="proxy">
高级:Nginx/Caddy 终止公网 HTTPS
</option>
</select></label
>
</div>
<label class="confirm-row"
><input
id="web-enabled"
name="web_enabled"
v-model="form.web_enabled"
type="checkbox"
/><span
><strong>初始化完成后开启管理页面</strong
><small
>桌面端可通过此开关决定是否启用浏览器管理页面。</small
></span
></label
>
</fieldset>
</div>
<p v-if="error" class="setup-error">{{ error }}</p>
<p v-if="resultMessage" class="setup-success">{{ resultMessage }}</p>
<footer>
<span
>初始化程序将自动配置本机防火墙;云安全组只需放行主服务端口。配置完成前仅保存在内存中。</span
>
<div class="wizard-actions">
<button
v-if="currentStep > 1"
class="back-button"
type="button"
@click="previousStep"
>
上一步</button
><button type="submit" :disabled="submitting || dnsTesting">
{{
currentStep < 4
? "下一步"
: submitting
? "正在保存……"
: "完成初始化"
}}
</button>
</div>
</footer>
</form>
</section>
</AccessShell>
</template>
<style scoped src="./SetupWizard.css"></style>