'use strict';
// The page a setup link opens. It works without signing in: the token in
// the URL is the credential. The config exists only in this page's memory;
// leaving the page loses it, as the link works once.
(() => {
const app = document.getElementById('app');
const token = location.pathname.split('/').pop();
function h(tag, props, ...kids) {
const el = document.createElement(tag);
for (const [k, v] of Object.entries(props || {})) {
if (v == null || v === false) continue;
if (k === 'class') el.className = v;
else if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v);
else if (['value', 'hidden', 'htmlFor', 'disabled', 'src', 'alt', 'href', 'type', 'id', 'autocomplete', 'inputMode', 'maxLength', 'required'].includes(k)) el[k] = v;
else el.setAttribute(k, v === true ? '' : v);
}
for (const c of kids.flat(Infinity)) if (c != null && c !== false) el.append(c instanceof Node ? c : String(c));
return el;
}
// Same drawing as favicon.svg.
function logo(size, plain) {
const s = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
for (const [k, v] of Object.entries({ width: size, height: size, viewBox: '0 0 64 64', 'aria-hidden': 'true' })) s.setAttribute(k, v);
s.innerHTML = (plain ? '' : '') +
'' +
(plain ? '' : '') + '';
return s;
}
const fmtExpiry = (iso) => new Date(iso).toLocaleString(undefined, { weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' });
async function call(method, body) {
const opt = { method, headers: {} };
if (body) {
opt.headers['Content-Type'] = 'application/json';
opt.body = JSON.stringify(body);
}
const r = await fetch('/api/v1/setup/' + encodeURIComponent(token), opt);
let data = {};
try { data = await r.json(); } catch { /* empty */ }
return { status: r.status, data };
}
function show(...kids) { app.replaceChildren(h('div', { class: 'setupbox' }, kids)); }
function invalid() {
app.replaceChildren(h('div', { class: 'setupbox center' },
h('span', { class: 'ghost' }, logo(72, true)),
h('h1', null, 'This link isn\'t valid'),
h('p', null, 'It was already used, it expired, or it was revoked. Ask whoever sent it for a new one.'),
h('p', { class: 'loginfoot' }, 'GHOSTWIRE')));
}
function start(info) {
const err = h('p', { class: 'err-text', role: 'alert' });
const pin = info.pinRequired
? h('input', { id: 'pin', class: 'pin', inputMode: 'numeric', autocomplete: 'one-time-code', maxLength: 8, required: true })
: null;
const btn = h('button', { type: 'submit', class: 'btn primary' }, info.pinRequired ? 'Continue' : 'Get my VPN profile');
const form = h('form', { class: 'loginform', onSubmit: async (e) => {
e.preventDefault();
err.textContent = '';
btn.disabled = true;
try {
const { status, data } = await call('POST', { pin: pin ? pin.value.trim() : '' });
if (status === 200) return ready(data);
if (status === 404) return invalid();
err.textContent = data.error || 'Something went wrong. Try again.';
if (pin) pin.select();
} catch {
err.textContent = 'No connection to the server. Try again.';
}
btn.disabled = false;
} },
pin ? h('div', { class: 'field' }, h('label', { htmlFor: 'pin' }, 'PIN'), pin) : null,
err, btn);
app.replaceChildren(h('div', { class: 'setupbox' },
h('div', { class: 'brand stack' }, logo(56), h('div', { class: 'wm' }, 'GHOSTWIRE')),
h('div', { class: 'center' },
h('h1', null, 'Set up your VPN'),
h('p', null, 'This link adds the VPN profile ', h('strong', { class: 'mono' }, info.name), ' to your device.',
info.pinRequired ? ' Enter the PIN you were given.' : '')),
form,
h('p', { class: 'note' }, 'The link works once and expires ' + fmtExpiry(info.expires) + '.')));
(pin || btn).focus();
}
function ready(res) {
const file = res.name + '.conf';
const download = () => {
const url = URL.createObjectURL(new Blob([res.config], { type: 'application/octet-stream' }));
const a = h('a', { href: url, download: file });
document.body.append(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
};
const qr = h('img', { class: 'qr', src: res.qr, alt: 'QR code of the VPN profile ' + res.name, hidden: true });
const qrBtn = h('button', { type: 'button', class: 'btn', onClick: () => { qr.hidden = !qr.hidden; qrBtn.textContent = qr.hidden ? 'Show QR code' : 'Hide QR code'; } }, 'Show QR code');
const step = (n, title, ...body) => h('li', null, h('span', { class: 'num' }, String(n)), h('div', null, h('strong', null, title), body));
// Leaving the page loses the only copy of the private key.
window.addEventListener('beforeunload', (e) => e.preventDefault());
show(
h('div', { class: 'brand' }, logo(32), h('div', { class: 'wm' }, 'GHOSTWIRE')),
h('h1', null, 'Your VPN profile is ready'),
h('div', { class: 'notice' }, 'Save it now. This page can\'t be opened again: the private key exists only here and isn\'t stored anywhere.'),
h('ol', { class: 'steps' },
step(1, 'Install WireGuard',
h('p', null, h('a', { href: 'https://apps.apple.com/app/wireguard/id1441195209', rel: 'noopener' }, 'App Store'), ' · ',
h('a', { href: 'https://play.google.com/store/apps/details?id=com.wireguard.android', rel: 'noopener' }, 'Google Play'), ' · ',
h('a', { href: 'https://www.wireguard.com/install/', rel: 'noopener' }, 'Other systems'))),
step(2, 'Add the profile',
h('button', { type: 'button', class: 'btn primary', onClick: download }, 'Download ' + file),
h('p', null, 'Open the downloaded file with WireGuard, or in WireGuard tap + and choose “Create from file”.')),
step(3, 'Setting up another device?',
qrBtn, qr,
h('p', null, 'Opened this on a computer? Scan the QR code with WireGuard on your phone.'))));
}
(async () => {
try {
const { status, data } = await call('GET');
if (status === 200) start(data);
else invalid();
} catch {
show(h('h1', null, 'No connection'), h('p', null, 'The server can\'t be reached. Reload the page to try again.'));
}
})();
})();