112 lines
4 KiB
JavaScript
112 lines
4 KiB
JavaScript
|
|
// 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();
|