summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--web/index.html513
-rw-r--r--web/js/main.js949
2 files changed, 782 insertions, 680 deletions
diff --git a/web/index.html b/web/index.html
index f36654f..dd60e7d 100644
--- a/web/index.html
+++ b/web/index.html
@@ -2,7 +2,7 @@
<html>
<head>
<script src="/js/main" type="module"></script>
- <style>
+ <style id="styles">
@keyframes fade-blink {
0% { opacity: 100%; }
100% { opacity: 0%; }
@@ -25,15 +25,6 @@
animation: fade-blink 1s ease-in-out infinite alternate;
}
- #settings-avatar-img {
- max-width: 200px;
- max-height: 200px;
- }
-
- [hidden] {
- display: none !important;
- }
-
input[type=email]:invalid {
background: #ff8080;
}
@@ -109,284 +100,302 @@
font-style: italic;
}
</style>
+
<template id="tpl-fenix-style">
<style>
body { font-size: 300%; }
input { font-size: 100%; }
</style>
</template>
- </head>
- <body>
- <noscript>this thing requires javascript!</noscript>
- <dialog id="message-modal">
- <div id="message-modal-content">
- <p id="message"></p>
- <a href="#" id="message-modal-close" hidden>close</a>
+ <template id="tpl-desktop-signedin">
+ <div class="container dialog">
+ successfully signed in!
</div>
- </dialog>
+ </template>
- <div id="desktop-signedin" class="container dialog" hidden>
- successfully signed in!
- </div>
-
- <div id="desktop-deleted" class="container dialog" hidden>
- account has been deleted.
- </div>
+ <template id="tpl-desktop-deleted">
+ <div class="container dialog">
+ account has been deleted.
+ </div>
+ </template>
- <div id="desktop-signup" class="container dialog" hidden>
- <form id="frm-signup">
- <label for="frm-signup-email">email</label>
- <input id="frm-signup-email" type="email" name="email" maxlength="256" value="">
- <label for="frm-signup-password">password</label>
- <input id="frm-signup-password" type="password" name="password">
- <input type="submit" name="submit" value="sign up">
+ <template id="btpl-credentials">
+ <form>
+ <label for="email">email</label>
+ <input id="email" type="email" name="email" maxlength="256">
+ <label for="password">password</label>
+ <input id="password" type="password" name="password" minlength="8" maxlength="256">
+ <input type="submit" name="submit">
</form>
- </div>
+ </template>
- <div id="desktop-signup-unverified" class="container dialog" hidden>
- <p>signup completed! please verify your account by clicking the
- link in the email you've just received.</p>
- <p>if you haven't received anything, go to Settings/Sync and resend
- the verification code.</p>
- </div>
+ <template id="tpl-desktop-signup">
+ <div class="container dialog">
+ <credentials-form submit-text="sign up"></credentials-form>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-signup-unverified">
+ <div class="container dialog">
+ <p>signup completed! please verify your account by clicking the
+ link in the email you've just received.</p>
+ <p>if you haven't received anything, go to Settings/Sync and resend
+ the verification code.</p>
+ </div>
+ </template>
- <div id="desktop-signin" class="container dialog" hidden>
- <form id="frm-signin">
- <label for="frm-signin-email">email</label>
- <input id="frm-signin-email" type="email" name="email" maxlength="256" value="">
- <label for="frm-signin-password">password</label>
- <input id="frm-signin-password" type="password" name="password">
- <input type="submit" name="submit" value="sign in">
- <hr>
+ <template id="tpl-desktop-signin">
+ <div class="container dialog">
+ <credentials-form submit-text="sign in"></credentials-form>
<div>
- <a href="#" class="_signup">sign up</a> |
- <a href="#" class="_reset">reset password</a>
+ <a href="#/signup" class="_signup">sign up</a> |
+ <a href="#/reset-password" class="_reset">reset password</a>
</div>
- </form>
- </div>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-signin-confirm">
+ <div class="container dialog">
+ <p>signin completed! please verify your session by copying the code
+ you've just received as an email into this box.</p>
+ <form>
+ <label for="code">code</label>
+ <input id="code" type="text" name="code" minlength="6" maxlength="6">
+ <input type="submit" name="submit" value="confirm signin">
+ <hr>
+ <a href="#" class="_resend">no email appeared? resend code</a>
+ </form>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-resetpw">
+ <div class="container dialog">
+ <form>
+ <label for="email">email</label>
+ <input id="email" type="email" name="email" maxlength="256">
+ <input type="submit" name="submit" value="send reset code">
+ </form>
+ </div>
+ </template>
- <div id="desktop-signin-confirm" class="container dialog" hidden>
- <p>signin completed! please verify your session by copying the code
- you've just received as an email into this box.</p>
- <form id="frm-signin-confirm">
- <label for="frm-signin-confirm-code">code</label>
- <input id="frm-signin-confirm-code" type="text" name="code" maxlength="6">
- <input type="submit" name="submit" value="confirm signin">
- <hr>
- <a href="#" class="_resend">no email appeared? resend code</a>
+ <template id="tpl-desktop-resetpw-newpw">
+ <div class="container dialog">
+ <form>
+ <table>
+ <tr>
+ <td><label for="new">new password</label></td>
+ <td><input id="new" type="password" name="new" minlength="8" maxlength="256"></td>
+ </tr>
+ <tr>
+ <td><label for="confirm">new password (confirm)</label></td>
+ <td><input id="confirm" type="password" name="new-confirm"
+ minlength="8" maxlength="256"></td>
+ </tr>
+ </table>
+ <div>
+ <input type="submit" name="submit">
+ </div>
+ </form>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-cwts">
+ <div class="container dialog">
+ <form id="frm">
+ <p>choose what to sync:</p>
+ <div class="cwts-container">
+ <div>
+ <input type="checkbox" id="addons" name="addons">
+ <label for="addons">add-ons</label>
+ </div>
+ <div>
+ <input type="checkbox" id="addresses" name="addresses">
+ <label for="addresses">addresses</label>
+ </div>
+ <div>
+ <input type="checkbox" id="bookmarks" name="bookmarks">
+ <label for="bookmarks">bookmarks</label>
+ </div>
+ <div>
+ <input type="checkbox" id="creditcards" name="creditcards">
+ <label for="creditcards">credit cards</label>
+ </div>
+ <div>
+ <input type="checkbox" id="history" name="history">
+ <label for="history">history</label>
+ </div>
+ <div>
+ <input type="checkbox" id="passwords" name="passwords">
+ <label for="passwords">passwords</label>
+ </div>
+ <!-- NOTE the spec says this key is named `preferences` -->
+ <div>
+ <input type="checkbox" id="prefs" name="prefs">
+ <label for="prefs">preferences</label>
+ </div>
+ <div>
+ <input type="checkbox" id="tabs" name="tabs">
+ <label for="tabs">open tabs</label>
+ </div>
+ </div>
+ <input type="submit" name="submit" value="start syncing">
+ </form>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-settings">
+ <div class="container">
+ <nav>
+ <a href="#/settings">settings</a> |
+ <a href="#/settings/change-password">change password</a> |
+ <a href="#/settings/destroy">delete account</a>
+ </nav>
+ <div class="settings-container tab"></div>
+ </div>
+ </template>
+
+ <template id="tpl-desktop-settings-main">
+ <style>
+ #avatar-img {
+ max-width: 200px;
+ max-height: 200px;
+ }
+ </style>
+ <form id="frm-avatar">
+ <img id="avatar-img">
+ <input type="file" accept="image/*" name="avatar">
+ <input type="submit" value="save">
</form>
- </div>
-
- <div id="desktop-resetpw" class="container dialog" hidden>
- <form id="frm-resetpw">
- <label for="frm-resetpw-email">email</label>
- <input id="frm-resetpw-email" type="email" name="email" maxlength="256" value="">
- <input type="submit" name="submit" value="send reset code">
+ <form id="frm-name">
+ <label for="name">user name:</label>
+ <input type="text" id="name" name="name" maxlength="256">
+ <input type="submit" value="save">
</form>
- </div>
-
- <div id="desktop-resetpw-newpw" class="container dialog" hidden>
- <form id="frm-resetpw-newpw">
+ <table id="clients">
+ <thead>
+ <tr>
+ <td>name</td>
+ <td>deviceType</td>
+ <td>createdTime</td>
+ <td>lastAccessTime</td>
+ <td>oauth?</td>
+ </tr>
+ </thead>
+ <tbody>
+ </tbody>
+ </table>
+ </template>
+
+ <template id="tpl-desktop-settings-destroy">
+ <p>deleting your account requires confirmation.</p>
+ <credentials-form submit-text="really delete accout"></credentials-form>
+ </template>
+
+ <template id="tpl-desktop-settings-chpw">
+ <form>
<table>
<tr>
- <td><label for="frm-resetpw-newpw-old">old password</label></td>
- <td><input id="frm-resetpw-newpw-old" type="password" name="old"></td>
+ <td><label for="old">old password</label></td>
+ <td><input id="old" type="password" name="old" minlength="8" maxlength="256"></td>
</tr>
<tr>
- <td><label for="frm-resetpw-newpw-new">new password</label></td>
- <td><input id="frm-resetpw-newpw-new" type="password" name="new"></td>
+ <td><label for="new">new password</label></td>
+ <td><input id="new" type="password" name="new" minlength="8" maxlength="256"></td>
</tr>
<tr>
- <td><label for="frm-resetpw-newpw-new-confirm">new password (confirm)</label></td>
- <td>
- <input id="frm-resetpw-newpw-new-confirm" type="password" name="new-confirm">
- </td>
+ <td><label for="confirm">new password (confirm)</label></td>
+ <td><input id="confirm" type="password" name="new-confirm"
+ minlength="8" maxlength="256"></td>
</tr>
</table>
<div>
- <input type="submit" name="submit" value="change password">
- </div>
- </form>
- </div>
-
- <div id="desktop-cwts" class="container dialog" hidden>
- <form id="frm-cwts">
- <p>choose what to sync:</p>
- <div class="cwts-container">
- <div>
- <input type="checkbox" id="frm-cwts-addons" name="addons">
- <label for="frm-cwts-addons">add-ons</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-addresses" name="addresses">
- <label for="frm-cwts-addresses">addresses</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-bookmarks" name="bookmarks">
- <label for="frm-cwts-bookmarks">bookmarks</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-creditcards" name="creditcards">
- <label for="frm-cwts-creditcards">credit cards</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-history" name="history">
- <label for="frm-cwts-history">history</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-passwords" name="passwords">
- <label for="frm-cwts-passwords">passwords</label>
- </div>
- <!-- NOTE the spec says this key is named `preferences` -->
- <div>
- <input type="checkbox" id="frm-cwts-prefs" name="prefs">
- <label for="frm-cwts-prefs">preferences</label>
- </div>
- <div>
- <input type="checkbox" id="frm-cwts-tabs" name="tabs">
- <label for="frm-cwts-tabs">open tabs</label>
- </div>
+ <input type="submit" name="submit">
</div>
- <input type="submit" name="submit" value="start syncing">
</form>
- </div>
-
- <div id="desktop-settings" class="container" hidden>
- <nav>
- <a href="#/settings">settings</a> |
- <a href="#/settings/change-password">change password</a> |
- <a href="#/settings/destroy">delete account</a>
- </nav>
- <div class="settings-container tab" id="desktop-settings-main" hidden>
- <form id="frm-settings-avatar">
- <img id="settings-avatar-img">
- <input type="file" accept="image/*" id="settings-avatar">
- <input type="submit" id="settings-avatar-save" value="save">
- </form>
- <form id="frm-settings-name">
- <label for="settings-name">user name:</label>
- <input type="text" id="settings-name" maxlength="256">
- <input type="submit" id="settings-name-save" value="save">
+ </template>
+
+ <template id="tpl-desktop-generate-invite">
+ <div class="container dialog">
+ <form>
+ <label for="ttl">invite valid for</label>
+ <select id="ttl" name="ttl" maxlength="256">
+ <option value="1">one hour</option>
+ <option value="24">one day</option>
+ <option value="168">one week</option>
+ </select>
+ <input type="submit" name="submit" value="generate invite">
</form>
- <table id="settings-clients">
- <thead>
- <tr>
- <td>name</td>
- <td>deviceType</td>
- <td>createdTime</td>
- <td>lastAccessTime</td>
- <td>oauth?</td>
- </tr>
- </thead>
- <tbody>
- </tbody>
- </table>
+ <div id="result" hidden>
+ invite link is <a id="link"></a>
+ </div>
</div>
- <div class="settings-container tab" id="desktop-settings-destroy" hidden>
- <p>deleting your account requires confirmation.</p>
- <form id="frm-settings-destroy">
- <label for="frm-settings-destroy-email">email</label>
- <input id="frm-settings-destroy-email" type="email"
- name="email" maxlength="256" value="">
- <label for="frm-settings-destroy-password">password</label>
- <input id="frm-settings-destroy-password" type="password" name="password">
- <input type="submit" name="submit" value="really delete account">
- </form>
+ </template>
+
+ <template id="tpl-fenix-signin-warning">
+ <div class="container">
+ <p>it looks like you're trying to sign in to sync from an android firefox instance.
+ we're very sorry, but this is going to hurt a bit.</p>
+ <p>to sign in you'll need to follow these steps:</p>
+ <ol>
+ <li>
+ enable USB debugging in the android:
+ <ol>
+ <li>go to setting → about phone</li>
+ <li>scroll to the build number</li>
+ <li>tap it until a "your are new a developer" message appears</li>
+ <li>go to settings → system → developer options</li>
+ <li>scroll down to "USB debugging", enable it</li>
+ </ol>
+ </li>
+ <li>enable USB debugging in the android firefox settings</li>
+ <li>connect your android device to a PC for USB debugging</li>
+ <li>on this PC, open firefox and go to <pre>about:debugging</pre></li>
+ <li>enable USB devices</li>
+ <li>connect to your android device</li>
+ <li>open this page in a normal tab, not through the sign-in interface.
+ this is very important, if there's no normal tab with this URL open
+ <em>signin will not work</em></li>
+ <li>open the login page through the sign-in interface as well</li>
+ <li>in the PC debugger, inspect the new tab that has just appeared</li>
+ <li><a href="#" id="signin">actually sign in</a>.
+ clicking this link will lead away from this page, finishing sign-in
+ will bring you back.</li>
+ <li>go to the javascript debug console of the tab we inspected previously,
+ copy the long block of code</li>
+ <li>locate the <pre>Firefox Accounts WebChannel</pre> extension in the
+ debugger and inspect it. this may fail, if it does go to the setup
+ tab of the debugger and disable USB devices, enable then, reconnect
+ to your devices, and repeat this step
+ <p><b>WARNING:</b> this is known to not work on firefox 102. if you get
+ a blank tab instead of a tab with debug tools as seen earlier you
+ may have to downgrade your android firefox to 101 to log in.
+ unfortunately this seems to require uninstalling and reinstalling
+ firefox, which wipes your data!</p>
+ </li>
+ <li>go to the javascript debug console, paste the block of code, and run it</li>
+ <li>enjoy sync!</li>
+ </ol>
</div>
- <div class="settings-container tab" id="desktop-settings-chpw" hidden>
- <form id="frm-settings-chpw">
- <table>
- <tr>
- <td><label for="frm-settings-chpw-old">old password</label></td>
- <td><input id="frm-settings-chpw-old" type="password" name="old"></td>
- </tr>
- <tr>
- <td><label for="frm-settings-chpw-new">new password</label></td>
- <td><input id="frm-settings-chpw-new" type="password" name="new"></td>
- </tr>
- <tr>
- <td><label for="frm-settings-chpw-new-confirm">new password (confirm)</label></td>
- <td>
- <input id="frm-settings-chpw-new-confirm" type="password" name="new-confirm">
- </td>
- </tr>
- </table>
- <div>
- <input type="submit" name="submit" value="change password">
- </div>
- </form>
- </div>
- </div>
+ </template>
- <div id="desktop-generate-invite" class="container dialog" hidden>
- <form id="frm-generate-invite">
- <label for="frm-generate-invite-email">invite valid for</label>
- <select id="frm-generate-invite-email" name="ttl" maxlength="256">
- <option value="1">one hour</option>
- <option value="24">one day</option>
- <option value="168">one week</option>
- </select>
- <input type="submit" name="submit" value="generate invite">
- </form>
- <div id="desktop-generate-invite-result" hidden>
- invite link is <a id="desktop-generate-invite-result-link"></a>
+ <template id="tpl-fenix-signin">
+ <div class="container dialog">
+ <credentials-form submit-text="do the fenix dance"></credentials-form>
</div>
- </div>
+ </template>
+ </head>
+ <body>
+ <noscript>this thing requires javascript!</noscript>
- <div id="fenix-signin-warning" class="container" hidden>
- <p>it looks like you're trying to sign in to sync from an android firefox instance.
- we're very sorry, but this is going to hurt a bit.</p>
- <p>to sign in you'll need to follow these steps:</p>
- <ol>
- <li>
- enable USB debugging in the android:
- <ol>
- <li>go to setting → about phone</li>
- <li>scroll to the build number</li>
- <li>tap it until a "your are new a developer" message appears</li>
- <li>go to settings → system → developer options</li>
- <li>scroll down to "USB debugging", enable it</li>
- </ol>
- </li>
- <li>enable USB debugging in the android firefox settings</li>
- <li>connect your android device to a PC for USB debugging</li>
- <li>on this PC, open firefox and go to <pre>about:debugging</pre></li>
- <li>enable USB devices</li>
- <li>connect to your android device</li>
- <li>open this page in a normal tab, not through the sign-in interface.
- this is very important, if there's no normal tab with this URL open
- <em>signin will not work</em></li>
- <li>open the login page through the sign-in interface as well</li>
- <li>in the PC debugger, inspect the new tab that has just appeared</li>
- <li><a href="#" id="fenix-signin-dialog-show">actually sign in</a>.
- clicking this link will lead away from this page, finishing sign-in
- will bring you back.</li>
- <li>go to the javascript debug console of the tab we inspected previously,
- copy the long block of code</li>
- <li>locate the <pre>Firefox Accounts WebChannel</pre> extension in the
- debugger and inspect it. this may fail, if it does go to the setup
- tab of the debugger and disable USB devices, enable then, reconnect
- to your devices, and repeat this step
- <p><b>WARNING:</b> this is known to not work on firefox 102. if you get
- a blank tab instead of a tab with debug tools as seen earlier you
- may have to downgrade your android firefox to 101 to log in.
- unfortunately this seems to require uninstalling and reinstalling
- firefox, which wipes your data!</p>
- </li>
- <li>go to the javascript debug console, paste the block of code, and run it</li>
- <li>enjoy sync!</li>
- </ol>
- </div>
+ <dialog id="message-modal">
+ <div id="message-modal-content">
+ <p id="message"></p>
+ <a href="#" id="message-modal-close" hidden>close</a>
+ </div>
+ </dialog>
- <div id="fenix-signin" class="container dialog" hidden>
- <form id="frm-fenix-signin">
- <label for="email">email</label>
- <input type="text" name="email" value="">
- <label for="password">password</label>
- <input type="password" name="password">
- <input type="submit" name="submit" value="do the fenix dance">
- </form>
+ <div id="content">
</div>
</body>
</html>
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());
}