119 lines
4.7 KiB
JavaScript
119 lines
4.7 KiB
JavaScript
// 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;
|
|
}
|