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