修改前检查点:保留初始化通知精简进度

保存仅保留 SMTP 的在途修改;旧校验测试待更新,尚未完成实机验收。
This commit is contained in:
xuans 2026-09-26 22:04:00 +08:00
parent d8e0bf0409
commit 01232e4c68
5 changed files with 36 additions and 296 deletions

View File

@ -45,8 +45,6 @@ export function useSetupWizard(props, emit) {
const dnsProviders = ref([]);
const dnsCredentials = ref({});
const dnsCredentialsVisible = ref(true);
const emailCredentials = ref("");
const emailCredentialsVisible = ref(true);
const dnsTestDomain = ref(props.status.web_hostname || "");
const dnsVerification = ref(null);
const verificationClock = ref(Date.now());
@ -75,7 +73,6 @@ export function useSetupWizard(props, emit) {
admin_password: "",
confirm_password: "",
server_port: props.status.mode === "bootstrap" ? 80 : (props.status.server_port || 80),
email_http_enabled: false,
email_smtp_enabled: false,
email_smtp_host: "",
email_smtp_port: 587,
@ -83,13 +80,6 @@ export function useSetupWizard(props, emit) {
email_smtp_password: "",
email_smtp_mode: "starttls",
email_smtp_allow_plaintext_auth: 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,
@ -116,30 +106,6 @@ export function useSetupWizard(props, emit) {
const targetLabel = computed(() =>
targetType.value === "hostname" ? "管理页面域名" : "公网 IP 地址",
);
const emailCredentialLabel = computed(() => {
switch (form.value.email_http_auth_mode) {
case "bearer":
return "Bearer Token 凭据(JSON)";
case "hmac_sha256":
return "HMAC-SHA256 凭据(JSON)";
case "session":
return "登录会话凭据(JSON)";
default:
return "HTTP 通知凭据";
}
});
const emailCredentialPlaceholder = computed(() => {
switch (form.value.email_http_auth_mode) {
case "bearer":
return '{"token":"请输入 Bearer Token"}';
case "hmac_sha256":
return '{"secret":"请输入签名密钥"}';
case "session":
return '{"account":"请输入账号","password":"请输入密码"}';
default:
return "无需填写凭据";
}
});
const resetURLPreview = computed(() => {
const rawHost =
targetType.value === "hostname"
@ -428,27 +394,6 @@ export function useSetupWizard(props, emit) {
return "•".repeat(Math.min(Math.max(text.length, 8), 64));
}
function emailCredentialPayload() {
if (
!form.value.email_http_enabled ||
form.value.email_http_auth_mode === "none"
) {
return {};
}
const text = emailCredentials.value.trim();
if (!text) return {};
const parsed = JSON.parse(text);
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
throw new Error("HTTP 通知凭据必须是 JSON 对象。");
}
for (const [key, value] of Object.entries(parsed)) {
if (!key.trim() || typeof value !== "string" || !value.trim()) {
throw new Error("HTTP 通知凭据的字段和值必须是非空文本。");
}
}
return parsed;
}
async function testDNS() {
dnsTestError.value = "";
resultMessage.value = "";
@ -586,42 +531,6 @@ export function useSetupWizard(props, emit) {
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" &&
(isBootstrap || !form.value.email_http_credential_ref.trim()) &&
!emailCredentials.value.trim()
) {
error.value = "启用 HTTP 通知认证后必须填写有效凭据。";
return false;
}
if (
form.value.email_http_enabled &&
form.value.email_http_auth_mode !== "none" &&
emailCredentials.value.trim()
) {
try {
emailCredentialPayload();
} catch (err) {
error.value = err.message || "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) {
@ -717,11 +626,6 @@ export function useSetupWizard(props, emit) {
.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(/[,,]/)
@ -769,7 +673,7 @@ export function useSetupWizard(props, emit) {
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_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),
@ -777,14 +681,6 @@ export function useSetupWizard(props, emit) {
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_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_credentials: emailCredentialPayload(),
email_http_headers: emailHeaders(),
email_event_mode: form.value.email_event_mode,
email_include_deployment_details:
form.value.email_include_deployment_details,
@ -898,8 +794,6 @@ export function useSetupWizard(props, emit) {
dnsCredentials,
dnsCredentialsVisible,
maskCredential,
emailCredentials,
emailCredentialsVisible,
dnsTestDomain,
dnsVerification,
currentStep,
@ -913,8 +807,6 @@ export function useSetupWizard(props, emit) {
emailAutofillElement,
handleEmailAutofill,
targetLabel,
emailCredentialLabel,
emailCredentialPlaceholder,
resetURLPreview,
selectedDNSProvider,
dnsVerificationValid,

View File

@ -35,8 +35,6 @@ const {
dnsCredentials,
dnsCredentialsVisible,
maskCredential,
emailCredentials,
emailCredentialsVisible,
dnsTestDomain,
dnsVerification,
currentStep,
@ -51,8 +49,6 @@ const {
handleEmailAutofill,
targetLabel,
resetURLPreview,
emailCredentialLabel,
emailCredentialPlaceholder,
selectedDNSProvider,
dnsVerificationValid,
formatVerificationExpiry,
@ -314,31 +310,7 @@ const {
<legend><span>02</span>服务与通知</legend>
<div class="notification-layout">
<div class="notification-guide">
<strong>
先完成基础配置
<HelpTip>
填写邮件服务地址即可继续,其他项目已经提供合理默认值。
</HelpTip>
</strong>
</div>
<div class="notification-row notification-service-row">
<label class="confirm-row compact-row">
<input
id="email-http-enabled"
v-model="form.email_http_enabled"
type="checkbox"
/>
<span>
<strong>
启用 HTTP 邮件通知
<HelpTip>
通过第三方邮件 API 发送任务通知。
</HelpTip>
</strong>
</span>
</label>
<label class="confirm-row compact-row">
<input
id="email-smtp-enabled"
@ -437,56 +409,6 @@ const {
</label>
</div>
<div class="notification-smtp-tip">
SMTP 密码只会写入加密敏感信息存储,不会回显到配置文件、接口响应或日志。建议优先使用 STARTTLS 或隐式 TLS。
</div>
</div>
<div v-if="form.email_http_enabled" class="notification-basic-panel">
<div class="notification-section-heading">
<div>
<strong>
邮件通知基础配置
<HelpTip>
填写服务地址、发件人和收件人即可,技术参数无需修改。
</HelpTip>
</strong>
</div>
<span class="notification-recommended-badge">推荐配置</span>
</div>
<div class="notification-row notification-basic-row">
<label>
<span>
邮件服务地址
<HelpTip>
填写邮件服务提供的地址,不需要自行拼接接口路径。
</HelpTip>
</span>
<input
id="email-endpoint"
name="email_endpoint"
v-model.trim="form.email_endpoint"
type="url"
placeholder="https://mail.example.com"
/>
</label>
<label>
<span>
邮件服务名称(可选)
<HelpTip>
仅用于日志和通知内容中的识别。
</HelpTip>
</span>
<input
id="email-provider"
name="email_provider"
v-model.trim="form.email_provider"
placeholder="例如:Smail"
/>
</label>
</div>
<div class="notification-row notification-sender-row">
<label>
<span>发件人名称</span>
@ -528,7 +450,7 @@ const {
<span>只有手动启动 Scert-admin 后,此地址才会临时可用。</span>
</div>
<div v-if="form.email_http_enabled" class="notification-advanced-panel">
<div v-if="form.email_smtp_enabled" class="notification-advanced-panel">
<button
class="notification-advanced-toggle"
type="button"
@ -537,7 +459,6 @@ const {
>
<span>
<strong>高级配置</strong>
<small>认证、请求路径、重试策略和通知内容选项</small>
</span>
<span class="notification-advanced-action">
{{ notificationAdvancedVisible ? "收起配置" : "展开配置" }}
@ -545,101 +466,6 @@ const {
</button>
<div v-if="notificationAdvancedVisible" class="notification-advanced-content">
<div class="notification-advanced-tip">
默认值适用于常见 HTTP 邮件服务。只有服务方提供了特殊要求时,才需要修改这里的内容。
</div>
<div class="notification-row notification-api-row">
<label>
<span>HTTP 请求路径</span>
<input
id="email-http-path"
v-model.trim="form.email_http_path"
placeholder="/api/email/send"
/>
</label>
<label>
<span>HTTP 认证方式</span>
<select
id="email-http-auth-mode"
v-model="form.email_http_auth_mode"
>
<option value="none">无认证</option>
<option value="bearer">Bearer Token</option>
<option value="hmac_sha256">HMAC-SHA256</option>
<option value="session">账号登录会话</option>
</select>
</label>
</div>
<div v-if="form.email_http_auth_mode === 'session'" class="notification-row notification-auth-row">
<label>
<span>登录请求路径</span>
<input
id="email-http-login-path"
v-model.trim="form.email_http_login_path"
placeholder="/api/login"
/>
</label>
<label v-if="!isBootstrap">
<span>已有凭据引用(可选)</span>
<input
id="email-http-credential-ref"
v-model.trim="form.email_http_credential_ref"
placeholder="已有安全凭据引用"
/>
</label>
</div>
<div v-else class="notification-row notification-auth-row">
<label v-if="!isBootstrap">
<span>已有凭据引用(可选)</span>
<input
id="email-http-credential-ref"
v-model.trim="form.email_http_credential_ref"
placeholder="已有安全凭据引用"
/>
</label>
<div v-if="form.email_http_auth_mode === 'none'" class="notification-auth-note">
当前接口不启用认证,不需要填写凭据。
</div>
</div>
<div
v-if="form.email_http_auth_mode !== 'none'"
class="notification-credential-field"
>
<div class="credential-visibility-row">
<span>{{ emailCredentialLabel }}</span>
<label class="credential-visibility-control">
<input
v-model="emailCredentialsVisible"
type="checkbox"
/>
<span>显示凭据明文</span>
</label>
</div>
<textarea
v-if="emailCredentialsVisible"
id="email-http-credentials-visible"
v-model="emailCredentials"
rows="3"
autocomplete="off"
:placeholder="emailCredentialPlaceholder"
></textarea>
<textarea
v-else
id="email-http-credentials-masked"
:value="maskCredential(emailCredentials)"
rows="3"
readonly
autocomplete="off"
></textarea>
<small>
配置时允许明文显示;提交后仅保存加密凭据引用,普通配置文件不会保存密码或令牌。
</small>
</div>
<div class="notification-row notification-delivery-row">
<label>
<span>通知事件模式</span>
@ -674,14 +500,6 @@ const {
</div>
<div class="notification-bottom-row">
<label class="email-headers-field">
<span>HTTP 自定义请求头(JSON,可选)</span>
<textarea
v-model.trim="form.email_http_headers"
rows="2"
placeholder='例如:{"X-Tenant":"scert"}'
></textarea>
</label>
<div class="setup-grid three-columns compact-options email-option-grid">
<label class="confirm-row compact-row">
<input

View File

@ -110,11 +110,14 @@ for (const driver of ["sqlite", "mysql", "postgres"]) {
email_http_enabled: true,
email_endpoint: "https://mail.example.test",
email_http_auth_mode: "bearer",
email_smtp_enabled: true,
email_smtp_host: "smtp.example.test",
email_smtp_username: "sender@example.test",
email_smtp_password: "smtp-test-only",
email_retry_count: 0,
recipients: "first@example.test,second@example.test",
email_http_credential_ref: "secret://client/not-trusted",
});
state.emailCredentials.value = JSON.stringify({ token: "test-notification-token" });
for (let step = 1; step < 4; step += 1) {
assert.equal(state.currentStep.value, step);
await state.handleSubmit();
@ -128,10 +131,18 @@ for (const driver of ["sqlite", "mysql", "postgres"]) {
assert.equal(submitted[0].database_verification_token, "database-token");
assert.equal(submitted[0].dns_verification_token, "dns-token");
assert.equal(submitted[0].web_public_ip, "203.0.113.20");
assert.equal(submitted[0].notification.http_enabled, true);
assert.equal(submitted[0].notification.http_enabled, false);
assert.equal(submitted[0].notification.smtp_enabled, true);
assert.equal(submitted[0].notification.smtp_host, "smtp.example.test");
assert.equal(submitted[0].notification.smtp_username, "sender@example.test");
assert.equal(submitted[0].notification.smtp_password, "smtp-test-only");
assert.equal(submitted[0].notification.smtp_port, 587);
assert.equal(submitted[0].notification.smtp_mode, "starttls");
assert.equal(submitted[0].notification.retry_count, 0);
assert.deepEqual(submitted[0].notification.recipients, ["first@example.test", "second@example.test"]);
assert.deepEqual(submitted[0].notification.http_credentials, { token: "test-notification-token" });
for (const key of ["endpoint", "http_auth_mode", "http_credentials", "http_path", "http_headers"]) {
assert.equal(Object.hasOwn(submitted[0].notification, key), false);
}
for (const key of ["dns_credential_ref", "certificate_root", "database", "confirm_password"]) {
assert.equal(Object.hasOwn(submitted[0], key), false);
}

View File

@ -294,7 +294,7 @@
}
.access-shell--setup .notification-layout:has(.smtp-basic-panel):has(.notification-advanced-content) {
grid-template-columns: minmax(0, 0.95fr) minmax(0, 0.95fr) minmax(0, 1.3fr);
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.access-shell--setup .notification-layout:has(.smtp-basic-panel):has(.notification-advanced-content) .notification-basic-row {

View File

@ -22,6 +22,25 @@ function hasClass(name) {
&& prop.value?.content.split(/\s+/).includes(name));
}
test("notification step exposes SMTP only and keeps sender fields in its panel", () => {
const root = template("./SetupWizard.vue");
const step = find(root, hasClass("setup-notification-step"));
const panel = find(step, hasClass("smtp-basic-panel"));
const id = (value) => (node) => node.props?.some((prop) =>
prop.name === "id" && prop.value?.content === value);
assert.ok(find(step, id("email-smtp-enabled")));
for (const value of ["email-http-enabled", "email-endpoint", "email-http-path", "email-http-auth-mode"]) {
assert.equal(find(step, id(value)), undefined);
}
for (const value of ["email-smtp-host", "email-smtp-password", "email-sender-name", "email-sender-email", "recipients"]) {
assert.ok(find(panel, id(value)), value);
}
const advanced = find(step, hasClass("notification-advanced-panel"));
assert.equal(advanced.props.find((prop) => prop.name === "if").exp.content, "form.email_smtp_enabled");
assert.ok(find(advanced, hasClass("notification-delivery-row")));
assert.ok(find(advanced, hasClass("email-option-grid")));
});
test("account heading precedes the two-column account fields", () => {
const root = template("./SetupWizard.vue");
const card = find(root, (node) => node.tag === "fieldset"