summaryrefslogtreecommitdiff
path: root/web/js
diff options
context:
space:
mode:
authorpennae <github@quasiparticle.net>2022-07-23 21:22:45 +0200
committerpennae <github@quasiparticle.net>2022-07-23 23:54:19 +0200
commitdbfb5500fc22c3e773221ca398547bc5b3dd35d0 (patch)
tree36c0d489e2372d6dc078ceb32a6bb6eec2d51fdb /web/js
parent3b262297057a148d03d2a177908039f068a4b110 (diff)
downloadminor-skulk-dbfb5500fc22c3e773221ca398547bc5b3dd35d0.tar.gz
minor-skulk-dbfb5500fc22c3e773221ca398547bc5b3dd35d0.tar.xz
minor-skulk-dbfb5500fc22c3e773221ca398547bc5b3dd35d0.zip
rewrite the ui using web components
this doesn't do much for functionality, but it makes extending things easier. hopefully.
Diffstat (limited to 'web/js')
-rw-r--r--web/js/main.js949
1 files changed, 521 insertions, 428 deletions
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 = `<span class="disabled">current session</span>`;
- 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 = `<span class="disabled">current session</span>`;
+ 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());
}