summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgumi <git@gumi.ca>2019-06-29 11:21:40 -0400
committergumi <git@gumi.ca>2019-06-29 11:21:40 -0400
commitc650856bddeac08ce4bf4f1cbe9f67ab0ed91890 (patch)
tree3558031f9c7810fa9d4769eef1bc2f142a4e6f51
parent7f3f119aefedce748de125124999e8106f600e92 (diff)
downloadlanding-c650856bddeac08ce4bf4f1cbe9f67ab0ed91890.tar.gz
landing-c650856bddeac08ce4bf4f1cbe9f67ab0ed91890.tar.bz2
landing-c650856bddeac08ce4bf4f1cbe9f67ab0ed91890.tar.xz
landing-c650856bddeac08ce4bf4f1cbe9f67ab0ed91890.zip
rework the password reset process: api v2.2
-rw-r--r--src/reset.html224
-rw-r--r--src/reset_password.html356
-rw-r--r--src/reset_password_init.html306
-rw-r--r--src/reset_user.html302
4 files changed, 997 insertions, 191 deletions
diff --git a/src/reset.html b/src/reset.html
index 8e334e0..5200b93 100644
--- a/src/reset.html
+++ b/src/reset.html
@@ -24,7 +24,7 @@
font-size: 12px;
line-height: 30px;
color: #777;
- background: #4CAF50;
+ background: rgba(0, 0, 0, 0.6);
}
.container {
@@ -34,11 +34,14 @@
position: relative;
}
- .container > form input[type="text"],
- .container > form input[type="password"],
- .container > form input[type="email"],
- .container > form button {
+ .container > form .button {
font: 400 12px/16px "Roboto", Helvetica, Arial, sans-serif;
+ -webkit-appearance: button;
+ -moz-appearance: button;
+ appearance: button;
+ display: block;
+ text-align: center;
+ text-decoration: none;
}
.container > form {
@@ -62,14 +65,6 @@
font-weight: 400;
}
- .container .status {
- margin: 15px 0 15px;
- display: block;
- font-size: 13px;
- font-weight: 400;
- display: none;
- }
-
fieldset {
border: medium none !important;
margin: 0 0 10px;
@@ -78,44 +73,25 @@
width: 100%;
}
- .container > form input[type="text"],
- .container > form input[type="password"],
- .container > form input[type="email"] {
- width: 100%;
- border: 1px solid #ccc;
- background: #FFF;
- margin: 0 0 5px;
- padding: 10px;
- }
-
- .container > form input[type="text"]:hover,
- .container > form input[type="password"]:hover,
- .container > form input[type="email"]:hover {
- -webkit-transition: border-color 0.3s ease-in-out;
- -moz-transition: border-color 0.3s ease-in-out;
- transition: border-color 0.3s ease-in-out;
- border: 1px solid #aaa;
- }
-
- .container > form button {
+ .container > form .button {
cursor: pointer;
width: 100%;
border: none;
- background: #4CAF50;
+ background: rgba(0, 0, 0, 0.6);
color: #FFF;
margin: 0 0 5px;
padding: 10px;
font-size: 15px;
}
- .container > form button:hover {
- background: #43A047;
+ .container > form .button:hover {
+ background: rgba(0, 0, 0, 0.9);
-webkit-transition: background 0.3s ease-in-out;
-moz-transition: background 0.3s ease-in-out;
transition: background-color 0.3s ease-in-out;
}
- .container > form button:active {
+ .container > form .button:active {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
}
@@ -124,20 +100,6 @@
border: 1px solid #aaa;
}
- .grecaptcha-badge {
- margin-bottom: 2em;
- margin-top: 2em;
- }
-
- .container > form.error .status {
- display: block!important;
- }
-
- input.invalid {
- border-color: #900!important;
- background-color: #FDD!important;
- }
-
.warning {
color: #fff;
margin: 1em;
@@ -146,30 +108,7 @@
padding: 1em;
border: 3px yellow dashed;
}
-
- ::-webkit-input-placeholder {
- color: #888;
- }
-
- :-moz-placeholder {
- color: #888;
- }
-
- ::-moz-placeholder {
- color: #888;
- }
-
- :-ms-input-placeholder {
- color: #888;
- }
</style>
- <script type="text/javascript">
- /* this block is intentionally left in <head>, for legacy browsers */
- if (!("fetch" in window && "Request" in window && "setCustomValidity" in document.createElement("input"))) {
- document.body.innerHTML = "<h1 style=\"color:#fff;margin:1em\">Please update your browser to continue.</h1>";
- throw new Error("outdated browser");
- }
- </script>
</head>
<body>
<noscript>
@@ -181,133 +120,36 @@
</noscript>
<div class="container">
- <form id="pwreset" action="#" method="POST">
+ <form>
<h3>The Mana World</h3>
- <h4>Password reset</h4>
- <span class="status">...</span>
+ <h4>Account recovery: please select</h4>
+ <br>
+ <fieldset>
+ <div>
+ <a class="button" href="/recover/password">I forgot my password</a>
+ </div>
+ </fieldset>
<fieldset>
- <input name="username" placeholder="Username" type="text" tabindex="1" minlength="4" maxlength="23" pattern="^[a-zA-Z0-9]{4,23}$" title="4-23 characters, alphanumeric" required autofocus>
+ <div>
+ <a class="button" href="/recover/username">I forgot my user name</a>
+ </div>
</fieldset>
<fieldset>
- <input name="email" placeholder="Email Address (optional)" type="email" maxlength="39" tabindex="3">
+ <div>
+ <a class="button" href="https://forums.themanaworld.org/viewtopic.php?f=20&t=7559">My account is banned</a>
+ </div>
</fieldset>
<fieldset>
<div>
- <button
- class="g-recaptcha"
- data-sitekey="6LdaVUcUAAAAAJ-7cORTu4cZCPSNjDqjz3y4nLVR"
- data-callback="onSubmit"
- data-badge="inline"
- data-submit="...Sending"
- type="submit">Submit</button>
+ <a class="button" href="https://forums.themanaworld.org/viewtopic.php?f=20&t=6472">My account was compromised</a>
</div>
</fieldset>
+ <fieldset>
+ <div>
+ <a class="button" href="https://forums.themanaworld.org/viewforum.php?f=3">I need help with something else</a>
+ </div>
+ </fieldset>
</form>
</div>
- <script>
- const nodes = {
- uname: document.querySelector("input[name=username]"),
- pwd: document.querySelector("input[name=password]"),
- email: document.querySelector("input[name=email]"),
- code: document.querySelector("input[name=code]"),
- form: document.querySelector(".container > form"),
- status: document.querySelector(".status"),
- captcha: undefined
- };
-
- const validateInput = node => {
- node.setCustomValidity("");
- if (node.checkValidity() === false) {
- node.classList.add("invalid");
- node.setCustomValidity(node.title);
- node.focus();
- return false;
- } else {
- node.classList.remove("invalid");
- return true;
- }
- };
-
- nodes.form.addEventListener("input", event => {
- validateInput(event.target);
- });
-
- function onSubmit(token) {
- if (nodes.form.checkValidity() === false) {
- validateInput(nodes.uname) &&
- validateInput(nodes.pwd) &&
- validateInput(nodes.email);
- grecaptcha.reset();
- return;
- }
-
- if (nodes.pwd.value.length < 12 && !window.confirm("Your password is quite short. For better security you should consider using a password at least 12 characters long.\n\nDo you really want to use a weak password?")) {
- grecaptcha.reset();
- nodes.pwd.focus();
- return;
- }
-
- if (nodes.email.value.length < 3 && !window.confirm("Warning: if you do not supply an email address you will be completely unable to reset your password if you loose it.\n\nDo you really want to continue without an email address?")) {
- grecaptcha.reset();
- nodes.email.focus();
- return;
- }
-
- const req = new Request("/api/account", {
- method: "POST",
- mode: "same-origin",
- cache: "no-cache",
- redirect: "follow",
- referrer: "no-referrer",
- headers: {
- "Accept": "application/json",
- "Content-Type": "application/json",
- "X-CAPTCHA-TOKEN": token
- },
- body: JSON.stringify({
- username: nodes.uname.value,
- password: nodes.pwd.value,
- email: nodes.email.value,
- }),
- });
-
- fetch(req)
- .then(response => {
- switch(response.status) {
- case 201:
- return response.json();
- case 400:
- throw new Error("Malformed request!");
- case 403:
- throw new Error("Failed the captcha challenge!");
- case 409:
- nodes.uname.focus();
- throw new Error("This username already exists!");
- case 429:
- throw new Error("Too many requests. Please try again later.");
- case 500:
- throw new Error("Internal server error. Please try again later.");
- case 502:
- throw new Error("Couldn't reach the server. Please try again later.");
- default:
- throw new Error(`Received status code ${response.status}.`);
- }
- })
- .then(response => {
- nodes.form.innerText = "Account created successfully.\nIt may take a few minutes before you are able to log in.";
- })
- .catch(error => {
- nodes.form.classList.add("error");
- nodes.status.innerText = error;
- nodes.status.style.display = "block"; // <= MS Edge bug
- grecaptcha.reset();
- });
- }
-
- function Init() {
- nodes.captcha = document.querySelector(".grecaptcha-badge");
- }
- </script>
- <script src="https://www.google.com/recaptcha/api.js?onload=Init" async defer></script>
</body>
</html>
diff --git a/src/reset_password.html b/src/reset_password.html
new file mode 100644
index 0000000..9c04907
--- /dev/null
+++ b/src/reset_password.html
@@ -0,0 +1,356 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <title>The Mana World</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ -o-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ }
+
+ body {
+ font-family: "Roboto", Helvetica, Arial, sans-serif;
+ font-weight: 100;
+ font-size: 12px;
+ line-height: 30px;
+ color: #777;
+ background: rgb(199, 83, 83);
+ }
+
+ .container {
+ max-width: 400px;
+ width: 100%;
+ margin: 0 auto;
+ position: relative;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"],
+ .container > form button {
+ font: 400 12px/16px "Roboto", Helvetica, Arial, sans-serif;
+ }
+
+ .container > form {
+ background: #F9F9F9;
+ padding: 25px;
+ margin: 150px 0;
+ box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
+ }
+
+ .container > form h3 {
+ display: block;
+ font-size: 30px;
+ font-weight: 300;
+ margin-bottom: 10px;
+ }
+
+ .container > form h4 {
+ margin: 5px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ }
+
+ .container .status {
+ margin: 15px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ display: none;
+ }
+
+ fieldset {
+ border: medium none !important;
+ margin: 0 0 10px;
+ min-width: 100%;
+ padding: 0;
+ width: 100%;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"] {
+ width: 100%;
+ border: 1px solid #ccc;
+ background: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ }
+
+ .container > form input[type="text"]:hover,
+ .container > form input[type="password"]:hover,
+ .container > form input[type="email"]:hover {
+ -webkit-transition: border-color 0.3s ease-in-out;
+ -moz-transition: border-color 0.3s ease-in-out;
+ transition: border-color 0.3s ease-in-out;
+ border: 1px solid #aaa;
+ }
+
+ .container > form button {
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ background: rgba(0, 0, 0, 0.6);
+ color: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ font-size: 15px;
+ }
+
+ .container > form button:hover {
+ background: rgba(0, 0, 0, 0.9);
+ -webkit-transition: background 0.3s ease-in-out;
+ -moz-transition: background 0.3s ease-in-out;
+ transition: background-color 0.3s ease-in-out;
+ }
+
+ .container > form button:active {
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
+ }
+
+ .container > form input:focus {
+ outline: 0;
+ border: 1px solid #aaa;
+ }
+
+ .container > form.error .status {
+ display: block!important;
+ }
+
+ input.invalid {
+ border-color: #900!important;
+ background-color: #FDD!important;
+ }
+
+ /*input:focus:required:valid {
+ background-color: greenyellow!important;
+ }*/
+
+ .warning {
+ color: #fff;
+ margin: 1em;
+ display: inline-block;
+ background: orangered;
+ padding: 1em;
+ border: 3px yellow dashed;
+ }
+
+ ::-webkit-input-placeholder {
+ color: #888;
+ }
+
+ :-moz-placeholder {
+ color: #888;
+ }
+
+ ::-moz-placeholder {
+ color: #888;
+ }
+
+ :-ms-input-placeholder {
+ color: #888;
+ }
+ </style>
+ <script type="text/javascript">
+ /* this block is intentionally left in <head>, for legacy browsers */
+ if (!("fetch" in window && "Request" in window && "setCustomValidity" in document.createElement("input"))) {
+ document.body.innerHTML = "<h1 style=\"color:#fff;margin:1em\">Please update your browser to continue.</h1>";
+ throw new Error("outdated browser");
+ }
+ </script>
+ </head>
+ <body>
+ <noscript>
+ <div class="warning">
+ <h1>Javascript is required for this page.</h1>
+ <!-- link to license -->
+ <!-- link to source code -->
+ </div>
+ </noscript>
+
+ <div class="container">
+ <form action="#" method="POST">
+ <h3>The Mana World</h3>
+ <h4>Password reset</h4>
+ <span class="status">...</span>
+ <fieldset>
+ <input name="username" placeholder="Username" type="text" tabindex="1" minlength="4" maxlength="23" pattern="^[a-zA-Z0-9]{4,23}$" title="4-23 characters, alphanumeric" required autofocus>
+ </fieldset>
+ <fieldset>
+ <input name="password" placeholder="New Password" type="password" tabindex="2" minlength="6" maxlength="23" pattern="^[a-zA-Z0-9]{6,23}$" title="6-23 characters, alphanumeric" required>
+ </fieldset>
+ <fieldset>
+ <div class="g-recaptcha"
+ data-sitekey="6LdaVUcUAAAAAJ-7cORTu4cZCPSNjDqjz3y4nLVR"
+ data-callback="onCaptchaCompletion"
+ data-size="invisible">
+ </div>
+ <div>
+ <button type="submit">Submit</button>
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ <script>
+ const uuid = document.location.hash.slice(1);
+ var recaptcha_loaded = false;
+
+ const nodes = {
+ uname: document.querySelector("input[name=username]"),
+ pwd: document.querySelector("input[name=password]"),
+ form: document.querySelector(".container > form"),
+ status: document.querySelector(".status"),
+ button: document.querySelector(".container > form button"),
+ };
+
+ if (uuid.length < 6) {
+ nodes.form.innerText = "The verification code is missing from the URL.\nCopy-paste the full URL provided in the email and try again.";
+ }
+
+ const validateInput = node => {
+ node.setCustomValidity("");
+ if (node.checkValidity() === false && node.value != "") {
+ node.classList.add("invalid");
+ return false;
+ } else {
+ node.classList.remove("invalid");
+ return true;
+ }
+ };
+
+ nodes.form.addEventListener("input", event => {
+ validateInput(event.target);
+ });
+
+ nodes.pwd.addEventListener("change", e => {
+ if (e.isTrusted && nodes.form.querySelector("input") && Reflect.has(window, "Rusha") && nodes.pwd.checkValidity()) {
+ const full_hash = Rusha.createHash().update(nodes.pwd.value).digest("hex");
+ const hash_prefix = full_hash.substring(0, 5);
+ const hash_suffix = full_hash.substring(5);
+
+ const req = new Request(`https://api.pwnedpasswords.com/range/${hash_prefix}`, {
+ method: "GET",
+ mode: "cors",
+ cache: "force-cache",
+ referrer: "no-referrer",
+ });
+
+ fetch(req)
+ .then(response => response.text())
+ .then(response => {
+ const found = response.split("\n").some(h => {
+ const [hs, times] = h.split(":");
+
+ if (hash_suffix.toUpperCase() === hs.toUpperCase())
+ return true;
+
+ return false;
+ });
+
+ if (found === true) {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = "WARNING: This password has previously appeared in a data breach. Please use a more secure alternative.\n>> checked by haveibeenpwned.com\n\n";
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ nodes.pwd.focus();
+ nodes.pwd.classList.add("invalid");
+ } else {
+ nodes.form.classList.remove("error");
+ nodes.status.style.display = "none";
+ }
+ });
+ }
+ });
+
+ nodes.form.addEventListener("submit", e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ if (nodes.pwd.value.length < 12 && !window.confirm("Your password is quite short. For better security you should consider using a password at least 12 characters long.\n\nDo you really want to use a weak password?")) {
+ nodes.pwd.focus();
+ return;
+ }
+
+ if (!recaptcha_loaded || !Reflect.has(window, "grecaptcha")) {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = "reCAPTCHA couldn't be loaded. Make sure to whitelist this page in noscript, ghostery or any other ad/tracker blocker.";
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ return;
+ }
+
+ grecaptcha.execute();
+ });
+
+ function onCaptchaCompletion(token) {
+ const req = new Request("/api/tmwa/account", {
+ method: "PUT",
+ mode: "same-origin",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token
+ },
+ body: JSON.stringify({
+ username: nodes.uname.value,
+ password: nodes.pwd.value,
+ code: uuid,
+ }),
+ });
+
+ fetch(req)
+ .then(response => {
+ switch(response.status) {
+ case 200:
+ case 201:
+ return response.json();
+ case 400:
+ throw new Error("Malformed request!");
+ case 403:
+ throw new Error("Failed the captcha challenge!");
+ case 404:
+ nodes.uname.focus();
+ throw new Error("No accounts found for this email address!");
+ case 408:
+ throw new Error("Request expired. Please try again later.");
+ case 429:
+ throw new Error("Too many requests. Please try again later.");
+ case 500:
+ throw new Error("Internal server error. Please try again later.");
+ case 502:
+ throw new Error("Couldn't reach the server. Please try again later.");
+ default:
+ throw new Error(`Received status code ${response.status}.`);
+ }
+ })
+ .then(response => {
+ nodes.form.innerText = "Your password has successfully been reset.\nHave fun playing The Mana World!";
+ })
+ .catch(error => {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = error;
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ grecaptcha.reset();
+ });
+ }
+
+ function ReInit() {
+ recaptcha_loaded = true;
+ }
+ </script>
+ <script src="https://www.google.com/recaptcha/api.js?onload=ReInit" async defer></script>
+ <script src="/rusha.min.js"></script>
+ </body>
+</html>
diff --git a/src/reset_password_init.html b/src/reset_password_init.html
new file mode 100644
index 0000000..015f8e9
--- /dev/null
+++ b/src/reset_password_init.html
@@ -0,0 +1,306 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <title>The Mana World</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ -o-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ }
+
+ body {
+ font-family: "Roboto", Helvetica, Arial, sans-serif;
+ font-weight: 100;
+ font-size: 12px;
+ line-height: 30px;
+ color: #777;
+ background: rgba(0, 0, 0, 0.6);
+ }
+
+ .container {
+ max-width: 400px;
+ width: 100%;
+ margin: 0 auto;
+ position: relative;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"],
+ .container > form button {
+ font: 400 12px/16px "Roboto", Helvetica, Arial, sans-serif;
+ }
+
+ .container > form {
+ background: #F9F9F9;
+ padding: 25px;
+ margin: 150px 0;
+ box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
+ }
+
+ .container > form h3 {
+ display: block;
+ font-size: 30px;
+ font-weight: 300;
+ margin-bottom: 10px;
+ }
+
+ .container > form h4 {
+ margin: 5px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ }
+
+ .container .status {
+ margin: 15px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ display: none;
+ }
+
+ fieldset {
+ border: medium none !important;
+ margin: 0 0 10px;
+ min-width: 100%;
+ padding: 0;
+ width: 100%;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"] {
+ width: 100%;
+ border: 1px solid #ccc;
+ background: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ }
+
+ .container > form input[type="text"]:hover,
+ .container > form input[type="password"]:hover,
+ .container > form input[type="email"]:hover {
+ -webkit-transition: border-color 0.3s ease-in-out;
+ -moz-transition: border-color 0.3s ease-in-out;
+ transition: border-color 0.3s ease-in-out;
+ border: 1px solid #aaa;
+ }
+
+ .container > form button {
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ background: rgba(0, 0, 0, 0.6);
+ color: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ font-size: 15px;
+ }
+
+ .container > form button:hover {
+ background: rgba(0, 0, 0, 0.9);
+ -webkit-transition: background 0.3s ease-in-out;
+ -moz-transition: background 0.3s ease-in-out;
+ transition: background-color 0.3s ease-in-out;
+ }
+
+ .container > form button:active {
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
+ }
+
+ .container > form input:focus {
+ outline: 0;
+ border: 1px solid #aaa;
+ }
+
+ .container > form.error .status {
+ display: block!important;
+ }
+
+ input.invalid {
+ border-color: #900!important;
+ background-color: #FDD!important;
+ }
+
+ /*input:focus:required:valid {
+ background-color: greenyellow!important;
+ }*/
+
+ .warning {
+ color: #fff;
+ margin: 1em;
+ display: inline-block;
+ background: orangered;
+ padding: 1em;
+ border: 3px yellow dashed;
+ }
+
+ ::-webkit-input-placeholder {
+ color: #888;
+ }
+
+ :-moz-placeholder {
+ color: #888;
+ }
+
+ ::-moz-placeholder {
+ color: #888;
+ }
+
+ :-ms-input-placeholder {
+ color: #888;
+ }
+ </style>
+ <script type="text/javascript">
+ /* this block is intentionally left in <head>, for legacy browsers */
+ if (!("fetch" in window && "Request" in window && "setCustomValidity" in document.createElement("input"))) {
+ document.body.innerHTML = "<h1 style=\"color:#fff;margin:1em\">Please update your browser to continue.</h1>";
+ throw new Error("outdated browser");
+ }
+
+ if (document.location.hash.length > 6) {
+ document.location.pathname = "/recover/reset";
+ }
+ </script>
+ </head>
+ <body>
+ <noscript>
+ <div class="warning">
+ <h1>Javascript is required for this page.</h1>
+ <!-- link to license -->
+ <!-- link to source code -->
+ </div>
+ </noscript>
+
+ <div class="container">
+ <form action="#" method="POST">
+ <h3>The Mana World</h3>
+ <h4>Password reset</h4>
+ <span class="status">...</span>
+ <fieldset>
+ <input name="email" placeholder="Email address" type="email" tabindex="1" minlength="1" maxlength="39" required autofocus>
+ </fieldset>
+ <fieldset>
+ <div class="g-recaptcha"
+ data-sitekey="6LdaVUcUAAAAAJ-7cORTu4cZCPSNjDqjz3y4nLVR"
+ data-callback="onCaptchaCompletion"
+ data-size="invisible">
+ </div>
+ <div>
+ <button type="submit">Submit</button>
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ <script>
+ var recaptcha_loaded = false;
+
+ const nodes = {
+ email: document.querySelector("input[name=email]"),
+ form: document.querySelector(".container > form"),
+ status: document.querySelector(".status"),
+ button: document.querySelector(".container > form button"),
+ };
+
+ const validateInput = node => {
+ node.setCustomValidity("");
+ if (node.checkValidity() === false && node.value != "") {
+ node.classList.add("invalid");
+ return false;
+ } else {
+ node.classList.remove("invalid");
+ return true;
+ }
+ };
+
+ nodes.form.addEventListener("input", event => {
+ validateInput(event.target);
+ });
+
+ nodes.form.addEventListener("submit", e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+
+
+ if (!recaptcha_loaded || !Reflect.has(window, "grecaptcha")) {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = "reCAPTCHA couldn't be loaded. Make sure to whitelist this page in noscript, ghostery or any other ad/tracker blocker.";
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ return;
+ }
+
+ grecaptcha.execute();
+ });
+
+ function onCaptchaCompletion(token) {
+ const req = new Request("/api/tmwa/account", {
+ method: "PUT",
+ mode: "same-origin",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token
+ },
+ body: JSON.stringify({
+ email: nodes.email.value,
+ }),
+ });
+
+ fetch(req)
+ .then(response => {
+ switch(response.status) {
+ case 200:
+ case 201:
+ return response.json();
+ case 400:
+ throw new Error("Malformed request!");
+ case 403:
+ throw new Error("Failed the captcha challenge!");
+ case 404:
+ nodes.uname.focus();
+ throw new Error("No accounts found for this email address!");
+ case 408:
+ throw new Error("Request expired. Please try again later.");
+ case 429:
+ throw new Error("A password reset request is already pending for this email address.");
+ case 500:
+ throw new Error("Internal server error. Please try again later.");
+ case 502:
+ throw new Error("Couldn't reach the server. Please try again later.");
+ default:
+ throw new Error(`Received status code ${response.status}.`);
+ }
+ })
+ .then(response => {
+ nodes.form.innerText = "An email was sent with the reset instructions.\nPlease use the embedded link to proceed.";
+ })
+ .catch(error => {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = error;
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ grecaptcha.reset();
+ });
+ }
+
+ function ReInit() {
+ recaptcha_loaded = true;
+ }
+ </script>
+ <script src="https://www.google.com/recaptcha/api.js?onload=ReInit" async defer></script>
+ </body>
+</html>
diff --git a/src/reset_user.html b/src/reset_user.html
new file mode 100644
index 0000000..58bb417
--- /dev/null
+++ b/src/reset_user.html
@@ -0,0 +1,302 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+ <title>The Mana World</title>
+ <style>
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ -webkit-font-smoothing: antialiased;
+ -moz-font-smoothing: antialiased;
+ -o-font-smoothing: antialiased;
+ font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+ }
+
+ body {
+ font-family: "Roboto", Helvetica, Arial, sans-serif;
+ font-weight: 100;
+ font-size: 12px;
+ line-height: 30px;
+ color: #777;
+ background: rgba(0, 0, 0, 0.6);
+ }
+
+ .container {
+ max-width: 400px;
+ width: 100%;
+ margin: 0 auto;
+ position: relative;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"],
+ .container > form button {
+ font: 400 12px/16px "Roboto", Helvetica, Arial, sans-serif;
+ }
+
+ .container > form {
+ background: #F9F9F9;
+ padding: 25px;
+ margin: 150px 0;
+ box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.2), 0 5px 5px 0 rgba(0, 0, 0, 0.24);
+ }
+
+ .container > form h3 {
+ display: block;
+ font-size: 30px;
+ font-weight: 300;
+ margin-bottom: 10px;
+ }
+
+ .container > form h4 {
+ margin: 5px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ }
+
+ .container .status {
+ margin: 15px 0 15px;
+ display: block;
+ font-size: 13px;
+ font-weight: 400;
+ display: none;
+ }
+
+ fieldset {
+ border: medium none !important;
+ margin: 0 0 10px;
+ min-width: 100%;
+ padding: 0;
+ width: 100%;
+ }
+
+ .container > form input[type="text"],
+ .container > form input[type="password"],
+ .container > form input[type="email"] {
+ width: 100%;
+ border: 1px solid #ccc;
+ background: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ }
+
+ .container > form input[type="text"]:hover,
+ .container > form input[type="password"]:hover,
+ .container > form input[type="email"]:hover {
+ -webkit-transition: border-color 0.3s ease-in-out;
+ -moz-transition: border-color 0.3s ease-in-out;
+ transition: border-color 0.3s ease-in-out;
+ border: 1px solid #aaa;
+ }
+
+ .container > form button {
+ cursor: pointer;
+ width: 100%;
+ border: none;
+ background: rgba(0, 0, 0, 0.6);
+ color: #FFF;
+ margin: 0 0 5px;
+ padding: 10px;
+ font-size: 15px;
+ }
+
+ .container > form button:hover {
+ background: rgba(0, 0, 0, 0.9);
+ -webkit-transition: background 0.3s ease-in-out;
+ -moz-transition: background 0.3s ease-in-out;
+ transition: background-color 0.3s ease-in-out;
+ }
+
+ .container > form button:active {
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
+ }
+
+ .container > form input:focus {
+ outline: 0;
+ border: 1px solid #aaa;
+ }
+
+ .container > form.error .status {
+ display: block!important;
+ }
+
+ input.invalid {
+ border-color: #900!important;
+ background-color: #FDD!important;
+ }
+
+ /*input:focus:required:valid {
+ background-color: greenyellow!important;
+ }*/
+
+ .warning {
+ color: #fff;
+ margin: 1em;
+ display: inline-block;
+ background: orangered;
+ padding: 1em;
+ border: 3px yellow dashed;
+ }
+
+ ::-webkit-input-placeholder {
+ color: #888;
+ }
+
+ :-moz-placeholder {
+ color: #888;
+ }
+
+ ::-moz-placeholder {
+ color: #888;
+ }
+
+ :-ms-input-placeholder {
+ color: #888;
+ }
+ </style>
+ <script type="text/javascript">
+ /* this block is intentionally left in <head>, for legacy browsers */
+ if (!("fetch" in window && "Request" in window && "setCustomValidity" in document.createElement("input"))) {
+ document.body.innerHTML = "<h1 style=\"color:#fff;margin:1em\">Please update your browser to continue.</h1>";
+ throw new Error("outdated browser");
+ }
+ </script>
+ </head>
+ <body>
+ <noscript>
+ <div class="warning">
+ <h1>Javascript is required for this page.</h1>
+ <!-- link to license -->
+ <!-- link to source code -->
+ </div>
+ </noscript>
+
+ <div class="container">
+ <form action="#" method="POST">
+ <h3>The Mana World</h3>
+ <h4>Account recovery</h4>
+ <span class="status">...</span>
+ <fieldset>
+ <input name="email" placeholder="Email address" type="email" tabindex="1" minlength="1" maxlength="39" required autofocus>
+ </fieldset>
+ <fieldset>
+ <div class="g-recaptcha"
+ data-sitekey="6LdaVUcUAAAAAJ-7cORTu4cZCPSNjDqjz3y4nLVR"
+ data-callback="onCaptchaCompletion"
+ data-size="invisible">
+ </div>
+ <div>
+ <button type="submit">Submit</button>
+ </div>
+ </fieldset>
+ </form>
+ </div>
+ <script>
+ var recaptcha_loaded = false;
+
+ const nodes = {
+ email: document.querySelector("input[name=email]"),
+ form: document.querySelector(".container > form"),
+ status: document.querySelector(".status"),
+ button: document.querySelector(".container > form button"),
+ };
+
+ const validateInput = node => {
+ node.setCustomValidity("");
+ if (node.checkValidity() === false && node.value != "") {
+ node.classList.add("invalid");
+ return false;
+ } else {
+ node.classList.remove("invalid");
+ return true;
+ }
+ };
+
+ nodes.form.addEventListener("input", event => {
+ validateInput(event.target);
+ });
+
+ nodes.form.addEventListener("submit", e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+
+
+ if (!recaptcha_loaded || !Reflect.has(window, "grecaptcha")) {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = "reCAPTCHA couldn't be loaded. Make sure to whitelist this page in noscript, ghostery or any other ad/tracker blocker.";
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ return;
+ }
+
+ grecaptcha.execute();
+ });
+
+ function onCaptchaCompletion(token) {
+ const req = new Request("/api/tmwa/account", {
+ method: "PUT",
+ mode: "same-origin",
+ cache: "no-cache",
+ redirect: "follow",
+ referrer: "no-referrer",
+ headers: {
+ "Accept": "application/json",
+ "Content-Type": "application/json",
+ "X-CAPTCHA-TOKEN": token
+ },
+ body: JSON.stringify({
+ email: nodes.email.value,
+ }),
+ });
+
+ fetch(req)
+ .then(response => {
+ switch(response.status) {
+ case 200:
+ case 201:
+ return response.json();
+ case 400:
+ throw new Error("Malformed request!");
+ case 403:
+ throw new Error("Failed the captcha challenge!");
+ case 404:
+ nodes.uname.focus();
+ throw new Error("No accounts found for this email address!");
+ case 408:
+ throw new Error("Request expired. Please try again later.");
+ case 429:
+ throw new Error("Too many requests. Please try again later.");
+ case 500:
+ throw new Error("Internal server error. Please try again later.");
+ case 502:
+ throw new Error("Couldn't reach the server. Please try again later.");
+ default:
+ throw new Error(`Received status code ${response.status}.`);
+ }
+ })
+ .then(response => {
+ nodes.form.innerText = "An email was sent with the list of your accounts.\nYou may perform a password reset with the provided link.";
+ })
+ .catch(error => {
+ nodes.form.classList.add("error");
+ nodes.status.innerText = error;
+ nodes.status.style.display = "block"; // <= MS Edge bug
+ grecaptcha.reset();
+ });
+ }
+
+ function ReInit() {
+ recaptcha_loaded = true;
+ }
+ </script>
+ <script src="https://www.google.com/recaptcha/api.js?onload=ReInit" async defer></script>
+ </body>
+</html>