diff options
-rw-r--r-- | src/router/index.ts | 5 | ||||
-rw-r--r-- | src/views/Migration.vue | 356 |
2 files changed, 361 insertions, 0 deletions
diff --git a/src/router/index.ts b/src/router/index.ts index 04828e4..99267f9 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -36,6 +36,11 @@ const routes: Array<RouteRecordRaw> = [ component: () => import(/* webpackChunkName: "registration" */ "../views/Registration.vue"), }, { + path: "/migration", + name: "migration", + component: () => import(/* webpackChunkName: "migration" */ "../views/Migration.vue"), + }, + { path: "/404:pathMatch(.*)", alias: "/:pathMatch(.*)", name: "not found", diff --git a/src/views/Migration.vue b/src/views/Migration.vue new file mode 100644 index 0000000..6f82779 --- /dev/null +++ b/src/views/Migration.vue @@ -0,0 +1,356 @@ +<template> + <main> + <div v-if="nextStep != 4"> + <h1>Account Migration</h1> + <p>Use this form to migrate a TMW Legacy account to Vault.</p> + <p>Please note you may only have <b>one</b> Vault account.</p> + <p>Once complete, you'll need the Mana Launcher to login.</p> + </div> + + <div v-if="step == -3"> + <h1>reCAPTCHA privacy notice</h1> + <p>This page uses Google reCAPTCHA. By continuing, you agree to use reCAPTCHA, which may register tracking cookies in your browser.</p> + <p>You may review the Google Privacy Policy at <a href="https://policies.google.com/privacy">https://policies.google.com/privacy</a>.</p> + + <button @click="start">I understand and wish to proceed</button> + <br><br> + + <h1>Proceeding without reCAPTCHA</h1> + <p>If you would rather not use reCAPTCHA, you may recover your account by contacting us by email.</p> + <br> + <address>support@themanaworld.org</address> + </div> + + <div v-if="step == -4"> + <h1>Loading...</h1> + <p>Please wait while reCAPTCHA is loading...</p> + <br><br> + + <h1>Proceeding without reCAPTCHA</h1> + <p>If you would rather not use reCAPTCHA, you may recover your account by contacting us by email.</p> + <br> + <address>support@themanaworld.org</address> + </div> + + <div v-if="step == -1"> + <h1>reCAPTCHA could not be loaded</h1> + <p>This page requires reCAPTCHA but something prevents it from loading. + If you are using an ad blocker or tracker blocker please whitelist this page and refresh to continue.</p> + + <h1>Proceeding without reCAPTCHA</h1> + <p>If you would rather not use reCAPTCHA, you may recover your account by contacting us by email.</p> + <br> + <address>support@themanaworld.org</address> + </div> + + <div v-if="step == 1"> + <h1>TMW Legacy Data</h1> + <p>Do note it validates the username, email and password.</p> + <p>Your Vault Password will be set to the same password you've used in TMW Legacy. Your Vault Username is your email.</p> + <p>If you opt for 2 Factor Authentication, you'll receive by email the secret to configure an authentication app e.g. Google Authenticator.</p> + <p>Once enabled, you will not be able to login without it.</p> + + <form @submit.prevent="checkEmail"> + <label for="email">Enter your email address:</label> + <input v-model="user.email" type="email" maxlength="39" id="email" ref="_email" placeholder="you@mail.com" required> + <label for="user">Enter your username:</label> + <input v-model="user.name" type="text" maxlength="39" id="user" ref="_user" placeholder="username" required> + <label for="password">Enter your password:</label> + <input v-model="user.pwd" type="password" maxlength="39" id="password" ref="_password" placeholder="password" required> + <label for="totp">Enable 2FA?</label> + <input v-model="user.totp" type="checkbox" id="totp" ref="_totp" checked> + <button type="submit">Submit →</button> + </form> + </div> + + <div v-if="step == 2"> + <h1>Confirm</h1> + <label for="c-email">Email address:</label> + <input id="c-email" disabled readonly type="email" :value="user.email" placeholder="(no email)"> + <button @click="confirm">Confirm</button> + </div> + + <div v-if="step == 3"> + <h1>Migration process started</h1> + <p>Your account migration has been requested.</p> + + <h1>Next steps</h1> + <p>If everything went well, you'll be able to login on Mana Launcher using the provided email and password.</p> + <p>If you enabled 2FA, you'll also need the token which has been sent to your email.</p> + + <br><br><br> + <h1>Can't find the account you were looking for?</h1> + <p>If data inserted here is incorrect or a Vault account already exists bound to that email, the migration will not happen.</p> + + <h1>Still need help?</h1> + <p>Feel free to <router-link :to="{ name: 'support' }">contact us</router-link> for further assistance.</p> + </div> + + <div class="g-recaptcha" id="recaptcha-container" + :data-sitekey="recaptcha_key" + data-size="invisible"> + </div> + </main> +</template> + +<script lang="ts"> +import { Options, Vue } from "vue-class-component"; +import reCAPTCHA from "@/reCAPTCHA"; + +@Options({}) +export default class Recovery extends Vue { + step = -3; // ask to use reCAPTCHA + nextStep = 1; // first step after reCAPTCHA confirmation + user = { + email: "", + name: "", + totp: false, + pwd: "", + }; + + recaptcha_key = process.env.VUE_APP_RECAPTCHA; + + async mounted () { + // already loaded (user returned to this page) + if (reCAPTCHA.isReady) { + if (this.step == -3) { + this.step = this.nextStep; + } + + await this.$nextTick(); + reCAPTCHA.instance.render("recaptcha-container", { + sitekey: process.env.VUE_APP_RECAPTCHA, + size: "invisible", + }); + reCAPTCHA.instance.reset(); + + if (this.step == 1) { + (this.$refs._email as HTMLInputElement).focus(); + } + } + } + + async start () { + this.step = -4; + + try { + await reCAPTCHA.load(); + this.step = this.nextStep; + await this.$nextTick(); + + if (this.step == 1) { + (this.$refs._email as HTMLInputElement).focus(); + } + } catch (err) { + this.step = -1 + } + } + + async checkEmail () { + this.step = reCAPTCHA.isReady ? 2 : -1; + // XXX: any actual checks needed here? + } + + private sleep (milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); + } + + async confirm () { + reCAPTCHA.instance.execute(); + let token = ""; + console.log("Confirmed") + + // the recaptcha API doesn't play nice with Vue + while (!(token = reCAPTCHA.instance.getResponse())) { + await this.sleep(1000); + } + console.log("Captcha OK") + + // ${process.env.VUE_APP_API} + // https://api.tmw2.org:13370/ + const req = new Request(`http://localhost:13370/tmwa_auth`, { + method: "POST", + mode: "no-cors", + cache: "no-cache", + redirect: "follow", + referrer: "no-referrer", + headers: { + "Accept": "application/json", + "Content-Type": "application/json", + "X-CAPTCHA-TOKEN": token, + }, + body: JSON.stringify({ + mail: this.user.email, + user: this.user.name, + pass: this.user.pwd, + totp: this.user.totp, + }), + }); + console.log("Req OK") + + const rawResponse = await fetch(req); + //const response: string = await rawResponse.text(); + console.log("Reply obtained") + + // FIXME: Not caught, always cause error 0 + switch (rawResponse.status) { + // TODO: don't use alerts: embed the error message on the page + case 0: + this.step = 3; + break; + case 200: + case 201: + this.step = 3; + break; + case 400: + self.alert("API: malformed request"); + document.location.reload(); + break; + case 403: + self.alert("Captcha or account validation failed.\nPlease try again later"); + document.location.reload(); + break; + case 404: + this.step = 1; + reCAPTCHA.instance.reset(); + await this.$nextTick(); + (this.$refs._email as HTMLInputElement).focus(); + break; + case 408: + this.step = -4; + break; + case 429: + self.alert("Too many requests.\nPlease try again later"); + document.location.reload(); + break; + case 500: + self.alert("Internal server error.\nPlease try again later"); + document.location.reload(); + break; + case 502: + self.alert("Couldn't reach the server.\nPlease try again later"); + document.location.reload(); + break; + default: + self.alert(`Unknown error: ${rawResponse.status}`); + document.location.reload(); + break; + } + } + +} +</script> + +<style scoped> +/* +TODO: share the stylesheet with Registration (DRY) +*/ +form { + margin-top: 20px; +} + +main { + & > h1 + div { + margin-top: 30px; + } + + & label { + display: block; + + &:nth-of-type(1n + 2) { + margin-top: 1em; + } + } + + & .pass-box { + position: relative; + + &:nth-of-type(1n + 2) { + margin-top: 1em; + } + } + + & input { + width: calc(100% - 2ch); + border: 1px solid #2f2e32; + font-size: 15px; + padding: 1ch; + margin-top: 0.6ch; + + & + .pass-box { + margin-top: 1em; + } + + & + span { + &::after { + content: "👁"; + font-family: monospace; + padding: 0 0.5ch 0 0.5ch; + } + + position: absolute; + right: -1px; + top: auto; + bottom: 0; + font-size: 1.9em; + cursor: pointer; + } + + &[type="text"] + span { + background: rgba(0, 0, 0, 0.2); + } + } + + & button { + margin-top: 1em; + width: 100%; + background-color: #34B039; + border: 1px solid #2f2e32; + display: inline-block; + cursor: pointer; + color: #ffffff; + font-size: 15px; + font-weight: bold; + padding: 1ch; + text-decoration: none; + + &:hover { + background-color: #2F9E33; + } + } + + & > div:nth-of-type(1n + 2) { + margin-top: 30px; + } + + & .exposed { + background: rgba(255, 0, 0, 0.1); + border: dashed 6px rgba(255, 0, 0, 0.9); + padding: 1em; + margin: 1em; + animation-name: scary; + animation-duration: 2s; + + & a { + display: block; + margin-top: 0.7em; + } + } + + & .error { + padding: 1em; + } +} + +@keyframes scary { + from { + background-color: rgba(255, 0, 0, 0); + border-color: rgba(255, 0, 0, 0); + } + + to { + background-color: rgba(255, 0, 0, 0.1); + border-color: rgba(255, 0, 0, 0.9); + } +} +</style> |