const state = {
me: null,
config: null,
smtpCredential: null,
dnsCredentials: [],
apiTokens: [],
domains: [],
events: [],
users: [],
settings: null,
selectedId: null,
view: 'domains',
busy: false
};
const viewMeta = {
domains: {
eyebrow: 'Domains',
title: '发信域名',
subtitle: '管理发信域名、DNS 检查和测试发送。'
},
dns: {
eyebrow: 'DNS API',
title: 'DNS API 凭据',
subtitle: '按服务商保存多组 DNS 凭据,再绑定到具体域名。'
},
smtp: {
eyebrow: 'SMTP',
title: 'SMTP 凭据',
subtitle: '生成可复制的 SMTP 用户名和密码。'
},
tokens: {
eyebrow: 'API Tokens',
title: '发送 API Token',
subtitle: '为不同系统创建独立发信 Token。'
},
admin: {
eyebrow: 'Admin',
title: '系统设置',
subtitle: '管理员设置默认发信参数和用户状态。'
}
};
const els = {
runtimeLine: document.querySelector('#runtimeLine'),
viewEyebrow: document.querySelector('#viewEyebrow'),
viewTitle: document.querySelector('#viewTitle'),
securityNotice: document.querySelector('#securityNotice'),
accountBox: document.querySelector('#accountBox'),
userRole: document.querySelector('#userRole'),
navButtons: document.querySelectorAll('.primary-nav [data-view]'),
viewPanels: document.querySelectorAll('[data-view-panel]'),
adminNavButton: document.querySelector('#adminNavButton'),
addDomainForm: document.querySelector('#addDomainForm'),
domainDnsCredential: document.querySelector('#domainDnsCredential'),
domainReadiness: document.querySelector('#domainReadiness'),
smtpCredentialForm: document.querySelector('#smtpCredentialForm'),
smtpCredentialState: document.querySelector('#smtpCredentialState'),
smtpCredentialCopy: document.querySelector('#smtpCredentialCopy'),
smtpUsername: document.querySelector('#smtpUsername'),
smtpPassword: document.querySelector('#smtpPassword'),
generateSmtpPassword: document.querySelector('#generateSmtpPassword'),
dnsCredentialForm: document.querySelector('#dnsCredentialForm'),
dnsCredentialFormTitle: document.querySelector('#dnsCredentialFormTitle'),
dnsCredentialFormHint: document.querySelector('#dnsCredentialFormHint'),
dnsCredentialIdField: document.querySelector('#dnsCredentialIdField'),
dnsCredentialName: document.querySelector('#dnsCredentialName'),
dnsDefaultTtl: document.querySelector('#dnsDefaultTtl'),
dnsProvider: document.querySelector('#dnsProvider'),
dnsZoneName: document.querySelector('#dnsZoneName'),
dnsCredentialList: document.querySelector('#dnsCredentialList'),
dnsCredentialCount: document.querySelector('#dnsCredentialCount'),
resetDnsCredentialForm: document.querySelector('#resetDnsCredentialForm'),
apiTokenForm: document.querySelector('#apiTokenForm'),
apiTokenList: document.querySelector('#apiTokenList'),
apiTokenCount: document.querySelector('#apiTokenCount'),
defaultSenderHost: document.querySelector('#defaultSenderHost'),
defaultSendingIp: document.querySelector('#defaultSendingIp'),
defaultSpfExtra: document.querySelector('#defaultSpfExtra'),
domainList: document.querySelector('#domainList'),
domainCount: document.querySelector('#domainCount'),
detailPanel: document.querySelector('#detailPanel'),
adminPanel: document.querySelector('#adminPanel'),
refreshButton: document.querySelector('#refreshButton'),
refreshButtonMobile: document.querySelector('#refreshButtonMobile'),
logoutButton: document.querySelector('#logoutButton'),
toastHost: document.querySelector('#toastHost')
};
init();
async function init() {
bindEvents();
await refreshAll();
}
function bindEvents() {
els.refreshButton.addEventListener('click', refreshAll);
els.refreshButtonMobile?.addEventListener('click', refreshAll);
els.logoutButton.addEventListener('click', logout);
document.addEventListener('click', handleGlobalClick);
els.addDomainForm.addEventListener('submit', addDomain);
els.smtpCredentialForm.addEventListener('submit', saveSmtpCredential);
els.generateSmtpPassword.addEventListener('click', generateSmtpPassword);
els.dnsCredentialForm.addEventListener('submit', saveDnsCredential);
els.dnsProvider.addEventListener('change', toggleDnsProviderFields);
els.resetDnsCredentialForm.addEventListener('click', resetDnsCredentialForm);
els.apiTokenForm.addEventListener('submit', createApiToken);
els.domainList.addEventListener('click', (event) => {
const item = event.target.closest('[data-domain-id]');
if (!item) return;
state.selectedId = Number(item.dataset.domainId);
setView('domains');
render();
});
els.detailPanel.addEventListener('submit', handleDetailSubmit);
els.adminPanel.addEventListener('submit', handleDetailSubmit);
}
async function refreshAll() {
setBusy(true);
try {
const me = await api('/api/me');
state.me = me.user;
const baseCalls = [
api('/api/config'),
api('/api/domains'),
api('/api/events'),
api('/api/smtp-credential'),
api('/api/dns-credentials'),
api('/api/api-tokens')
];
const [config, domains, events, smtpCredential, dnsCredentials, apiTokens] = await Promise.all(baseCalls);
state.config = config;
state.domains = domains.domains || [];
state.events = events.events || [];
state.smtpCredential = smtpCredential.credential || null;
state.dnsCredentials = dnsCredentials.credentials || [];
state.apiTokens = apiTokens.tokens || [];
if (state.me?.role === 'admin') {
const [settings, users] = await Promise.all([
api('/api/admin/settings'),
api('/api/admin/users')
]);
state.settings = settings.settings || null;
state.users = users.users || [];
}
if (!state.selectedId && state.domains.length) state.selectedId = state.domains[0].id;
if (state.selectedId && !state.domains.some((domain) => domain.id === state.selectedId)) {
state.selectedId = state.domains[0]?.id || null;
}
renderDefaults();
render();
} catch (error) {
toast(error.message, 'error');
} finally {
setBusy(false);
}
}
function renderDefaults() {
if (!state.config) return;
const ip = state.config.sendingIp || '未设置发信 IP';
els.runtimeLine.textContent = `${state.config.mailHostname} · ${ip} · ${viewMeta[state.view]?.subtitle || ''}`;
els.defaultSenderHost.value = state.config.mailHostname || '';
els.defaultSendingIp.value = state.config.sendingIp || '';
els.defaultSpfExtra.value = state.config.defaultSpfMechanisms || '';
els.smtpUsername.value = state.smtpCredential?.username || state.config.submission?.username || '';
els.smtpCredentialState.textContent = state.smtpCredential?.passwordSet ? '已配置' : '未配置';
els.userRole.textContent = state.me?.role === 'admin' ? '管理员' : '用户';
els.userRole.className = `badge ${state.me?.role === 'admin' ? 'bg-green-lt' : 'bg-secondary-lt'}`;
els.adminNavButton.classList.toggle('hidden', state.me?.role !== 'admin');
if (state.me?.role !== 'admin' && state.view === 'admin') setView('domains');
els.accountBox.innerHTML = `
Username
${escapeHtml(state.me?.username || '')}
Email
${escapeHtml(state.me?.email || '')}
`;
renderDnsOptions();
renderDomainReadiness();
renderSmtpCredentialCopy();
renderDnsCredentials();
renderApiTokens();
renderAdminPanel();
renderSecurityNotice();
toggleDnsProviderFields();
}
function render() {
els.domainCount.textContent = String(state.domains.length);
els.dnsCredentialCount.textContent = String(state.dnsCredentials.length);
els.apiTokenCount.textContent = String(state.apiTokens.length);
renderViewMeta();
renderDomainList();
renderDetail();
}
function renderViewMeta() {
const meta = viewMeta[state.view] || viewMeta.domains;
els.viewEyebrow.textContent = meta.eyebrow;
els.viewTitle.textContent = meta.title;
if (state.config) {
const ip = state.config.sendingIp || '未设置发信 IP';
els.runtimeLine.textContent = `${state.config.mailHostname} · ${ip} · ${meta.subtitle}`;
}
for (const button of els.navButtons) {
button.classList.toggle('active', button.dataset.view === state.view);
}
for (const panel of els.viewPanels) {
panel.classList.toggle('active', panel.dataset.viewPanel === state.view);
}
}
function setView(view) {
if (!viewMeta[view]) return;
if (view === 'admin' && state.me?.role !== 'admin') return;
state.view = view;
renderViewMeta();
}
function renderSecurityNotice() {
els.securityNotice.classList.toggle('hidden', !state.config.usingDefaultAdminPassword);
els.securityNotice.textContent = state.config.usingDefaultAdminPassword
? '当前仍在使用默认管理密码,请修改 .env 后重启服务。'
: '';
}
function renderDomainReadiness() {
const verified = state.domains.filter((domain) => domain.status?.verified).length;
const latestEvent = state.events[0];
els.domainReadiness.innerHTML = `
${metricCard('域名', state.domains.length, `${verified} 个已通过`)}
${metricCard('DNS API', state.dnsCredentials.length, state.dnsCredentials.length ? '可一键写入' : '尚未配置')}
${metricCard('SMTP', state.smtpCredential?.passwordSet ? 'Ready' : 'Missing', state.smtpCredential?.username || '未设置凭据')}
${metricCard('最近发送', latestEvent ? formatDate(latestEvent.createdAt) : '暂无', latestEvent?.status || '无记录')}
${!state.dnsCredentials.length ? '' : ''}
${!state.smtpCredential?.passwordSet ? '' : ''}
`;
}
function metricCard(label, value, hint) {
return `
${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(hint || '')}
`;
}
function renderDnsOptions(selectedId = '') {
const activeId = selectedId || els.domainDnsCredential.value || '';
if (!state.dnsCredentials.length) {
els.domainDnsCredential.innerHTML = '';
return;
}
const options = [
'',
...state.dnsCredentials.map((credential) => `
`)
].join('');
els.domainDnsCredential.innerHTML = options;
}
function renderSmtpCredentialCopy() {
const credential = state.smtpCredential;
if (!credential?.username && !credential?.passwordSet) {
els.smtpCredentialCopy.innerHTML = `
还没有 SMTP 凭据
保存用户名和密码后,这里会显示可复制内容。
`;
return;
}
const password = credential?.password || '';
els.smtpCredentialCopy.innerHTML = `
Username
${escapeHtml(credential.username || '')}
Password
${password
? `
${escapeHtml(password)}`
: '
旧密码无法回显,请重新设置一次新密码后复制。
'}
`;
}
function renderDnsCredentials() {
els.dnsCredentialCount.textContent = String(state.dnsCredentials.length);
if (!state.dnsCredentials.length) {
els.dnsCredentialList.innerHTML = `
暂无 DNS API 凭据
先在右侧新增一组凭据;添加域名时下拉框就会出现可选项。
`;
return;
}
els.dnsCredentialList.innerHTML = state.dnsCredentials.map((credential) => `
${escapeHtml(credential.name)}
${badge({ className: credential.credentialSet ? 'ok' : 'warn', label: credential.credentialSet ? '已保存' : '缺密钥' })}
${providerLabel(credential.provider)} · ${escapeHtml(credential.zoneName || '未设置 Zone')} · TTL ${escapeHtml(credential.defaultTtl)}
`).join('');
}
function renderApiTokens() {
els.apiTokenCount.textContent = String(state.apiTokens.length);
if (!state.apiTokens.length) {
els.apiTokenList.innerHTML = `
暂无发送 Token
创建后请立即保存完整 Token,页面之后只显示前缀。
`;
return;
}
els.apiTokenList.innerHTML = state.apiTokens.map((token) => `
${escapeHtml(token.name)}
${escapeHtml(token.tokenPrefix)}... · ${escapeHtml(formatDate(token.createdAt))}
`).join('');
}
function renderAdminPanel() {
if (state.me?.role !== 'admin') {
els.adminPanel.classList.add('hidden');
els.adminPanel.innerHTML = '';
return;
}
const settings = state.settings || state.config || {};
els.adminPanel.classList.remove('hidden');
els.adminPanel.innerHTML = `
${state.users.slice(0, 12).map((user) => `
${escapeHtml(user.username)}
${escapeHtml(user.email)} · ${escapeHtml(user.role)} · ${escapeHtml(user.status)}
`).join('')}
`;
}
function renderDomainList() {
if (!state.domains.length) {
els.domainList.innerHTML = '暂无域名
';
return;
}
els.domainList.innerHTML = state.domains.map((domain) => {
const status = statusMeta(domain.status);
return `
`;
}).join('');
}
function renderDetail() {
const domain = state.domains.find((item) => item.id === state.selectedId);
if (!domain) {
els.detailPanel.innerHTML = `
选择或添加一个域名
DNS 引导、验证结果、DKIM 记录和测试发送会显示在这里。
`;
return;
}
const guide = domain.status || {};
const records = guide.records || [];
const warnings = guide.warnings || [];
const checkedAt = guide.checkedAt ? formatDate(guide.checkedAt) : '尚未检查';
const applyDisabled = domain.dnsCredentialId ? '' : 'disabled';
const applyTitle = domain.dnsCredentialId ? '一键配置 DNS' : '请先在域名设置里绑定 DNS API 凭据';
els.detailPanel.innerHTML = `
${renderSetupOverview(domain, guide, records)}
${guide.apply ? renderApplyResult(guide.apply) : ''}
${warnings.length ? renderWarnings(warnings) : ''}
${renderDnsGuide(records)}
${renderSettingsForm(domain)}
${renderLiveDns(guide.live)}
${renderSendForm(domain)}
${escapeHtml(apiExample(domain))}
${renderOptionalRecords(guide.optionalRecords || [])}
`;
}
function renderSetupOverview(domain, guide, records) {
const important = [['verification', '域名验证'], ['dkim', 'DKIM'], ['spf', 'SPF'], ['dmarc', 'DMARC'], ['ptr', 'PTR']];
const cards = important.map(([key, label]) => {
const record = records.find((item) => item.key === key);
const meta = statusMeta(record || {});
return `${escapeHtml(label)}${record ? escapeHtml(meta.label) : '待生成'}
`;
}).join('');
const credential = state.dnsCredentials.find((item) => item.id === domain.dnsCredentialId);
return `
Sending domain
${escapeHtml(domain.domain)}
${escapeHtml(domain.senderHost)} / ${escapeHtml(domain.sendingIp || '未设置 IP')}
${credential ? `DNS API: ${escapeHtml(credential.name)}` : 'DNS API: 未绑定'}
${cards}
`;
}
function renderApplyResult(apply) {
const rows = apply.results || [];
return `
${rows.map((row) => `
${escapeHtml(row.key)} · ${escapeHtml(row.type)}
${escapeHtml(row.host)} · ${escapeHtml(row.detail || row.error || '')}
${badge({ className: row.ok ? 'ok' : 'failed', label: row.ok ? '成功' : '失败' })}
`).join('')}
`;
}
function renderDnsGuide(records) {
if (!records.length) {
return `
尚未生成检查结果
点击“立即检查”后会生成域名验证、DKIM、SPF、DMARC 和 PTR 引导。
`;
}
return `${records.map((record, index) => renderRecordCard(record, index + 1)).join('')}
`;
}
function renderRecordCard(record, index) {
const meta = statusMeta(record);
const current = Array.isArray(record.current) ? record.current : (record.current ? [record.current] : []);
const warnings = record.warnings || [];
return `
${index}
${escapeHtml(record.label)}
${escapeHtml(record.type)} · ${escapeHtml(record.host)}
${badge(meta)}
目标值
${escapeHtml(record.value || '')}
${current.length ? `
当前值${current.map((value) => `${escapeHtml(value)}`).join('')}
` : ''}
${warnings.length ? `
${warnings.map((warning) => `- ${escapeHtml(warning)}
`).join('')}
` : ''}
`;
}
function renderRecordTable(records) {
if (!records.length) return '点击“立即检查”生成 SPF、DKIM、DMARC 和 PTR 检查结果。
';
return `
| 项目 | 主机 | 类型 | 目标值 | 状态 | |
${records.map(renderRecordRow).join('')}
`;
}
function renderRecordRow(record) {
const meta = statusMeta(record);
return `
| ${escapeHtml(record.label)} |
${escapeHtml(record.host)} |
${escapeHtml(record.type)} |
${escapeHtml(record.value || '')} |
${badge(meta)} |
|
`;
}
function renderSettingsForm(domain) {
const dnsOptions = [
'',
...state.dnsCredentials.map((credential) => ``)
].join('');
return `
`;
}
function renderLiveDns(live) {
if (!live) return '暂无检查数据
';
const rows = [['根域 TXT', live.rootTxt], ['验证 TXT', live.verificationTxt], ['DKIM TXT', live.dkimTxt], ['DMARC TXT', live.dmarcTxt], ['发信主机 A', live.senderA], ['发信 IP PTR', live.ptr]];
return `${rows.map(([label, values]) => `
${label}${(values && values.length) ? values.map((value) => `
${escapeHtml(value)}`).join('') : '
未发现
'}
`).join('')}
`;
}
function renderSendForm(domain) {
return `
`;
}
function renderEvents(domain) {
const events = state.events.filter((event) => event.domain === domain.domain).slice(0, 8);
if (!events.length) return '暂无发送记录
';
return `${events.map((event) => `
${escapeHtml(event.subject)}${badge({ className: event.status === 'queued' ? 'ok' : 'failed', label: event.status })}
${escapeHtml(event.sender)} -> ${escapeHtml((event.recipients || []).join(', '))}${escapeHtml(formatDate(event.createdAt))}`).join('')}
`;
}
function renderSmtpBox(domain) {
const submission = state.config?.submission;
const credential = state.smtpCredential;
const password = credential?.password || '';
if (!submission?.enabled) return 'SMTP Submission 未启用。
';
return `
Host${escapeHtml(submission.host)}
Ports${(submission.ports || []).map((item) => `${escapeHtml(item.port)} · ${escapeHtml(item.protocol)}`).join('')}
Username${escapeHtml(credential?.username || submission.username || '')}
Password${password ? `
${escapeHtml(password)}
` : '
旧密码无法回显,请在 SMTP 页面重新设置一次新密码后复制。
'}
AUTH${submission.requireTlsForAuth ? '需要 TLS 后认证' : '允许明文认证'}
Fromnoreply@${escapeHtml(domain.domain)}
`;
}
function renderOptionalRecords(records) {
if (!records.length) return '完成基础发信配置后可逐步启用。
';
return renderRecordTable(records.map((record) => ({ ...record, status: 'idle' })));
}
function renderWarnings(warnings) {
return `${warnings.map((warning) => `- ${escapeHtml(warning)}
`).join('')}
`;
}
function apiExample(domain) {
const token = state.apiTokens[0] ? `${state.apiTokens[0].tokenPrefix}...` : '';
return `curl -X POST ${state.config?.appBaseUrl || 'https://mail.ss5.xyz'}/api/send \\
-H 'Authorization: Bearer ${token}' \\
-H 'Content-Type: application/json' \\
-d '{
"from": "noreply@${domain.domain}",
"to": "user@example.com",
"subject": "Hello from MailHub",
"text": "Signed with DKIM and queued by MailHub."
}'`;
}
async function addDomain(event) {
event.preventDefault();
await mutate(async () => {
const data = Object.fromEntries(new FormData(event.target).entries());
const result = await api('/api/domains', { method: 'POST', body: JSON.stringify(data) });
state.domains.unshift(result.domain);
state.selectedId = result.domain.id;
event.target.reset();
renderDefaults();
render();
await checkSelected();
});
}
async function saveSmtpCredential(event) {
event.preventDefault();
await mutate(async () => {
const data = Object.fromEntries(new FormData(event.target).entries());
const result = await api('/api/smtp-credential', { method: 'PUT', body: JSON.stringify(data) });
state.smtpCredential = result.credential;
if (state.config?.submission) {
state.config.submission.username = result.credential.username;
state.config.submission.passwordSet = result.credential.passwordSet;
}
els.smtpPassword.value = '';
renderDefaults();
render();
toast('SMTP 凭据已保存');
});
}
async function saveDnsCredential(event) {
event.preventDefault();
await mutate(async () => {
const data = Object.fromEntries(new FormData(event.target).entries());
const id = Number(data.id || 0);
delete data.id;
const path = id ? `/api/dns-credentials/${id}` : '/api/dns-credentials';
const method = id ? 'PATCH' : 'POST';
const result = await api(path, { method, body: JSON.stringify(data) });
if (id) {
state.dnsCredentials = state.dnsCredentials.map((item) => item.id === id ? result.credential : item);
} else {
state.dnsCredentials.unshift(result.credential);
}
resetDnsCredentialForm();
renderDefaults();
render();
toast(id ? 'DNS API 已更新' : 'DNS API 已新增');
});
}
function editDnsCredential(id) {
const credential = state.dnsCredentials.find((item) => item.id === id);
if (!credential) return;
setView('dns');
resetDnsCredentialForm();
els.dnsCredentialIdField.value = credential.id;
els.dnsCredentialName.value = credential.name || '';
els.dnsProvider.value = credential.provider || 'cloudflare';
els.dnsZoneName.value = credential.zoneName || '';
els.dnsDefaultTtl.value = credential.defaultTtl || 600;
els.dnsCredentialFormTitle.textContent = `编辑 ${credential.name}`;
els.dnsCredentialFormHint.textContent = '密钥字段留空会保留原值。';
toggleDnsProviderFields();
els.dnsCredentialName.focus();
}
function resetDnsCredentialForm() {
els.dnsCredentialForm.reset();
els.dnsCredentialIdField.value = '';
els.dnsDefaultTtl.value = 600;
els.dnsCredentialFormTitle.textContent = '新增 DNS API';
els.dnsCredentialFormHint.textContent = '选择服务商后只填写对应字段。';
toggleDnsProviderFields();
}
function toggleDnsProviderFields() {
const provider = els.dnsProvider.value || 'cloudflare';
for (const fieldset of document.querySelectorAll('[data-provider-field]')) {
fieldset.classList.toggle('hidden', fieldset.dataset.providerField !== provider);
}
}
async function createApiToken(event) {
event.preventDefault();
await mutate(async () => {
const data = Object.fromEntries(new FormData(event.target).entries());
const result = await api('/api/api-tokens', { method: 'POST', body: JSON.stringify(data) });
state.apiTokens.unshift(result.token);
event.target.reset();
renderDefaults();
render();
await navigator.clipboard.writeText(result.token.token);
toast('Token 已生成并复制,请立即保存');
});
}
function generateSmtpPassword() {
const bytes = new Uint8Array(24);
crypto.getRandomValues(bytes);
const password = btoa(String.fromCharCode(...bytes)).replace(/[+/=]/g, '').slice(0, 28);
els.smtpPassword.type = 'text';
els.smtpPassword.value = password;
els.smtpPassword.focus();
els.smtpPassword.select();
}
async function handleGlobalClick(event) {
const viewButton = event.target.closest('[data-view]');
if (viewButton && !event.target.closest('[data-domain-id]')) {
event.preventDefault();
setView(viewButton.dataset.view);
return;
}
const copyButton = event.target.closest('[data-copy]');
if (copyButton) {
event.preventDefault();
const value = copyButton.dataset.copy || '';
if (!value) return;
await navigator.clipboard.writeText(value);
toast('已复制');
return;
}
const actionTarget = event.target.closest('[data-action]');
const action = actionTarget?.dataset.action;
if (!action) return;
const id = Number(actionTarget.closest('[data-id]')?.dataset.id || actionTarget.dataset.id || 0);
if (action === 'check') return checkSelected();
if (action === 'apply-dns') return applyDnsSelected();
if (action === 'copy-all-dns') return copyAllDns();
if (action === 'rotate-dkim') return rotateDkim();
if (action === 'delete-domain') return deleteSelected();
if (action === 'edit-dns') return editDnsCredential(id);
if (action === 'test-dns') return testDnsCredential(id);
if (action === 'delete-dns') return deleteDnsCredential(id);
if (action === 'delete-token') return deleteApiToken(id);
if (action === 'toggle-user') return toggleUser(id, actionTarget.closest('[data-status]')?.dataset.status);
}
async function handleDetailSubmit(event) {
const form = event.target.closest('form[data-form]');
if (!form) return;
event.preventDefault();
if (form.dataset.form === 'settings') return saveSettings(form);
if (form.dataset.form === 'send') return sendTest(form);
if (form.dataset.form === 'admin-settings') return saveAdminSettings(form);
}
async function checkSelected() {
const id = state.selectedId;
if (!id) return;
await mutate(async () => {
const result = await api(`/api/domains/${id}/check`, { method: 'POST' });
replaceDomain(result.domain);
render();
});
}
async function applyDnsSelected() {
const id = state.selectedId;
if (!id) return;
await mutate(async () => {
const result = await api(`/api/domains/${id}/apply-dns`, { method: 'POST' });
replaceDomain(result.domain);
render();
toast(result.apply?.ok ? 'DNS 配置完成' : 'DNS 配置部分失败');
});
}
async function saveSettings(form) {
const id = state.selectedId;
const data = Object.fromEntries(new FormData(form).entries());
await mutate(async () => {
const result = await api(`/api/domains/${id}`, { method: 'PATCH', body: JSON.stringify(data) });
replaceDomain(result.domain);
render();
await checkSelected();
});
}
async function sendTest(form) {
const id = state.selectedId;
const data = Object.fromEntries(new FormData(form).entries());
await mutate(async () => {
await api(`/api/domains/${id}/test-send`, { method: 'POST', body: JSON.stringify(data) });
const events = await api('/api/events');
state.events = events.events || [];
renderDefaults();
render();
toast('已提交到发信队列');
});
}
async function saveAdminSettings(form) {
const data = Object.fromEntries(new FormData(form).entries());
await mutate(async () => {
const result = await api('/api/admin/settings', { method: 'PATCH', body: JSON.stringify(data) });
state.settings = result.settings;
state.config = { ...state.config, ...result.settings };
renderDefaults();
render();
toast('系统设置已保存');
});
}
async function rotateDkim() {
const id = state.selectedId;
if (!confirm('轮换 DKIM 后需要更新 DNS TXT 记录。继续?')) return;
await mutate(async () => {
const result = await api(`/api/domains/${id}/rotate-dkim`, { method: 'POST' });
replaceDomain(result.domain);
render();
await checkSelected();
});
}
async function deleteSelected() {
const id = state.selectedId;
const domain = state.domains.find((item) => item.id === id);
if (!confirm(`删除 ${domain?.domain || '该域名'}?`)) return;
await mutate(async () => {
await api(`/api/domains/${id}`, { method: 'DELETE' });
state.domains = state.domains.filter((item) => item.id !== id);
state.selectedId = state.domains[0]?.id || null;
renderDefaults();
render();
});
}
async function testDnsCredential(id) {
await mutate(async () => {
const result = await api(`/api/dns-credentials/${id}/test`, { method: 'POST' });
toast(result.ok ? 'DNS API 连接成功' : result.error || 'DNS API 连接失败');
});
}
async function deleteDnsCredential(id) {
const credential = state.dnsCredentials.find((item) => item.id === id);
if (!confirm(`删除 ${credential?.name || '该 DNS API 凭据'}?`)) return;
await mutate(async () => {
await api(`/api/dns-credentials/${id}`, { method: 'DELETE' });
state.dnsCredentials = state.dnsCredentials.filter((item) => item.id !== id);
state.domains = state.domains.map((domain) => domain.dnsCredentialId === id ? { ...domain, dnsCredentialId: null } : domain);
if (Number(els.dnsCredentialIdField.value || 0) === id) resetDnsCredentialForm();
renderDefaults();
render();
});
}
async function deleteApiToken(id) {
if (!confirm('删除该 API Token?')) return;
await mutate(async () => {
await api(`/api/api-tokens/${id}`, { method: 'DELETE' });
state.apiTokens = state.apiTokens.filter((item) => item.id !== id);
renderDefaults();
render();
});
}
async function toggleUser(id, status) {
const next = status === 'active' ? 'disabled' : 'active';
await mutate(async () => {
const result = await api(`/api/admin/users/${id}`, { method: 'PATCH', body: JSON.stringify({ status: next }) });
state.users = state.users.map((user) => user.id === id ? result.user : user);
renderDefaults();
});
}
async function copyAllDns() {
const domain = state.domains.find((item) => item.id === state.selectedId);
const records = domain?.status?.records || [];
if (!records.length) return toast('暂无 DNS 记录');
const text = records.map((record) => `${record.host}\t${record.type}\t${record.value || ''}`).join('\n');
await navigator.clipboard.writeText(text);
toast('已复制全部 DNS 记录');
}
async function logout() {
setBusy(true);
try {
await fetch('/api/logout', { method: 'POST' });
} finally {
window.location.href = '/login';
}
}
async function mutate(fn) {
setBusy(true);
try {
await fn();
} catch (error) {
toast(error.message, 'error');
} finally {
setBusy(false);
}
}
async function api(path, options = {}) {
const response = await fetch(path, {
...options,
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) }
});
const text = await response.text();
const payload = text ? JSON.parse(text) : {};
if (!response.ok) {
if (response.status === 401) {
window.location.href = '/login';
return {};
}
throw new Error(payload.error || `HTTP ${response.status}`);
}
return payload;
}
function replaceDomain(domain) {
state.domains = state.domains.map((item) => item.id === domain.id ? domain : item);
}
function statusMeta(target) {
if (!target || (!target.checkedAt && !target.status)) return { className: 'idle', label: '未检查' };
if (target.verified || target.status === 'ok') return { className: 'ok', label: '通过' };
if (target.status === 'pending') return { className: 'pending', label: '传播中' };
if (target.status === 'missing') return { className: 'missing', label: '缺失' };
if (target.status === 'warn') return { className: 'warn', label: '需调整' };
return { className: 'warn', label: '待配置' };
}
function badge(meta) {
return `${escapeHtml(meta.label)}`;
}
function badgeTone(className) {
return {
ok: 'bg-green-lt',
pending: 'bg-blue-lt',
warn: 'bg-yellow-lt',
missing: 'bg-red-lt',
failed: 'bg-red-lt',
idle: 'bg-secondary-lt'
}[className] || 'bg-secondary-lt';
}
function providerLabel(provider) {
return { cloudflare: 'Cloudflare', aliyun: '阿里云 DNS', dnspod: '腾讯云 DNSPod' }[provider] || provider;
}
function setBusy(value) {
state.busy = value;
document.body.classList.toggle('busy', value);
els.refreshButton.disabled = value;
if (els.refreshButtonMobile) els.refreshButtonMobile.disabled = value;
}
function toast(message, tone = 'info') {
const node = document.createElement('div');
node.className = `app-toast ${tone}`;
node.textContent = message;
els.toastHost.appendChild(node);
setTimeout(() => node.remove(), 3200);
}
function formatDate(value) {
if (!value) return '';
return new Intl.DateTimeFormat('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(new Date(value));
}
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function escapeAttr(value) {
return escapeHtml(value).replace(/`/g, '`');
}