import { h, PageHeader, Empty, Table, Card, Badge, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll } from '/static/hoover/index.js?v=7';
const _issueState = { domain: '', modalIdx: -1, account: null, validating: false };
function _accountCard(account) {
if (!account || !account.registered) {
return h('div', { class: 'card' },
h('div', { class: 'card-header' },
[
h('span', null, 'ACME Account'),
h('button', {
class: 'btn btn-sm btn-primary',
style: 'margin-left:auto;',
'on:click': () => registerAccountModal(),
}, 'Register Account'),
]
),
h('div', { class: 'card-body' },
h('div', { class: 'text-muted text-sm' }, 'Not registered'),
),
);
}
return h('div', { class: 'card' },
h('div', { class: 'card-header' },
[
h('span', null, 'ACME Account'),
h('button', {
class: 'btn btn-sm btn-outline',
style: 'margin-left:auto;',
'on:click': () => settingsModal(account),
}, '\u2699'),
]
),
h('div', { class: 'card-body' },
h('div', null, ['Registered as ', h('strong', null, esc(account.email))]),
h('div', { class: 'text-sm text-muted' }, ['CA: ', esc(account.ca)]),
),
);
}
function registerAccountModal() {
openModal((inner) => {
formModal(inner, 'Register ACME Account',
[
{ label: 'Email', id: 'reg-email', type: 'email', placeholder: 'you@example.com' },
{
label: 'CA Provider',
id: 'reg-server',
tag: 'select',
options: [['letsencrypt', "Let's Encrypt"], ['zerossl', 'ZeroSSL']],
},
],
[
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal() },
{
label: 'Register',
cls: 'btn-primary',
action: 'r',
handler: async () => {
const email = ($val('reg-email') || '').trim();
if (!email) { toast('Email is required', 'error'); return; }
const server = document.getElementById('reg-server')?.value || 'letsencrypt';
const resp = await apiFetch('/api/certs/account/register', {
method: 'POST',
body: { email, server },
});
if (resp.ok) {
toast('ACME account registered', 'success');
closeModal();
modelFetch('acme');
} else {
toast(resp.error || 'Registration failed', 'error');
}
},
},
],
);
});
}
function settingsModal(account) {
openModal((inner) => {
inner.innerHTML = '
Account Settings
'
+ ''
+ '
'
+ '
'
+ '
'
+ '
'
+ '
'
+ '
Danger Zone
'
+ '
'
+ '
'
+ ''
+ ''
+ '
';
inner.querySelector('[data-action="set-cancel"]')?.addEventListener('click', () => {
closeModal();
});
inner.querySelector('[data-action="set-save"]')?.addEventListener('click', async () => {
const email = ($val('set-email') || '').trim();
if (!email) { toast('Email is required', 'error'); return; }
const resp = await apiFetch('/api/certs/email', {
method: 'POST',
body: { email },
});
if (resp.ok) {
toast('Email updated', 'success');
closeModal();
modelFetch('acme');
} else {
toast(resp.error || 'Failed', 'error');
}
});
inner.querySelector('[data-action="set-deactivate"]')?.addEventListener('click', async () => {
if (!confirm('Deactivate ACME account? You will need to register again to issue certificates.')) return;
const resp = await apiFetch('/api/certs/account', { method: 'DELETE' });
if (resp.ok) {
toast('Account deactivated', 'success');
closeModal();
modelFetch('acme');
} else {
toast(resp.error || 'Failed', 'error');
}
});
});
}
function issueCertModal(state) {
_issueState.domain = '';
_issueState.modalIdx = -1;
_issueState.account = null;
_issueState.validating = false;
(async () => {
const accountResp = await apiFetch('/api/certs/account');
_issueState.account = accountResp.ok ? accountResp.data : null;
_renderIssueModal(state);
})();
}
function _renderIssueModal(state) {
const account = _issueState.account;
const registered = account && account.registered;
const accountBadge = registered
? esc(account.email) + ' (' + esc(account.ca) + ')'
: 'No account registered';
openModal((inner) => {
inner.innerHTML = 'Issue Certificate
'
+ ''
+ '
'
+ 'Using account: ' + esc(accountBadge) + '
'
+ (registered
? '
'
: '
Register an ACME account first
'
)
+ '
'
+ '
'
+ ''
+ (registered
? ''
: ''
+ ''
)
+ '
';
_issueState.modalIdx = document.querySelectorAll('#modal-root > div').length - 1;
inner.querySelector('[data-action="ic-cancel"]')?.addEventListener('click', () => {
closeModal(_issueState.modalIdx);
});
inner.querySelector('[data-action="ic-register"]')?.addEventListener('click', () => {
closeModal(_issueState.modalIdx);
registerAccountModal();
});
if (!registered) return;
inner.querySelector('[data-action="ic-validate"]')?.addEventListener('click', async () => {
if (_issueState.validating) return;
const domain = ($val('ic-domain') || '').trim();
if (!domain) { toast('Domain is required', 'error'); return; }
_issueState.validating = true;
_issueState.domain = domain;
try {
const resp = await apiFetch('/api/certs/validate', {
method: 'POST',
body: { domain },
});
if (!resp.ok) {
toast(resp.error || 'Validation failed', 'error');
return;
}
_showValidate(inner, domain, resp.data.checks, resp.data.ready, state);
} finally {
_issueState.validating = false;
}
});
});
}
function _showValidate(inner, domain, checks, ready, state) {
const resultsHtml = checks.map(c => {
let cls = 'text-success';
let icon = '\u2713';
if (!c.passed && c.blocking) { cls = 'text-danger'; icon = '\u2717'; }
else if (!c.passed && !c.blocking) { cls = 'text-warning'; icon = '\u26A0'; }
return '' + icon + ' ' + esc(c.name) + ''
+ ': ' + '' + esc(c.message) + '
';
}).join('');
inner.innerHTML = 'Validate: ' + esc(domain) + '
'
+ ''
+ '
'
+ '
' + resultsHtml + '
'
+ '
'
+ ''
+ ''
+ ''
+ '
';
inner.querySelector('[data-action="ic-cancel"]')?.addEventListener('click', () => {
closeModal(_issueState.modalIdx);
});
inner.querySelector('[data-action="ic-validate"]')?.addEventListener('click', async () => {
if (_issueState.validating) return;
const domain2 = ($val('ic-domain') || '').trim();
if (!domain2) { toast('Domain is required', 'error'); return; }
_issueState.validating = true;
_issueState.domain = domain2;
try {
const resp2 = await apiFetch('/api/certs/validate', {
method: 'POST',
body: { domain: domain2 },
});
if (!resp2.ok) { toast(resp2.error || 'Validation failed', 'error'); return; }
_showValidate(inner, domain2, resp2.data.checks, resp2.data.ready, state);
} finally {
_issueState.validating = false;
}
});
inner.querySelector('[data-action="ic-issue"]')?.addEventListener('click', async () => {
const body = { domain: _issueState.domain };
const issueResp = await apiFetch('/api/certs/issue/start', {
method: 'POST',
body,
});
if (issueResp.ok) {
toast('Issuance started for ' + _issueState.domain, 'success');
closeModal(_issueState.modalIdx);
const rid = issueResp.data?.request_id;
if (rid) pollCertIssue(rid, state);
} else {
toast(issueResp.error || 'Failed', 'error');
}
});
}
async function pollCertIssue(rid, state) {
poll({
url: '/api/certs/issue/' + enc(rid),
successKey: (d) => d.status === 'completed',
onErrorKey: (d) => d.status === 'failed',
onComplete: (d) => {
toast('Certificate issued for ' + (d.domain || rid), 'success');
modelFetch('acme');
},
onError: (d) => {
toast('Issuance failed: ' + (d?.error || 'unknown'), 'error');
},
});
}
export default definePage({
init() {
return {
acme: getModel('acme'),
};
},
render(state) {
const guard = renderGuard(state.acme, 'Certificates', 'ACME certificate management', state.acme.data);
if (guard) return guard;
const account = state.acme.data?.account || { registered: false, email: '', ca: '' };
const rows = (state.acme.data?.certs || []).map(c => {
const badge = certStatusBadge({ expired: c.expired, daysRemaining: c.days_remaining });
return h('tr', { key: c.domain },
h('td', null, h('strong', null, esc(c.domain || 'unknown'))),
h('td', { class: 'text-sm' }, esc(c.issuer || '-')),
h('td', null, esc(c.expiry || 'N/A')),
h('td', null, badge),
ActionCell({
editLabel: 'Renew',
editClick: async () => {
const resp = await apiFetch('/api/certs/' + enc(c.domain) + '/renew', { method: 'POST' });
if (resp.ok) toast('Renewal started for ' + c.domain, 'success');
else toast(resp.error || 'Failed', 'error');
},
removeUrl: '/api/certs/' + enc(c.domain),
removeMessage: 'Remove certificate for ' + c.domain + '?',
removeSuccess: 'Certificate removed',
removeRefresh: 'acme',
}),
);
});
return [
PageHeader({
title: 'Certificates',
subtitle: 'ACME certificate management',
actions: h('button', { class: 'btn btn-primary',
'on:click': () => issueCertModal(state) }, 'Issue Certificate'),
}),
_accountCard(account),
rows.length
? Table({
columns: ['Domain', 'Issuer', 'Expiry', 'Status', 'Actions'],
rows,
})
: Empty({ text: 'No certificates found. Issue a certificate to get started.' }),
];
},
});