Files
vacuum-wall/webui/static/pages/wireguard.js
T

123 lines
5.5 KiB
JavaScript

import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob } from '/static/hoover/index.js?v=8';
const addPeer = QuickModal({
title: 'Add WireGuard Peer',
fields: [
{ label: 'Name', id: 'wg-name', placeholder: 'client-name' },
{ label: 'Endpoint (optional)', id: 'wg-endpoint', placeholder: '1.2.3.4:51820' },
{ label: 'Allowed IPs (optional)', id: 'wg-allowed', placeholder: '10.0.0.2/32' },
{ label: 'Persistent Keepalive (optional)', id: 'wg-keepalive', type: 'number', placeholder: '25' },
],
submit: {
url: '/api/wireguard/peers',
body: () => ({
name: ($val('wg-name') || '').trim(),
endpoint: ($val('wg-endpoint') || '').trim() || undefined,
allowed_ips: ($val('wg-allowed') || '').trim() ? [($val('wg-allowed') || '').trim()] : [],
persistent_keepalive: $val('wg-keepalive') ? parseInt($val('wg-keepalive')) : undefined,
}),
validate: (b) => !b.name ? 'Name is required' : null,
successMsg: 'Peer added',
},
refresh: 'wireguard',
});
function downloadConfigModal(peerName, config, state) {
openModal((inner, idx) => {
formModal(inner, 'Download Config for ' + peerName,
[{ label: 'Server Endpoint', id: 'wg-srv-endpoint', placeholder: 'vpn.example.com:51820' }],
[
{ label: 'Cancel', cls: 'btn-outline', action: 'c', handler: () => closeModal(idx) },
{
label: 'Generate', cls: 'btn-primary', action: 's', handler: async () => {
const endpoint = ($val('wg-srv-endpoint') || '').trim();
if (!endpoint) { toast('Server endpoint is required', 'error'); return; }
const resp = await apiFetch('/api/wireguard/generate-client', {
method: 'POST',
body: { name: peerName, server_endpoint: endpoint },
});
if (resp.ok && resp.data?.config) {
downloadBlob(new Blob([resp.data.config], { type: 'text/plain' }), peerName + '.conf');
toast('Config downloaded', 'success');
closeModal(idx);
} else {
toast(resp.error || 'Failed', 'error');
}
},
},
],
);
});
}
export default definePage({
init() {
return {
wireguard: getModel('wireguard'),
};
},
render(state) {
const guard = renderGuard(state.wireguard, 'WireGuard', 'Tunnel & peer management', state.wireguard.data);
if (guard) return guard;
const st = state.wireguard.data?.status || {};
const isUp = st.up || false;
const listenPort = (state.wireguard.data?.config?.interface || {}).listen_port || '-';
const peerRows = (state.wireguard.data?.peers || []).map(p => {
const hasHandshake = !!p.latest_handshake;
return html`<tr key=${p.name}>
<td>
<${StatusDot} status=${hasHandshake ? 'success' : 'danger'} />
<strong>${esc(p.name || 'unnamed')}</strong>
</td>
<td><${MonoText} text=${p.public_key || 'N/A'} maxLength=20 /></td>
<td class="text-sm">${esc(p.allowed_ips || '-')}</td>
<td class="text-sm">${esc(p.endpoint || '-')}</td>
<td class="text-sm">${esc(p.latest_handshake || 'Never')}</td>
<td class="text-sm">
Recv: ${esc(p.transfer_recv || '0')}<br/>
Sent: ${esc(p.transfer_sent || '0')}
</td>
<${ActionCell}
editLabel="Config" editClick=${() => downloadConfigModal(p.name, state.wireguard.data?.config, state)}
removeUrl=${'/api/wireguard/peers/' + enc(p.name)}
removeMessage=${'Remove peer ' + p.name + '?'}
removeSuccess="Peer removed"
removeRefresh="wireguard" />
</tr>`;
});
const actions = ActionGroup(
html`<button class="btn btn-primary" onClick=${() => addPeer(state)}>Add Peer</button>`,
ActionButton({
url: '/api/wireguard/' + (isUp ? 'down' : 'up'),
labelOn: 'Stop', labelOff: 'Start', condition: isUp,
successMsg: 'Tunnel ' + (isUp ? 'stopped' : 'started'),
refresh: 'wireguard',
}),
ActionButton({
url: '/api/wireguard/apply',
successMsg: 'Config applied',
label: 'Apply',
refresh: 'wireguard',
}),
);
return [
PageHeader({
title: 'WireGuard',
subtitle: 'Tunnel: ' + (st.up ? 'up' : 'down') + ', Listen: ' + listenPort,
actions,
}),
ServiceStatus({ state: st.up ? 'up' : 'down' }),
peerRows.length
? Table({
columns: ['Peer', 'Public Key', 'Allowed IPs', 'Endpoint', 'Handshake', 'Transfer', 'Actions'],
rows: peerRows,
})
: Empty({ text: 'No peers configured. Add a peer above.' }),
];
},
});