From 3b9024129d89e29d89c2015702f50f8e1140441f Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Sun, 6 May 2018 11:30:52 +0200
Subject: [PATCH] See #187: Front logic for password reset

---
 .../registration/password_reset_email.html    | 12 +++
 front/src/components/auth/Login.vue           | 33 ++++---
 front/src/router/index.js                     | 19 +++++
 front/src/views/auth/PasswordReset.vue        | 75 ++++++++++++++++
 front/src/views/auth/PasswordResetConfirm.vue | 85 +++++++++++++++++++
 5 files changed, 211 insertions(+), 13 deletions(-)
 create mode 100644 api/funkwhale_api/templates/registration/password_reset_email.html
 create mode 100644 front/src/views/auth/PasswordReset.vue
 create mode 100644 front/src/views/auth/PasswordResetConfirm.vue

diff --git a/api/funkwhale_api/templates/registration/password_reset_email.html b/api/funkwhale_api/templates/registration/password_reset_email.html
new file mode 100644
index 00000000..7a587d72
--- /dev/null
+++ b/api/funkwhale_api/templates/registration/password_reset_email.html
@@ -0,0 +1,12 @@
+{% load i18n %}{% autoescape off %}
+{% blocktrans %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktrans %}
+
+{% trans "Please go to the following page and choose a new password:" %}
+{{ funkwhale_url }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }}
+{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
+
+{% trans "Thanks for using our site!" %}
+
+{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
+
+{% endautoescape %}
diff --git a/front/src/components/auth/Login.vue b/front/src/components/auth/Login.vue
index b06ce89f..f3add57b 100644
--- a/front/src/components/auth/Login.vue
+++ b/front/src/components/auth/Login.vue
@@ -12,9 +12,15 @@
             </ul>
           </div>
           <div class="field">
-            <i18next tag="label" path="Username or email"/>
+            <label>
+              {{ $t('Username or email') }} |
+              <router-link :to="{path: '/signup'}">
+                {{ $t('Create an account') }}
+              </router-link>
+            </label>
             <input
             ref="username"
+            tabindex="1"
             required
             type="text"
             autofocus
@@ -23,18 +29,16 @@
             >
           </div>
           <div class="field">
-            <i18next tag="label" path="Password"/>            
-            <input
-            required
-            type="password"
-            placeholder="Enter your password"
-            v-model="credentials.password"
-            >
+            <label>
+              {{ $t('Password') }} |
+              <router-link :to="{name: 'auth.password-reset', query: {email: credentials.username}}">
+                {{ $t('Reset your password') }}
+              </router-link>
+            </label>
+            <password-input :index="2" required v-model="credentials.password" />
+
           </div>
-          <button :class="['ui', {'loading': isLoading}, 'button']" type="submit"><i18next path="Login"/></button>
-          <router-link class="ui right floated basic button" :to="{path: '/signup'}">
-            <i18next path="Create an account"/>
-          </router-link>
+          <button tabindex="3" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit"><i18next path="Login"/></button>
         </form>
       </div>
     </div>
@@ -42,12 +46,15 @@
 </template>
 
 <script>
+import PasswordInput from '@/components/forms/PasswordInput'
 
 export default {
-  name: 'login',
   props: {
     next: {type: String, default: '/'}
   },
+  components: {
+    PasswordInput
+  },
   data () {
     return {
       // We need to initialize the component with any
diff --git a/front/src/router/index.js b/front/src/router/index.js
index 2e06bda9..3bad260b 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -9,6 +9,8 @@ import Signup from '@/components/auth/Signup'
 import Profile from '@/components/auth/Profile'
 import Settings from '@/components/auth/Settings'
 import Logout from '@/components/auth/Logout'
+import PasswordReset from '@/views/auth/PasswordReset'
+import PasswordResetConfirm from '@/views/auth/PasswordResetConfirm'
 import Library from '@/components/library/Library'
 import LibraryHome from '@/components/library/Home'
 import LibraryArtist from '@/components/library/Artist'
@@ -59,6 +61,23 @@ export default new Router({
       component: Login,
       props: (route) => ({ next: route.query.next || '/library' })
     },
+    {
+      path: '/auth/password/reset',
+      name: 'auth.password-reset',
+      component: PasswordReset,
+      props: (route) => ({
+        defaultEmail: route.query.email
+      })
+    },
+    {
+      path: '/auth/password/reset/confirm',
+      name: 'auth.password-reset-confirm',
+      component: PasswordResetConfirm,
+      props: (route) => ({
+        defaultUid: route.query.uid,
+        defaultToken: route.query.token
+      })
+    },
     {
       path: '/signup',
       name: 'signup',
diff --git a/front/src/views/auth/PasswordReset.vue b/front/src/views/auth/PasswordReset.vue
new file mode 100644
index 00000000..6e80661b
--- /dev/null
+++ b/front/src/views/auth/PasswordReset.vue
@@ -0,0 +1,75 @@
+<template>
+  <div class="main pusher" v-title="$t('Reset your password')">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>{{ $t('Reset your password') }}</h2>
+        <form class="ui form" @submit.prevent="submit()">
+          <div v-if="errors.length > 0" class="ui negative message">
+            <div class="header">{{ $('Error while asking for a password reset') }}</div>
+            <ul class="list">
+              <li v-for="error in errors">{{ error }}</li>
+            </ul>
+          </div>
+          <p>{{ $t('Use this form to request a password reset. We will send an email to the given address with instructions to reset your password.') }}</p>
+          <div class="field">
+            <label>{{ $t('Account\'s email') }}</label>
+            <input
+              required
+              ref="email"
+              type="email"
+              autofocus
+              :placeholder="$t('Input the email address binded to your account')"
+              v-model="email">
+          </div>
+          <router-link :to="{path: '/login'}">
+            {{ $t('Back to login') }}
+          </router-link>
+          <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
+            {{ $t('Ask for a password reset') }}</button>
+        </form>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  props: ['defaultEmail'],
+  data () {
+    return {
+      email: this.defaultEmail,
+      isLoading: false,
+      errors: []
+    }
+  },
+  mounted () {
+    this.$refs.email.focus()
+  },
+  methods: {
+    submit () {
+      let self = this
+      self.isLoading = true
+      self.errors = []
+      let payload = {
+        email: this.email
+      }
+      return axios.post('auth/password/reset/', payload).then(response => {
+        self.isLoading = false
+        self.$router.push({
+          name: 'auth.password-reset-confirm'
+        })
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/auth/PasswordResetConfirm.vue b/front/src/views/auth/PasswordResetConfirm.vue
new file mode 100644
index 00000000..d2919249
--- /dev/null
+++ b/front/src/views/auth/PasswordResetConfirm.vue
@@ -0,0 +1,85 @@
+<template>
+  <div class="main pusher" v-title="$t('Change your password')">
+    <div class="ui vertical stripe segment">
+      <div class="ui small text container">
+        <h2>{{ $t('Change your password') }}</h2>
+        <form v-if="!success" class="ui form" @submit.prevent="submit()">
+          <div v-if="errors.length > 0" class="ui negative message">
+            <div class="header">{{ $('Error while changing your password') }}</div>
+            <ul class="list">
+              <li v-for="error in errors">{{ error }}</li>
+            </ul>
+          </div>
+          <template v-if="token && uid">
+            <div class="field">
+              <label>{{ $t('New password') }}</label>
+              <password-input v-model="newPassword" />
+            </div>
+            <router-link :to="{path: '/login'}">
+              {{ $t('Back to login') }}
+            </router-link>
+            <button :class="['ui', {'loading': isLoading}, 'right', 'floated', 'green', 'button']" type="submit">
+              {{ $t('Update your password') }}</button>
+          </template>
+          <template v-else>
+            <p>{{ $t('If the email address provided in the previous step is valid and binded to a user account, you should receive an email with reset instructions in the next couple of minutes.') }}</p>
+          </template>
+        </form>
+        <div v-else class="ui positive message">
+          <div class="header">{{ $t('Password updated successfully') }}</div>
+          <p>{{ $t('Your password has been updated successfully.') }}</p>
+          <router-link :to="{name: 'login'}">
+            {{ $t('Proceed to login') }}
+          </router-link>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import PasswordInput from '@/components/forms/PasswordInput'
+
+export default {
+  props: ['defaultToken', 'defaultUid'],
+  components: {
+    PasswordInput
+  },
+  data () {
+    return {
+      newPassword: '',
+      isLoading: false,
+      errors: [],
+      token: this.defaultToken,
+      uid: this.defaultUid,
+      success: false
+    }
+  },
+  methods: {
+    submit () {
+      let self = this
+      self.isLoading = true
+      self.errors = []
+      let payload = {
+        uid: this.uid,
+        token: this.token,
+        new_password1: this.newPassword,
+        new_password2: this.newPassword
+      }
+      return axios.post('auth/password/reset/confirm/', payload).then(response => {
+        self.isLoading = false
+        self.success = true
+      }, error => {
+        self.errors = error.backendErrors
+        self.isLoading = false
+      })
+    }
+  }
+
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
-- 
GitLab