diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 5a94194c29..5cb2acba1d 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -246,6 +246,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) + store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) diff --git a/src/boot/routes.js b/src/boot/routes.js index 7dc4b2a5da..cd02711cc3 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -9,6 +9,7 @@ import UserProfile from 'components/user_profile/user_profile.vue' import Search from 'components/search/search.vue' import Settings from 'components/settings/settings.vue' import Registration from 'components/registration/registration.vue' +import PasswordReset from 'components/password_reset/password_reset.vue' import UserSettings from 'components/user_settings/user_settings.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' @@ -46,6 +47,7 @@ export default (store) => { { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, + { name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index 3ec7fe0c19..b4fdcefbf0 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -33,6 +33,11 @@ type="password" > +
+ + {{ $t('password_reset.forgot_password') }} + +
({ + user: { + email: '' + }, + isPending: false, + success: false, + throttled: false, + error: null + }), + computed: { + ...mapState({ + signedIn: (state) => !!state.users.currentUser, + instance: state => state.instance + }), + mailerEnabled () { + return this.instance.mailerEnabled + } + }, + created () { + if (this.signedIn) { + this.$router.push({ name: 'root' }) + } + }, + methods: { + dismissError () { + this.error = null + }, + submit () { + this.isPending = true + const email = this.user.email + const instance = this.instance.server + + passwordResetApi({ instance, email }).then(({ status }) => { + this.isPending = false + this.user.email = '' + + if (status === 204) { + this.success = true + this.error = null + } else if (status === 404 || status === 400) { + this.error = this.$t('password_reset.not_found') + this.$nextTick(() => { + this.$refs.email.focus() + }) + } else if (status === 429) { + this.throttled = true + this.error = this.$t('password_reset.too_many_requests') + } + }).catch(() => { + this.isPending = false + this.user.email = '' + this.error = this.$t('general.generic_error') + }) + } + } +} + +export default passwordReset diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue new file mode 100644 index 0000000000..00474e95d1 --- /dev/null +++ b/src/components/password_reset/password_reset.vue @@ -0,0 +1,116 @@ + + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 6a9af55c85..ddde471ab8 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -608,5 +608,16 @@ "person_talking": "{count} person talking", "people_talking": "{count} people talking", "no_results": "No results" + }, + "password_reset": { + "forgot_password": "Forgot password?", + "password_reset": "Password reset", + "instruction": "Enter your email address or username. We will send you a link to reset your password.", + "placeholder": "Your email or username", + "check_email": "Check your email for a link to reset your password.", + "return_home": "Return to the home page", + "not_found": "We couldn't find that email or username.", + "too_many_requests": "You have reached the limit of attempts, try again later.", + "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 90ed66643a..3af65f400c 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -389,5 +389,16 @@ "person_talking": "Популярно у {count} человека", "people_talking": "Популярно у {count} человек", "no_results": "Ничего не найдено" + }, + "password_reset": { + "forgot_password": "Забыли пароль?", + "password_reset": "Сброс пароля", + "instruction": "Введите ваш email или имя пользователя, и мы отправим вам ссылку для сброса пароля.", + "placeholder": "Ваш email или имя пользователя", + "check_email": "Проверьте ваш email и перейдите по ссылке для сброса пароля.", + "return_home": "Вернуться на главную страницу", + "not_found": "Мы не смогли найти аккаунт с таким email-ом или именем пользователя.", + "too_many_requests": "Вы исчерпали допустимое количество попыток, попробуйте позже.", + "password_reset_disabled": "Сброс пароля отключен. Cвяжитесь с администратором вашего сервера." } } diff --git a/src/services/new_api/password_reset.js b/src/services/new_api/password_reset.js new file mode 100644 index 0000000000..4319962503 --- /dev/null +++ b/src/services/new_api/password_reset.js @@ -0,0 +1,18 @@ +import { reduce } from 'lodash' + +const MASTODON_PASSWORD_RESET_URL = `/auth/password` + +const resetPassword = ({ instance, email }) => { + const params = { email } + const query = reduce(params, (acc, v, k) => { + const encoded = `${k}=${encodeURIComponent(v)}` + return `${acc}&${encoded}` + }, '') + const url = `${instance}${MASTODON_PASSWORD_RESET_URL}?${query}` + + return window.fetch(url, { + method: 'POST' + }) +} + +export default resetPassword