Browse Source

feat(webapp): adds reset password form

Nils Wisiol 5 years ago
parent
commit
ddf24742ce
4 changed files with 231 additions and 1 deletions
  1. 5 0
      webapp/src/App.vue
  2. 5 0
      webapp/src/router/index.js
  3. 220 0
      webapp/src/views/ResetPassword.vue
  4. 1 1
      webapp/src/views/Welcome.vue

+ 5 - 0
webapp/src/App.vue

@@ -118,6 +118,11 @@ export default {
         'icon': 'mdi-gift-outline',
         'text': 'Donate',
       },
+      'reset-password': {
+        'name': 'reset-password',
+        'icon': 'mdi-lock-reset',
+        'text': 'Reset Account Password',
+      },
     }
   }),
 }

+ 5 - 0
webapp/src/router/index.js

@@ -33,6 +33,11 @@ const routes = [
     name: 'docs',
     beforeEnter(to) { location.href = to.path },
   },
+  {
+    path: '/reset-password/:email?',
+    name: 'reset-password',
+    component: () => import(/* webpackChunkName: "signup" */ '../views/ResetPassword.vue')
+  },
   {
     path: '/donate/',
     name: 'donate',

+ 220 - 0
webapp/src/views/ResetPassword.vue

@@ -0,0 +1,220 @@
+<template>
+    <v-container
+            class="fill-height"
+            fluid
+    >
+        <v-row
+                align="center"
+                justify="center"
+        >
+            <v-col
+                    cols="12"
+                    sm="8"
+                    md="6"
+            >
+                <v-form @submit.prevent="resetPassword" ref="form">
+                    <v-card class="elevation-12 pb-4">
+                        <v-toolbar
+                                color="primary"
+                                dark
+                                flat
+                        >
+                            <v-toolbar-title>Reset Account Password</v-toolbar-title>
+                        </v-toolbar>
+                        <v-card-text>
+                            <v-alert :value="!!(errors && errors.length)" type="error">
+                                <div v-if="errors.length > 1">
+                                    <li v-for="error of errors" :key="error.message">
+                                        <b>{{ error.message }}</b>
+                                        {{ error }}
+                                    </li>
+                                </div>
+                                <div v-else>
+                                    {{ errors[0] }}
+                                </div>
+                            </v-alert>
+                            <v-alert v-if="done" type="success">
+                                <p>
+                                    We received the password reset request. If an account with this email address exists
+                                    in our database, we sent an email with password reset instructions. If you did not
+                                    receive an email, you can
+                                    <router-link :to="{name: 'signup', params: email ? {email: email} : {}}">
+                                        create an account
+                                    </router-link>
+                                    .
+                                </p>
+                            </v-alert>
+
+                            <v-text-field
+                                    v-model="email"
+                                    label="Email"
+                                    prepend-icon="mdi-email"
+                                    outline
+                                    required
+                                    :disabled="working"
+                                    :rules="email_rules"
+                                    :error-messages="email_errors"
+                                    @change="email_errors=[]"
+                                    validate-on-blur
+                                    ref="emailField"
+                                    tabindex="1"
+                            />
+
+                            <v-layout>
+                                <v-text-field
+                                            v-model="captchaSolution"
+                                            label="Type CAPTCHA text here"
+                                            prepend-icon="mdi-account-check"
+                                            outline
+                                            required
+                                            :disabled="working"
+                                            :rules="captcha_rules"
+                                            :error-messages="captcha_errors"
+                                            @change="captcha_errors=[]"
+                                            @keypress="captcha_errors=[]"
+                                            class="uppercase"
+                                            ref="captchaField"
+                                            tabindex="2"
+                                />
+                                <div class="ml-4 text-center">
+                                    <v-progress-circular
+                                            indeterminate
+                                            v-if="captchaWorking"
+                                    ></v-progress-circular>
+                                    <img
+                                            v-if="captcha && !captchaWorking"
+                                            :src="'data:image/png;base64,'+captcha.challenge"
+                                            alt="Passwords can also be reset by sending an email to our support."
+                                    >
+                                    <br/>
+                                    <v-btn text outlined @click="getCaptcha(true)" :disabled="captchaWorking">New Captcha
+                                    </v-btn>
+                                </div>
+                            </v-layout>
+                        </v-card-text>
+                        <v-card-actions class="justify-center">
+                            <v-btn
+                                    depressed
+                                    color="primary"
+                                    type="submit"
+                                    :disabled="working"
+                                    :loading="working"
+                                    tabindex="3"
+                            >Reset Password
+                            </v-btn>
+                        </v-card-actions>
+                    </v-card>
+                </v-form>
+            </v-col>
+        </v-row>
+    </v-container>
+</template>
+
+<script>
+  import axios from 'axios';
+  import {email_pattern} from '../validation';
+
+  const HTTP = axios.create({
+    baseURL: '/api/v1/',
+    headers: {},
+  });
+
+  export default {
+    name: 'SignUp',
+    data: () => ({
+      valid: false,
+      working: false,
+      done: false,
+      captchaWorking: true,
+      errors: [],
+      captcha: null,
+
+      /* email field */
+      email: '',
+      email_rules: [v => !!email_pattern.test(v || '') || 'We need an email address for account recovery and technical support.'],
+      email_errors: [],
+
+      /* captcha field */
+      captchaSolution: '',
+      captcha_rules: [v => !!v || 'Please enter the text displayed in the picture so we are (somewhat) convinced you are human'],
+      captcha_errors: [],
+    }),
+    async mounted() {
+      if ('email' in this.$route.params && this.$route.params.email !== undefined) {
+        this.email = this.$route.params.email;
+      }
+      this.getCaptcha();
+      this.initialFocus();
+    },
+    methods: {
+      async getCaptcha(focus = false) {
+        this.captchaWorking = true;
+        this.captchaSolution = "";
+        try {
+          this.captcha = (await HTTP.post('captcha/')).data;
+          if(focus) {
+            this.$refs.captchaField.focus()
+          }
+        } finally {
+          this.captchaWorking = false;
+        }
+      },
+      async initialFocus() {
+        return this.$refs.emailField.focus();
+      },
+      async resetPassword() {
+        if (!this.$refs.form.validate()) {
+          return;
+        }
+        this.working = true;
+        this.errors = [];
+        try {
+          await HTTP.post('auth/account/reset-password/', {
+            email: this.email.toLowerCase(),
+            captcha: {
+              id: this.captcha.id,
+              solution: this.captchaSolution,
+            },
+          });
+          this.done = true;
+        } catch (error) {
+          if (error.response) {
+            // status is not 2xx
+            if (error.response.status < 500 && typeof error.response.data === 'object') {
+              // 3xx or 4xx
+              let extracted = false;
+              this.getCaptcha(true);
+              if ('captcha' in error.response.data) {
+                if ('non_field_errors' in error.response.data.captcha) {
+                  this.captcha_errors = [error.response.data.captcha.non_field_errors[0]];
+                  extracted = true;
+                }
+                if ('solution' in error.response.data.captcha) {
+                  this.captcha_errors = error.response.data.captcha.solution;
+                  extracted = true;
+                }
+              }
+              if (!extracted) {
+                this.errors = error.response;
+              }
+            } else {
+              // 5xx
+              this.errors = ['Something went wrong at the server, but we currently do not know why. The customer support was already notified.'];
+            }
+          } else if (error.request) {
+            this.errors = ['Cannot contact our servers. Are you offline?'];
+          } else {
+            this.errors = [error.message];
+          }
+        }
+        this.working = false;
+      },
+    },
+  };
+</script>
+
+<style lang="scss">
+    .uppercase input {
+        text-transform: uppercase;
+    }
+</style>

+ 1 - 1
webapp/src/views/Welcome.vue

@@ -26,7 +26,7 @@
               Thank you for your interest in deSEC.
               To create your deSEC account, please click the verification link in the email that we sent you.
               If you did not receive an email, please check your spam folder or if you already have an account
-              by using the <router-link to="/">account recovery form</router-link>. <!-- TODO -->
+              by using the <router-link :to="{name: 'reset-password'}">password reset form</router-link>.
             </v-alert>
           </v-card-text>
           <v-card-actions>