From dbfb5500fc22c3e773221ca398547bc5b3dd35d0 Mon Sep 17 00:00:00 2001 From: pennae Date: Sat, 23 Jul 2022 21:22:45 +0200 Subject: rewrite the ui using web components this doesn't do much for functionality, but it makes extending things easier. hopefully. --- web/js/main.js | 949 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 521 insertions(+), 428 deletions(-) (limited to 'web/js/main.js') diff --git a/web/js/main.js b/web/js/main.js index 23610b6..cefb003 100644 --- a/web/js/main.js +++ b/web/js/main.js @@ -206,18 +206,8 @@ function wrapHandler(fn) { }; } -function switchTo(id, tabID) { - for (const screen of document.getElementsByClassName("container")) { - screen.hidden = true; - } - $(id).hidden = false; - - if (tabID) { - for (const tab of $(id).getElementsByClassName("tab")) { - tab.hidden = true; - } - $(tabID).hidden = false; - } +function switchTo(v) { + $("content").replaceChildren(v); } function dateDiffText(when) { @@ -243,520 +233,623 @@ function dateDiffText(when) { } ////////////////////////////////////////// -// initialization +// credentials form ////////////////////////////////////////// -let client_config; -var channel = new Channel(); -const isAndroid = /Android/.test(navigator.userAgent); - -document.body.onload = () => { - showMessage("Loading ...", "animate"); - - if (isAndroid) { - document.head.appendChild($("tpl-fenix-style").content); - } - - fetch("/.well-known/fxa-client-configuration") - .then(resp => { - if (!resp.ok) throw new Error(resp.statusText); - return resp.json(); - }) - .then(data => { - client_config = data; - hideMessage(); - return initUI(); - }) - .catch(e => { - showError("initialization failed: ", e); - }); -}; - -async function initUI() { - return isAndroid - ? await initUIAndroid() - : await initUIDesktop(); -} - -async function initUIDesktop() { - let present = wrapHandler(async () => { - if (window.location.hash.startsWith("#/verify/")) { - verify_init(JSON.parse(urlatob(window.location.hash.substr(9)))); - } else if (window.location.hash.startsWith("#/register/")) { - signup_init(window.location.hash.substr(11)); - } else { - let data = await channel.getStatus(); - if (!data.signedInUser) { - signin_init(); - } else if (window.location.hash == "#/settings") { - settings_init(data); - } else if (window.location.hash == "#/settings/change-password") { - settings_chpw_init(data); - } else if (window.location.hash == "#/settings/destroy") { - settings_destroy_init(data); - } else if (window.location.hash == "#/force_auth") { - signin_init(); - } else if (window.location.hash == "#/generate-invite") { - generate_invite_init(data); - } else { - switchTo("desktop-signedin"); - } - } - hideMessage(); - }); - - window.onpopstate = () => window.setTimeout(present, 0); - - for (let a of $("desktop-settings").querySelectorAll("nav a")) { - a.onclick = async ev => { +class CredentialsForm extends HTMLElement { + constructor() { + super(); + this.replaceChildren($("btpl-credentials").content.cloneNode(true)); + let frm = this.querySelector("form"); + frm['submit'].value = this.getAttribute('submit-text'); + frm.onsubmit = wrapHandler(async ev => { ev.preventDefault(); - window.location = ev.target.href; - await present(); - }; + this.dispatchEvent(new CustomEvent('confirm', { + detail: { + email: ev.target['email'].value, + password: ev.target['password'].value, + }, + })); + }); } - await present(); -} - -async function initUIAndroid() { - fenix_signin_init(); -} +}; +customElements.define('credentials-form', CredentialsForm); ////////////////////////////////////////// // signin form ////////////////////////////////////////// -function signin_init() { - switchTo("desktop-signin"); - let frm = $("frm-signin"); - frm.onsubmit = wrapHandler(signin_run); - frm.getElementsByClassName("_signup")[0].onclick = wrapHandler(signin_signup_instead); - frm.getElementsByClassName("_reset")[0].onclick = wrapHandler(ev => { - ev.preventDefault(); - password_reset_init(); - }); -} - -function signin_signup_instead(ev) { - ev.preventDefault(); - signup_init(); -} - -// NOTE it looks like firefox discards its session token before asking for reauth -// (eg if oauth verification of a sync token fails). we can't use the reauth -// endpoint for that, so we'll just always log in. -async function signin_run(ev) { - ev.preventDefault(); - let frm = ev.target; - - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); - } - - let c = new AuthClient(client_config.auth_server_base_url); - try { - let session = await c.signIn(frm["email"].value, frm["password"].value, { - keys: true, +class Signin extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-signin").content.cloneNode(true)); + let frm = shadow.querySelector("credentials-form"); + frm.addEventListener('confirm', wrapHandler(async ev => { + await this._signin(ev.detail.email, ev.detail.password); + })); + shadow.querySelector("a._signup").onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; + }); + shadow.querySelector("a._reset").onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; }); + } - signin_confirm_init(frm["email"].value, session); - } catch (e) { - if (e.errno == 104) { - showRecoverableError(` - account is not verified. please verify or wait a few minutes and register again. - `.trim()); - } else { - throw e; + // NOTE it looks like firefox discards its session token before asking for reauth + // (eg if oauth verification of a sync token fails). we can't use the reauth + // endpoint for that, so we'll just always log in. + async _signin(email, password) { + let c = new AuthClient(client_config.auth_server_base_url); + try { + let session = await c.signIn(email, password, { keys: true }); + switchTo(new SigninConfirm(email, session)); + } catch (e) { + if (e.errno == 104) { + showRecoverableError(` + account is not verified. please verify or wait a few minutes and register again. + `.trim()); + } else { + throw e; + } } } -} +}; +customElements.define('do-signin', Signin); ////////////////////////////////////////// // sign-in confirmation ////////////////////////////////////////// -function signin_confirm_init(email, session) { - let frm = $("frm-signin-confirm"); - frm.getElementsByClassName("_resend")[0].onclick = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - await c.sessionResendVerifyCode(session.sessionToken); - alert("code resent!"); - }); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - c.sessionVerifyCode(session.sessionToken, frm["code"].value) - .then(resp => { +class SigninConfirm extends HTMLElement { + constructor(email, session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-signin-confirm").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.querySelector("a._resend").onclick = wrapHandler(async ev => { + ev.preventDefault(); + let c = new AuthClient(client_config.auth_server_base_url); + await c.sessionResendVerifyCode(session.sessionToken); + alert("code resent!"); + }); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + let c = new AuthClient(client_config.auth_server_base_url); + try { + await c.sessionVerifyCode(session.sessionToken, frm["code"].value); channel.loadCredentials(email, session, { offered: [], declined: [] }); - switchTo("desktop-signedin"); - }) - .catch(e => { + switchTo(new SignedIn()); + } catch (e) { showError("verification failed: ", e); - }); - }); - switchTo("desktop-signin-confirm"); -} + } + }); + } +}; +customElements.define('do-signin-confirm', SigninConfirm); ////////////////////////////////////////// // password reset ////////////////////////////////////////// -function password_reset_init() { - let frm = $("frm-resetpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - - let c = new AuthClient(client_config.auth_server_base_url); - let token = await c.passwordForgotSendCode(frm["email"].value); - let code = prompt(` - please enter your password reset code from the email you've just received - `.trim()); - if (code === null) { - return showRecoverableError("password reset aborted"); - } - - let reset_token = await c.passwordForgotVerifyCode(code, token.passwordForgotToken); - password_reset_newpw(frm['email'].value, reset_token.accountResetToken); - }); - switchTo("desktop-resetpw"); -} - -function password_reset_newpw(email, reset_token) { - let frm = $("frm-resetpw-newpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); +class ResetPassword extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-resetpw").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); - let c = new AuthClient(client_config.auth_server_base_url); - if (frm['new'].value != frm['new-confirm'].value) { - return showRecoverableError("passwords don't match!"); - } + let c = new AuthClient(client_config.auth_server_base_url); + let token = await c.passwordForgotSendCode(frm["email"].value); + let code = prompt("please enter your password reset code from the email you've just received"); + if (code === null) { + return showRecoverableError("password reset aborted"); + } - await c.accountReset(email, frm['new'].value, reset_token); - switchTo("desktop-signin"); - }); - switchTo("desktop-resetpw-newpw"); -} + let reset_token = await c.passwordForgotVerifyCode(code, token.passwordForgotToken); + switchTo(new ResetPasswordCreate(frm['email'].value, reset_token.accountResetToken)); + }); + } +}; +customElements.define('do-resetpw', ResetPassword); + +class ResetPasswordCreate extends HTMLElement { + constructor(email, reset_token) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.append( + $("styles").cloneNode(true), + $("tpl-desktop-resetpw-newpw").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = async ev => { + ev.preventDefault(); + if (frm['new'].value != frm['new-confirm'].value) { + return showRecoverableError("passwords don't match!"); + } + let c = new AuthClient(client_config.auth_server_base_url); + await c.accountReset(email, frm['new'].value, reset_token); + switchTo(new Signin()); + }; + } +}; +customElements.define('do-resetpw-create', ResetPasswordCreate); ////////////////////////////////////////// // signup form ////////////////////////////////////////// -function signup_init(code) { - let frm = $("frm-signup"); - switchTo("desktop-signup"); - frm.onsubmit = wrapHandler(async ev => signup_run(ev, code)); -} - -async function signup_run(ev, code) { - ev.preventDefault(); - let frm = ev.target; - - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); +class Signup extends HTMLElement { + constructor(code) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-signup").content.cloneNode(true)); + let frm = shadow.querySelector("credentials-form"); + frm.addEventListener('confirm', async ev => { + await this._signup(ev.detail.email, ev.detail.password, code); + }) } - let c = new AuthClient(client_config.auth_server_base_url); - let session = await c.signUp(frm["email"].value, frm["password"].value, { - keys: true, - style: code, - }); - - cwts_continue(frm["email"].value, session); -} + async _signup(email, password, code) { + let c = new AuthClient(client_config.auth_server_base_url); + let session = await c.signUp(email, password, { + keys: true, + style: code, + }); -////////////////////////////////////////// -// choose-what-to-sync form -////////////////////////////////////////// + switchTo(new CWTS(email, session)); + } +}; +customElements.define('do-signup', Signup); + +class CWTS extends HTMLElement { + constructor(email, session) { + super(); + // TODO we don't query browser capabilities, but we probably should + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-cwts").content.cloneNode(true)); + let frm = shadow.querySelector("form"); + frm.onsubmit = wrapHandler((ev) => { + ev.preventDefault(); -function cwts_continue(email, session) { - // TODO we don't query browser capabilities, but we probably should - let frm = $("frm-cwts"); - frm.onsubmit = wrapHandler((ev) => { - ev.preventDefault(); + let offered = [], declined = []; + for (const engine of frm.querySelectorAll("input[type=checkbox]")) { + if (!engine.checked) declined.push(engine.name); + else offered.push(engine.name); + } + channel.loadCredentials(email, session, { offered, declined }); - let offered = [], declined = []; - for (const engine of frm.querySelectorAll("input[type=checkbox]")) { - if (!engine.checked) declined.push(engine.name); - else offered.push(engine.name); - } - channel.loadCredentials(email, session, { offered, declined }); + switchTo(new SignupUnverified()); + }); + } +}; +customElements.define('do-cwts', CWTS); - switchTo("desktop-signup-unverified"); - }); - switchTo("desktop-cwts"); +class SignupUnverified extends HTMLElement { + constructor() { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-signup-unverified").content.cloneNode(true)); + } } +customElements.define('do-signup-unverified', SignupUnverified); ////////////////////////////////////////// // verification ////////////////////////////////////////// -function verify_init(context) { +class SignedIn extends HTMLElement { + constructor() { + super(); + this.replaceChildren($("tpl-desktop-signedin").content.cloneNode(true)); + } +}; +customElements.define('signed-in', SignedIn); + +async function verify(context) { let c = new AuthClient(client_config.auth_server_base_url); - c.verifyCode(context.uid, context.code) - .then(resp => { - switchTo("desktop-signedin"); - }) - .catch(e => { - showError("verification failed: ", e); - }); + try { + await c.verifyCode(context.uid, context.code) + switchTo(new SignedIn()); + } catch(e) { + showError("verification failed: ", e); + } } ////////////////////////////////////////// -// settings root +// settings ////////////////////////////////////////// -function settings_init(session) { - switchTo("desktop-settings", "desktop-settings-main"); +class Settings extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + this.session = session; + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-settings").content.cloneNode(true)); + + for (let a of shadow.querySelectorAll("nav a")) { + a.onclick = wrapHandler(async ev => { + ev.preventDefault(); + window.location = ev.target.href; + await this._display(); + }); + } + + this._display(); + } + + async _display() { + let tab = SettingsMain; + if (window.location.hash == "#/settings/change-password") { + tab = SettingsChangePassword; + } else if (window.location.hash == "#/settings/destroy") { + tab = SettingsDestroy; + } + + try { + this.shadowRoot.querySelector(".tab").replaceChildren(new tab(this.session)); + } catch(e) { + showError("initialization failed: ", e); + } + } +}; +customElements.define('do-settings', Settings); + +class SettingsMain extends HTMLElement { + constructor(session) { + super(); + this.session = session; + this.replaceChildren($("tpl-desktop-settings-main").content.cloneNode(true)); + this._display(); + } - let inner = async () => { + async _display() { showMessage("Loading ...", "animate"); let ac = new AuthClient(client_config.auth_server_base_url) - let pc = new ProfileClient(ac, session); + let pc = new ProfileClient(ac, this.session); let initProfile = async () => { let profile = await pc.getProfile(); - $("settings-name").value = profile.displayName || ""; - $("frm-settings-name").onsubmit = wrapHandler(async ev => { - showMessage("Applying ...") + this.querySelector("#name").value = profile.displayName || ""; + this.querySelector("#frm-name").onsubmit = wrapHandler(async ev => { ev.preventDefault(); - await pc.setDisplayName($("settings-name").value); + showMessage("Applying ...") + await pc.setDisplayName(ev.target['name'].value); hideMessage(); }); - $("settings-avatar-img").src = profile.avatar; - $("frm-settings-avatar").onsubmit = wrapHandler(async ev => { - showMessage("Saving ...") + this.querySelector("#avatar-img").src = profile.avatar; + this.querySelector("#frm-avatar").onsubmit = wrapHandler(async ev => { ev.preventDefault(); - await pc.setAvatar($("settings-avatar").files[0]); - settings_init(session); + showMessage("Saving ...") + await pc.setAvatar(ev.target['avatar'].files[0]); + await this._display(); }); }; await Promise.all([ initProfile(), - settings_populateClients(ac, session), + this._populateClients(ac), ]); hideMessage(); - }; - - inner().catch(e => { - showError("initialization failed: ", e); - }); -} + } -async function settings_populateClients(authClient, session) { - let clients = await authClient.attachedClients(session.signedInUser.sessionToken); - - let body = $("settings-clients").getElementsByTagName("tbody")[0]; - body.innerHTML = ""; - for (const c of clients) { - let row = document.createElement("tr"); - let add = (val, tip) => { - let cell = document.createElement("td"); - cell.innerText = val || ""; - if (tip) cell.title = tip; - row.appendChild(cell); - }; - let addDateDiff = val => { - let text = dateDiffText(new Date(val)); - add(text, new Date(val)); - }; - add(c.name); - add(c.deviceType); - addDateDiff(c.createdTime * 1000); - addDateDiff(c.lastAccessTime * 1000); - add(c.scope ? "yes" : "", (c.scope ? c.scope : "").replace(/ +/g, "\n")); - if (c.isCurrentSession) { - let cell = document.createElement("td"); - cell.innerHTML = `current session`; - row.appendChild(cell); - } else if (c.deviceId || c.sessionTokenId || c.refreshTokenId) { - let remove = document.createElement("button"); - remove.innerText = 'remove'; - remove.onclick = wrapHandler(async ev => { - ev.preventDefault(); - let info = { clientId: c.clientId }; - if (c.deviceId) - info.deviceId = c.deviceId; - else if (c.sessionTokenId) - info.sessionTokenId = c.sessionTokenId; - else if (c.refreshTokenId) - info.refreshTokenId = c.refreshTokenId; - showMessage("Processing ...", "animate"); - await authClient.attachedClientDestroy(session.signedInUser.sessionToken, info); - await settings_populateClients(authClient, session); - hideMessage(); - }); - row.appendChild(remove); + async _populateClients(authClient) { + let clients = await authClient.attachedClients(this.session.signedInUser.sessionToken); + + let body = this.querySelector("#clients tbody"); + body.innerHTML = ""; + for (const c of clients) { + let row = document.createElement("tr"); + let add = (val, tip) => { + let cell = document.createElement("td"); + cell.innerText = val || ""; + if (tip) cell.title = tip; + row.appendChild(cell); + }; + let addDateDiff = val => { + let text = dateDiffText(new Date(val)); + add(text, new Date(val)); + }; + add(c.name); + add(c.deviceType); + addDateDiff(c.createdTime * 1000); + addDateDiff(c.lastAccessTime * 1000); + add(c.scope ? "yes" : "", (c.scope ? c.scope : "").replace(/ +/g, "\n")); + if (c.isCurrentSession) { + let cell = document.createElement("td"); + cell.innerHTML = `current session`; + row.appendChild(cell); + } else if (c.deviceId || c.sessionTokenId || c.refreshTokenId) { + let remove = document.createElement("button"); + remove.innerText = 'remove'; + remove.onclick = wrapHandler(async ev => { + ev.preventDefault(); + let info = { clientId: c.clientId }; + if (c.deviceId) + info.deviceId = c.deviceId; + else if (c.sessionTokenId) + info.sessionTokenId = c.sessionTokenId; + else if (c.refreshTokenId) + info.refreshTokenId = c.refreshTokenId; + showMessage("Processing ...", "animate"); + await authClient.attachedClientDestroy(this.session.signedInUser.sessionToken, info); + await this._populateClients(authClient, this.session); + hideMessage(); + }); + row.appendChild(remove); + } + body.appendChild(row); } - body.appendChild(row); } -} +}; +customElements.define('do-settings-main', SettingsMain); -////////////////////////////////////////// -// settings change password -////////////////////////////////////////// +class SettingsChangePassword extends HTMLElement { + constructor(session) { + super(); + this.replaceChildren($("tpl-desktop-settings-chpw").content.cloneNode(true)); -function settings_chpw_init(session) { - switchTo("desktop-settings", "desktop-settings-chpw"); - let frm = $("frm-settings-chpw"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - let frm = ev.target; + let frm = this.querySelector("form"); + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + if (frm['new'].value != frm['new-confirm'].value) { + return showRecoverableError("passwords don't match!"); + } - if (frm["new"].value != frm["new-confirm"].value) { - showRecoverableError("passwords don't match"); - return; - } + let c = new AuthClient(client_config.auth_server_base_url); + await c.passwordChange(session.signedInUser.email, frm['old'].value, frm['new'].value, {}); - let c = new AuthClient(client_config.auth_server_base_url); - let resp = await c.passwordChange(session.signedInUser.email, frm['old'].value, frm['new'].value, {}); + channel.passwordChanged(session.signedInUser.email, session.signedInUser.uid); + alert("password changed"); + }); + } +}; +customElements.define('do-settings-chpw', SettingsChangePassword); - channel.passwordChanged(session.signedInUser.email, session.signedInUser.uid); - alert("password changed"); - }); -} +class SettingsDestroy extends HTMLElement { + constructor(session) { + super(); + this.replaceChildren($("tpl-desktop-settings-destroy").content.cloneNode(true)); -////////////////////////////////////////// -// settings destroy -////////////////////////////////////////// + this.querySelector("credentials-form").addEventListener('confirm', wrapHandler(async ev => { + let c = new AuthClient(client_config.auth_server_base_url); + await c.accountDestroy(ev.detail.email, ev.detail.password); -function settings_destroy_init(session) { - switchTo("desktop-settings", "desktop-settings-destroy"); - let frm = $("frm-settings-destroy"); - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); - let frm = ev.target; + channel.accountDestroyed(ev.detail.email, session.signedInUser.uid); + switchTo(new AccountDestroyed()); + })); + } +}; +customElements.define('do-settings-destroy', SettingsDestroy); - if (frm[0].value == "" || !frm[0].validity.valid) { - return showRecoverableError("email is not valid"); - } - if (frm[1].value == "" || !frm[1].validity.valid) { - return showRecoverableError("password is not valid"); - } +class AccountDestroyed extends HTMLElement { + constructor() { + super(); + this.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-deleted").content.cloneNode(true)); + } +}; +customElements.define('account-destroyed', AccountDestroyed); - let c = new AuthClient(client_config.auth_server_base_url); - await c.accountDestroy(frm["email"].value, frm["password"].value); +////////////////////////////////////////// +// generate invite +////////////////////////////////////////// - channel.accountDestroyed(frm["email"], session.signedInUser.uid); - switchTo("desktop-deleted"); - }); -} +class GenerateInvite extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-desktop-generate-invite").content.cloneNode(true)); + + let frm = shadow.querySelector("form"); + shadow.querySelector("#result").hidden = true; + frm.onsubmit = wrapHandler(async ev => { + ev.preventDefault(); + + let server_url = new URL(client_config.auth_server_base_url); + server_url.pathname = server_url.pathname.split("/").slice(0, -1).join("/") + "/_invite"; + let c = new AuthClient(server_url.toString()); + let resp = await c.sessionPost( + "/generate", + session.signedInUser.sessionToken, + { 'ttl_hours': parseInt(frm["ttl"].value) }); + + shadow.querySelector("#link").href = resp.url; + shadow.querySelector("#link").innerText = resp.url; + shadow.querySelector("#result").hidden = false; + }); + } +}; +customElements.define('do-generate-invite', GenerateInvite); ////////////////////////////////////////// -// generate invite +// fenix signin ////////////////////////////////////////// -function generate_invite_init(session) { - let frm = $("frm-generate-invite"); - $("desktop-generate-invite-result").hidden = true; - frm.onsubmit = wrapHandler(async ev => { - ev.preventDefault(); +class FenixSignin extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-fenix-signin-warning").content.cloneNode(true)); + shadow.querySelector("#signin").onclick = wrapHandler(async ev => { + ev.preventDefault(); + switchTo(new FenixSigninEnter()); + }); + } +} +customElements.define('do-fenix-signin', FenixSignin); + +class FenixSigninEnter extends HTMLElement { + constructor(session) { + super(); + const shadow = this.attachShadow({mode: 'open'}); + shadow.replaceChildren( + $("styles").cloneNode(true), + $("tpl-fenix-signin").content.cloneNode(true)); + shadow.querySelector("credentials-form").addEventListener('confirm', wrapHandler(async ev => { + ev.preventDefault(); + await this._step2(ev.detail.email, ev.detail.password); + })); + } - let server_url = new URL(client_config.auth_server_base_url); - server_url.pathname = server_url.pathname.split("/").slice(0, -1).join("/") + "/_invite"; - let c = new AuthClient(server_url.toString()); - let resp = await c.sessionPost( - "/generate", - session.signedInUser.sessionToken, - { 'ttl_hours': parseInt(frm["ttl"].value) }); + async _step2(email, password) { + let url = new URL(window.location); + let params = new URLSearchParams(url.search); + let param = (p) => { + let val = params.get(p); + if (val === undefined) throw `missing parameter ${p}`; + return val; + }; - $("desktop-generate-invite-result-link").href = resp.url; - $("desktop-generate-invite-result-link").innerText = resp.url; - $("desktop-generate-invite-result").hidden = false; + let c = new AuthClient(client_config.auth_server_base_url); + let session = await c.signIn(email, password, { keys: true }); + let verifyCode = prompt("enter verify code"); + await c.sessionVerifyCode(session.sessionToken, verifyCode); + try { + let keys = await c.accountKeys(session.keyFetchToken, session.unwrapBKey); + let scoped_keys = await c.getOAuthScopedKeyData( + session.sessionToken, + param("client_id"), + param("scope")); + for (var scope in scoped_keys) { + scoped_keys[scope] = await deriveScopedKey( + keys.kB, + session.uid, + scope, + scoped_keys[scope].keyRotationSecret, + scoped_keys[scope].keyRotationTimestamp); + } - }); - switchTo("desktop-generate-invite"); + let keys_jwe = await encryptScopedKeys(scoped_keys, param("keys_jwk")); + + let code = await c.createOAuthCode( + session.sessionToken, + param("client_id"), + param("state"), + { + access_type: param("access_type"), + keys_jwe, + response_type: param("response_type"), + scope: param("scope"), + code_challenge_method: param("code_challenge_method"), + code_challenge: param("code_challenge"), + }); + + console.log(`browser.tabs.executeScript({code: \` + port = browser.runtime.connectNative("mozacWebchannel"); + port.postMessage({ + id: "account_updates", + message: { + command: "fxaccounts:oauth_login", + data: { + "code": "${code.code}", + "state": "${code.state}", + "redirect": "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", + "action": "signin" + }, + messageId: 1, + }}); + \`});`); + + switchTo(new FenixSignin()); + } finally { + c.sessionDestroy(session.sessionToken); + } + } } +customElements.define('do-fenix-signin-enter', FenixSigninEnter); ////////////////////////////////////////// -// fenix signin +// initialization ////////////////////////////////////////// -function fenix_signin_init() { - switchTo("fenix-signin-warning"); - $("fenix-signin-dialog-show").onclick = wrapHandler(async ev => { - ev.preventDefault(); - switchTo("fenix-signin"); - $("frm-fenix-signin").onsubmit = wrapHandler(fenix_signin_step2); - }); -} +let client_config; +var channel = new Channel(); +const isAndroid = /Android/.test(navigator.userAgent); + +document.body.onload = async () => { + showMessage("Loading ...", "animate"); -async function fenix_signin_step2(ev) { - ev.preventDefault(); + if (isAndroid) { + document.head.appendChild($("tpl-fenix-style").content); + } - let url = new URL(window.location); - let params = new URLSearchParams(url.search); - let param = (p) => { - let val = params.get(p); - if (val === undefined) throw `missing parameter ${p}`; - return val; - }; + try { + let resp = await fetch("/.well-known/fxa-client-configuration"); + if (!resp.ok) throw new Error(resp.statusText); + client_config = await resp.json(); + hideMessage(); + initUI(); + } catch(e) { + showError("initialization failed: ", e); + } +}; - let frm = ev.target; - let c = new AuthClient(client_config.auth_server_base_url); - let session = await c.signIn(frm["email"].value, frm["password"].value, { - keys: true, - }); - let verifyCode = prompt("enter verify code"); - await c.sessionVerifyCode(session.sessionToken, verifyCode); - try { - let keys = await c.accountKeys(session.keyFetchToken, session.unwrapBKey); - let scoped_keys = await c.getOAuthScopedKeyData( - session.sessionToken, - param("client_id"), - param("scope")); - for (var scope in scoped_keys) { - scoped_keys[scope] = await deriveScopedKey( - keys.kB, - session.uid, - scope, - scoped_keys[scope].keyRotationSecret, - scoped_keys[scope].keyRotationTimestamp); +async function initUI() { + if (isAndroid) { + await initUIAndroid(); + } else { + await initUIDesktop(); + } +} + +async function initUIDesktop() { + let present = wrapHandler(async () => { + if (window.location.hash.startsWith("#/verify/")) { + await verify(JSON.parse(urlatob(window.location.hash.substr(9)))); + } else if (window.location.hash.startsWith("#/register/")) { + switchTo(new Signup(window.location.hash.substr(11))); + } else { + let data = await channel.getStatus(); + if (window.location.hash == "#/signup") { + switchTo(new Signup()); + } else if (window.location.hash == "#/reset-password") { + switchTo(new ResetPassword()); + } else if (!data.signedInUser + || window.location.hash == "#/force_auth") { + switchTo(new Signin()); + } else if (window.location.hash == "#/settings" + || window.location.hash == "#/settings/change-password" + || window.location.hash == "#/settings/destroy") { + switchTo(new Settings(data)); + } else if (window.location.hash == "#/generate-invite") { + switchTo(new GenerateInvite(data)); + } else { + switchTo(new SignedIn()); + } } + hideMessage(); + }); - let keys_jwe = await encryptScopedKeys(scoped_keys, param("keys_jwk")); - - let code = await c.createOAuthCode( - session.sessionToken, - param("client_id"), - param("state"), - { - access_type: param("access_type"), - keys_jwe, - response_type: param("response_type"), - scope: param("scope"), - code_challenge_method: param("code_challenge_method"), - code_challenge: param("code_challenge"), - }); + window.onpopstate = () => window.setTimeout(present, 0); + window.onhashchange = () => window.setTimeout(present, 0); + await present(); +} - console.log(`browser.tabs.executeScript({code: \` - port = browser.runtime.connectNative("mozacWebchannel"); - port.postMessage({ - id: "account_updates", - message: { - command: "fxaccounts:oauth_login", - data: { - "code": "${code.code}", - "state": "${code.state}", - "redirect": "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel", - "action": "signin" - }, - messageId: 1, - }}); - \`});`); - - switchTo("fenix-signin-warning"); - } finally { - c.sessionDestroy(session.sessionToken); - } +async function initUIAndroid() { + switchTo(new FenixSignin()); } -- cgit v1.2.3