This commit is contained in:
root
2026-09-19 20:21:47 +02:00
commit 0798933b05
62 changed files with 7658 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
// Login and registration.
import { api, keyStore } from './api.js';
import { h, clear, download, toast } from './dom.js';
export function authView({ onLogin }) {
let mode = 'login';
const host = h('section', { class: 'auth' });
const brand = h('header', { class: 'brand' },
h('p', { class: 'kicker' }, 'HALCYON INSTRUMENT & CONTROL'),
h('h1', { class: 'logo' }, 'FLIGHT CONTROL'),
h('p', { class: 'tagline' }, 'BELT UPLINK TERMINAL // MODEL HC-33'),
);
const panel = h('div', { class: 'panel auth-panel' });
host.append(brand, panel);
function tabs() {
const tab = (id, label) => h('button', {
class: `tab ${mode === id ? 'on' : ''}`, type: 'button', role: 'tab',
'aria-selected': mode === id ? 'true' : 'false',
onclick: () => { mode = id; render(); },
}, label);
return h('div', { class: 'tabs auth-tabs', role: 'tablist' }, tab('login', 'LOGIN'), tab('register', 'NEW ACCOUNT'));
}
function render() {
clear(panel).append(tabs(), mode === 'login' ? loginForm() : registerForm());
panel.querySelector('input')?.focus();
}
function loginForm() {
const err = h('p', { class: 'form-error', role: 'alert' });
const key = h('input', { id: 'key', type: 'password', autocomplete: 'current-password', required: true, spellcheck: 'false' });
const show = h('input', { type: 'checkbox', id: 'show', onchange: (e) => { key.type = e.target.checked ? 'text' : 'password'; } });
const btn = h('button', { class: 'btn primary wide', type: 'submit' }, 'ENGAGE LINK');
return h('form', {
class: 'stack',
onsubmit: async (e) => {
e.preventDefault();
const k = key.value.trim();
if (!k) return;
btn.disabled = true;
err.textContent = '';
try {
await api.me(k);
keyStore.set(k);
onLogin();
} catch (ex) {
err.textContent = ex.status === 401 ? 'ACCESS DENIED: KEY NOT RECOGNISED' : `ERROR: ${ex.message}`;
btn.disabled = false;
}
},
},
h('label', { for: 'key' }, 'ACCESS KEY'),
key,
h('label', { class: 'check', for: 'show' }, show, 'SHOW KEY'),
err,
btn,
h('p', { class: 'hint' }, 'Your access key was shown once, when you registered. There is no password: the key is the login.'),
);
}
function registerForm() {
const err = h('p', { class: 'form-error', role: 'alert' });
const name = h('input', { id: 'name', type: 'text', maxlength: '40', required: true, autocomplete: 'username', spellcheck: 'false' });
const btn = h('button', { class: 'btn primary wide', type: 'submit' }, 'REGISTER');
return h('form', {
class: 'stack',
onsubmit: async (e) => {
e.preventDefault();
const callsign = name.value.trim();
if (!callsign) { err.textContent = 'ENTER A CALLSIGN'; return; }
btn.disabled = true;
err.textContent = '';
try {
const res = await api.register(callsign);
showKey(callsign, res);
} catch (ex) {
err.textContent = ex.status === 409 ? 'CALLSIGN ALREADY IN USE' : `ERROR: ${ex.message}`;
btn.disabled = false;
}
},
},
h('label', { for: 'name' }, 'CALLSIGN'),
name,
h('p', { class: 'hint' }, '1 to 40 characters. You will be issued one command ship and a secret access key.'),
err,
btn,
);
}
function showKey(callsign, res) {
const enter = h('button', { class: 'btn primary wide', type: 'button', disabled: true }, 'ENTER FLIGHT CONTROL');
const ack = h('input', { type: 'checkbox', id: 'ack', onchange: (e) => { enter.disabled = !e.target.checked; } });
enter.addEventListener('click', () => { keyStore.set(res.api_key); onLogin(); });
clear(panel).append(
h('div', { class: 'panel-title' }, 'ACCOUNT CREATED'),
h('div', { class: 'stack keyreveal' },
h('p', null, `WELCOME, ${callsign.toUpperCase()}. COMMAND SHIP #${res.ship_id} IS WAITING ON THE DOCK.`),
h('p', { class: 'warn' }, 'THIS IS YOUR ACCESS KEY. IT IS SHOWN ONCE AND CANNOT BE RECOVERED.'),
h('code', { class: 'keybox', tabindex: '0' }, res.api_key),
h('div', { class: 'row' },
h('button', { class: 'btn', type: 'button', onclick: async () => {
try { await navigator.clipboard.writeText(res.api_key); toast('KEY COPIED TO CLIPBOARD'); } catch { toast('COPY FAILED: SELECT THE KEY AND COPY IT BY HAND', 'err'); }
} }, 'COPY'),
h('button', { class: 'btn', type: 'button', onclick: () => download(`halcyon-key-${callsign}.txt`, `${res.api_key}\n`, 'text/plain') }, 'SAVE AS FILE'),
),
h('label', { class: 'check', for: 'ack' }, ack, 'I HAVE STORED MY KEY SAFELY'),
enter,
),
);
}
render();
return host;
}