pithos/hosted/console/console.js

112 lines
4 KiB
JavaScript
Raw Permalink Normal View History

// The read-only account console (DESIGN-theseus-id.md §11.3). Theseus signs a
// Theseus ID sign-in for project "pithos" on this origin; the gateway answers
// with a "console" session that sees /hosted/v1/me and nothing else.
const $ = (id) => document.getElementById(id);
const KEY = 'pithos-console-token';
const store = {
get() { try { return sessionStorage.getItem(KEY); } catch { return null; } },
set(v) { try { if (v) sessionStorage.setItem(KEY, v); else sessionStorage.removeItem(KEY); } catch {} },
};
async function call(method, path, { body, token } = {}) {
const res = await fetch(path, {
method,
headers: { ...(body ? { 'content-type': 'application/json' } : {}), ...(token ? { authorization: `Bearer ${token}` } : {}) },
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw Object.assign(new Error(data.error || `HTTP ${res.status}`), { status: res.status });
return data;
}
function say(text, error = false) {
$('status').textContent = text || '';
$('status').className = error ? 'error' : '';
}
const bytes = (n) => {
if (!Number.isFinite(n)) return '—';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
return `${n.toFixed(i ? 1 : 0)} ${u[i]}`;
};
const GATES = { bns: 'a BNS name', deposit: 'a deposit', invite: 'an invite code' };
const STATES = { empty: 'Ready, no drives moved in yet', active: 'Running', 'moving-out': 'Moving out', released: 'Released' };
function show(me) {
const rows = [
['Account', me.id],
['Theseus ID', me.theseusId || '—'],
['State', STATES[me.state] || me.state],
['Opened', me.createdAt ? new Date(me.createdAt).toLocaleDateString() : '—'],
['Opened with', me.gate?.name ? `${GATES[me.gate.type] || me.gate.type} (${me.gate.name})` : (GATES[me.gate?.type] || '—')],
['Downloaded this month', me.downloadCapBytes ? `${bytes(me.usage?.down)} of ${bytes(me.downloadCapBytes)}` : bytes(me.usage?.down)],
['Uploaded this month', bytes(me.usage?.up)],
];
$('facts').replaceChildren(...rows.flatMap(([k, v]) => {
const dt = document.createElement('dt');
dt.textContent = k;
const dd = document.createElement('dd');
dd.textContent = v;
return [dt, dd];
}));
$('signed-out').hidden = true;
$('signed-in').hidden = false;
}
function signedOut() {
$('signed-in').hidden = true;
$('signed-out').hidden = false;
const inTheseus = !!window.theseusId;
$('signin').hidden = !inTheseus;
$('no-theseus').hidden = inTheseus;
}
async function refresh() {
const token = store.get();
if (!token) return signedOut();
try {
show(await call('GET', '/hosted/v1/me', { token }));
} catch (e) {
store.set(null);
signedOut();
if (e.status !== 401) say(e.message, true);
}
}
const THESEUS_ERRORS = {
denied: '',
locked: 'Unlock your Theseus Vault, then try again.',
'no-vault': 'Set up the Theseus Vault to get a Theseus ID.',
'origin-not-listed': 'Theseus does not list this page as part of Pithos, so it will not sign in here.',
};
$('signin').addEventListener('click', async () => {
$('signin').disabled = true;
say('Signing in…');
try {
const { projectId, nonce } = await call('GET', '/hosted/v1/theseus-id/challenge');
const signed = await window.theseusId.signIn({ projectId, nonce });
const s = await call('POST', '/hosted/v1/console/session', { body: { message: signed.message, signature: signed.signature, move: signed.move } });
if (signed.move && s.moved && window.theseusId.moved) await window.theseusId.moved({ projectId, account: signed.account }).catch(() => {});
store.set(s.token);
say('');
await refresh();
} catch (e) {
say(e.code in THESEUS_ERRORS ? THESEUS_ERRORS[e.code] : e.message, true);
} finally {
$('signin').disabled = false;
}
});
$('signout').addEventListener('click', async () => {
const token = store.get();
store.set(null);
if (token) await call('DELETE', '/hosted/v1/session', { token }).catch(() => {});
signedOut();
});
refresh();