mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-15 23:47:33 +08:00
support reset password by email reset link
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
const defaultTimeout = 10000; // 10s
|
||||
const requestForgetPasswordTimeout = 30000; // 30s
|
||||
const baseApiUrlPath = '/api';
|
||||
const baseQrcodePath = '/qrcode';
|
||||
const baseProxyUrlPath = '/proxy';
|
||||
@@ -7,6 +9,8 @@ const baiduMapJavascriptUrl = 'https://api.map.baidu.com/api?v=3.0';
|
||||
const amapJavascriptUrl = 'https://webapi.amap.com/maps?v=2.0';
|
||||
|
||||
export default {
|
||||
defaultTimeout: defaultTimeout,
|
||||
requestForgetPasswordTimeout: requestForgetPasswordTimeout,
|
||||
baseApiUrlPath: baseApiUrlPath,
|
||||
baseQrcodePath: baseQrcodePath,
|
||||
baseProxyUrlPath: baseProxyUrlPath,
|
||||
|
||||
@@ -33,6 +33,10 @@ export function isUserRegistrationEnabled() {
|
||||
return getServerSetting('r') === '1';
|
||||
}
|
||||
|
||||
export function isUserForgetPasswordEnabled() {
|
||||
return getServerSetting('f') === '1';
|
||||
}
|
||||
|
||||
export function isDataExportingEnabled() {
|
||||
return getServerSetting('e') === '1';
|
||||
}
|
||||
|
||||
+16
-1
@@ -13,7 +13,7 @@ let needBlockRequest = false;
|
||||
let blockedRequests = [];
|
||||
|
||||
axios.defaults.baseURL = api.baseApiUrlPath;
|
||||
axios.defaults.timeout = 10000; // 10s
|
||||
axios.defaults.timeout = api.defaultTimeout;
|
||||
axios.interceptors.request.use(config => {
|
||||
const token = userState.getToken();
|
||||
|
||||
@@ -102,6 +102,21 @@ export default {
|
||||
firstDayOfWeek
|
||||
});
|
||||
},
|
||||
requestResetPassword: ({ email }) => {
|
||||
return axios.post('forget_password/request.json', {
|
||||
email
|
||||
}, {
|
||||
timeout: api.requestForgetPasswordTimeout
|
||||
});
|
||||
},
|
||||
resetPassword: ({ email, token, password }) => {
|
||||
return axios.post('forget_password/reset/by_token.json?token=' + token, {
|
||||
email,
|
||||
password
|
||||
}, {
|
||||
ignoreError: true
|
||||
});
|
||||
},
|
||||
logout: () => {
|
||||
return axios.get('logout.json');
|
||||
},
|
||||
|
||||
@@ -558,6 +558,7 @@ export default {
|
||||
'api not found': 'Failed to request api',
|
||||
'not implemented': 'Not implemented',
|
||||
'database operation failed': 'Database operation failed',
|
||||
'smtp server is not enabled': 'Smtp server is not enabled',
|
||||
'incomplete or incorrect submission': 'Incomplete or incorrect submission',
|
||||
'operation failed': 'Operation failed',
|
||||
'nothing will be updated': 'Nothing will be updated',
|
||||
@@ -580,6 +581,9 @@ export default {
|
||||
'login name or password is invalid': 'Login name or password is invalid',
|
||||
'login name or password is wrong': 'Login name or password is wrong',
|
||||
'user is disabled': 'User is disabled',
|
||||
'email is invalid': 'Email is invalid',
|
||||
'email is empty or invalid': 'Email is empty or invalid',
|
||||
'new password equals old password': 'New password equals old password',
|
||||
'unauthorized access': 'Unauthorized access',
|
||||
'current token is invalid': 'Current token is invalid',
|
||||
'current token is expired': 'Current token is expired',
|
||||
@@ -592,6 +596,7 @@ export default {
|
||||
'token is not found': 'Token is not found',
|
||||
'token is expired': 'Token is expired',
|
||||
'token is empty': 'Token is empty',
|
||||
'password reset token is invalid or expired': 'Password reset token is invalid or expired',
|
||||
'passcode is invalid': 'Passcode is invalid',
|
||||
'two factor backup code is invalid': 'Two factor backup code is invalid',
|
||||
'two factor is not enabled': 'Two factor is not enabled',
|
||||
@@ -812,7 +817,9 @@ export default {
|
||||
'This year or later': 'This year or later',
|
||||
'Log In': 'Log In',
|
||||
'Click here to log in': 'Click here to log in',
|
||||
'Back to log in': 'Back to log in',
|
||||
'Don\'t have an account?': 'Don\'t have an account?',
|
||||
'Forget Password?': 'Forget Password?',
|
||||
'Create an account': 'Create an account',
|
||||
'Username cannot be empty': 'Username cannot be empty',
|
||||
'Password cannot be empty': 'Password cannot be empty',
|
||||
@@ -839,6 +846,15 @@ export default {
|
||||
'Use a passcode': 'Use a passcode',
|
||||
'PIN code is invalid': 'PIN code is invalid',
|
||||
'PIN code is wrong': 'PIN code is wrong',
|
||||
'Send Reset Link': 'Send Reset Link',
|
||||
'Please input your email address used for registration and we\'ll send you an email with reset password link': 'Please input your email address used for registration and we\'ll send you an email with reset password link',
|
||||
'Password reset email has been sent': 'Password reset email has been sent',
|
||||
'Unable to send password reset email': 'Unable to send password reset email',
|
||||
'Reset Password': 'Reset Password',
|
||||
'Update Password': 'Update Password',
|
||||
'Please input your email address again, and input the new password.': 'Please input your email address again, and input the new password.',
|
||||
'Password has been updated': 'Password has been updated',
|
||||
'Unable to reset password': 'Unable to reset password',
|
||||
'Sign Up': 'Sign Up',
|
||||
'Overview': 'Overview',
|
||||
'Asset Summary': 'Asset Summary',
|
||||
|
||||
@@ -558,6 +558,7 @@ export default {
|
||||
'api not found': '接口调用失败',
|
||||
'not implemented': '未实现',
|
||||
'database operation failed': '数据库操作失败',
|
||||
'smtp server is not enabled': 'Smtp 服务器没有启用',
|
||||
'incomplete or incorrect submission': '提交不完整或不正确',
|
||||
'operation failed': '操作失败',
|
||||
'nothing will be updated': '没有内容更新',
|
||||
@@ -580,6 +581,9 @@ export default {
|
||||
'login name or password is invalid': '登录名或密码无效',
|
||||
'login name or password is wrong': '登录名或密码错误',
|
||||
'user is disabled': '用户已禁用',
|
||||
'email is invalid': '邮箱无效',
|
||||
'email is empty or invalid': '邮箱为空或无效',
|
||||
'new password equals old password': '新密码与旧密码相同',
|
||||
'unauthorized access': '未授权的登录',
|
||||
'current token is invalid': '当前认证令牌无效',
|
||||
'current token is expired': '当前认证令牌已过期',
|
||||
@@ -592,6 +596,7 @@ export default {
|
||||
'token is not found': '认证令牌不存在',
|
||||
'token is expired': '认证令牌已过期',
|
||||
'token is empty': '认证令牌为空',
|
||||
'password reset token is invalid or expired': '密码重置令牌无效或已过期',
|
||||
'passcode is invalid': '验证码无效',
|
||||
'two factor backup code is invalid': '两步验证备用码无效',
|
||||
'two factor is not enabled': '两步验证没有启用',
|
||||
@@ -812,7 +817,9 @@ export default {
|
||||
'This year or later': '今年或更晚',
|
||||
'Log In': '登录',
|
||||
'Click here to log in': '点击这里登录',
|
||||
'Back to log in': '返回登录页',
|
||||
'Don\'t have an account?': '还没有账号?',
|
||||
'Forget Password?': '忘记密码?',
|
||||
'Create an account': '创建新账号',
|
||||
'Username cannot be empty': '用户名不能为空',
|
||||
'Password cannot be empty': '密码不能为空',
|
||||
@@ -839,6 +846,15 @@ export default {
|
||||
'Use a passcode': '使用验证码',
|
||||
'PIN code is invalid': 'PIN码无效',
|
||||
'PIN code is wrong': 'PIN码错误',
|
||||
'Send Reset Link': '发送重置链接',
|
||||
'Please input your email address used for registration and we\'ll send you an email with reset password link': '请输入您注册时使用的电子邮箱地址,我们将发送一封包含重置密码链接的邮件给您',
|
||||
'Password reset email has been sent': '重置密码邮件已发送',
|
||||
'Unable to send password reset email': '无法发送重置密码邮件',
|
||||
'Reset Password': '重置密码',
|
||||
'Update Password': '更新密码',
|
||||
'Please input your email address again, and input the new password.': '请再次输入您的邮箱,然后输入新的密码。',
|
||||
'Password has been updated': '密码已经更新',
|
||||
'Unable to reset password': '无法重置密码',
|
||||
'Sign Up': '注册',
|
||||
'Overview': '总览',
|
||||
'Asset Summary': '资产概要',
|
||||
|
||||
@@ -5,6 +5,8 @@ import userState from '@/lib/userstate.js';
|
||||
import MainLayout from '@/views/desktop/MainLayout.vue';
|
||||
import LoginPage from '@/views/desktop/LoginPage.vue';
|
||||
import SignUpPage from '@/views/desktop/SignupPage.vue';
|
||||
import ForgetPasswordPage from '@/views/desktop/ForgetPasswordPage.vue';
|
||||
import ResetPasswordPage from '@/views/desktop/ResetPasswordPage.vue';
|
||||
import UnlockPage from '@/views/desktop/UnlockPage.vue';
|
||||
|
||||
import HomePage from '@/views/desktop/HomePage.vue';
|
||||
@@ -157,6 +159,18 @@ const router = createRouter({
|
||||
component: SignUpPage,
|
||||
beforeEnter: checkNotLogin
|
||||
},
|
||||
{
|
||||
path: '/forgetpassword',
|
||||
component: ForgetPasswordPage,
|
||||
beforeEnter: checkNotLogin
|
||||
},
|
||||
{
|
||||
path: '/resetpassword',
|
||||
component: ResetPasswordPage,
|
||||
props: route => ({
|
||||
token: route.query.token
|
||||
})
|
||||
},
|
||||
{
|
||||
path: '/unlock',
|
||||
component: UnlockPage,
|
||||
|
||||
@@ -247,6 +247,60 @@ export const useRootStore = defineStore('root', {
|
||||
userState.clearWebAuthnConfig();
|
||||
this.resetAllStates(true);
|
||||
},
|
||||
requestResetPassword({ email }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.requestResetPassword({
|
||||
email
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to send password reset email' });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to send password reset email', error);
|
||||
|
||||
if (error && error.processed) {
|
||||
reject(error);
|
||||
} else if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else {
|
||||
reject({ message: 'Unable to send password reset email' });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
resetPassword({ email, token, password }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.resetPassword({
|
||||
token,
|
||||
email,
|
||||
password
|
||||
}).then(response => {
|
||||
const data = response.data;
|
||||
|
||||
if (!data || !data.success || !data.result) {
|
||||
reject({ message: 'Unable to reset password' });
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data.result);
|
||||
}).catch(error => {
|
||||
logger.error('failed to reset password', error);
|
||||
|
||||
if (error && error.processed) {
|
||||
reject(error);
|
||||
} else if (error.response && error.response.data && error.response.data.errorMessage) {
|
||||
reject({ error: error.response.data });
|
||||
} else {
|
||||
reject({ message: 'Unable to reset password' });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
updateUserProfile({ profile, currentPassword }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
services.updateProfile({
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="layout-wrapper">
|
||||
<router-link to="/">
|
||||
<div class="auth-logo d-flex align-start gap-x-3">
|
||||
<img alt="logo" class="login-page-logo" :src="ezBookkeepingLogoPath" />
|
||||
<h1 class="font-weight-medium leading-normal text-2xl">{{ $t('global.app.title') }}</h1>
|
||||
</div>
|
||||
</router-link>
|
||||
<v-row no-gutters class="auth-wrapper">
|
||||
<v-col cols="12" md="8" class="d-none d-md-flex align-center justify-center position-relative">
|
||||
<div class="d-flex auth-img-footer" v-if="!isDarkMode">
|
||||
<v-img src="img/desktop/background.svg"/>
|
||||
</div>
|
||||
<div class="d-flex auth-img-footer" v-if="isDarkMode">
|
||||
<v-img src="img/desktop/background-dark.svg"/>
|
||||
</div>
|
||||
<div class="d-flex align-center justify-center w-100 pt-10">
|
||||
<v-img max-width="600px" src="img/desktop/people4.svg"/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="auth-card d-flex flex-column">
|
||||
<div class="d-flex align-center justify-center h-100">
|
||||
<v-card variant="flat" class="w-100 mt-0 px-4 pt-12" max-width="500">
|
||||
<v-card-text>
|
||||
<h5 class="text-h5 mb-3">{{ $t('Forget Password?') }}</h5>
|
||||
<p class="mb-0">{{ $t('Please input your email address used for registration and we\'ll send you an email with reset password link') }}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="pb-0 mb-6">
|
||||
<v-form>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
autofocus="autofocus"
|
||||
clearable
|
||||
:disabled="requesting"
|
||||
:label="$t('E-mail')"
|
||||
:placeholder="$t('Your email address')"
|
||||
v-model="email"
|
||||
@keyup.enter="requestResetPassword"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-btn block type="submit" :disabled="!email || requesting" @click="requestResetPassword">
|
||||
{{ $t('Send Reset Link') }}
|
||||
<v-progress-circular indeterminate size="24" class="ml-2" v-if="requesting"></v-progress-circular>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<router-link class="d-flex align-center justify-center" to="/login"
|
||||
:class="{ 'disabled': requesting }">
|
||||
<v-icon :icon="icons.left"/>
|
||||
<span>{{ $t('Back to log in') }}</span>
|
||||
</router-link>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
<v-spacer/>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<v-card variant="flat" class="w-100 px-4 pb-4" max-width="500">
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-menu location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn variant="text"
|
||||
:disabled="requesting"
|
||||
v-bind="props">{{ currentLanguageName }}</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="(lang, locale) in allLanguages" :key="locale">
|
||||
<v-list-item-title
|
||||
class="cursor-pointer"
|
||||
@click="changeLanguage(locale)">
|
||||
{{ lang.displayName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="d-flex align-center pt-0">
|
||||
<v-divider />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="text-center text-sm">
|
||||
<span>Powered by </span>
|
||||
<a href="https://github.com/mayswind/ezbookkeeping" target="_blank">ezBookkeeping</a> <span>{{ version }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useTheme } from 'vuetify';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useRootStore } from '@/stores/index.js';
|
||||
import { useSettingsStore } from '@/stores/setting.js';
|
||||
|
||||
import assetConstants from '@/consts/asset.js';
|
||||
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
requesting: false,
|
||||
icons: {
|
||||
left: mdiChevronLeft
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore),
|
||||
ezBookkeepingLogoPath() {
|
||||
return assetConstants.ezBookkeepingLogoPath;
|
||||
},
|
||||
version() {
|
||||
return 'v' + this.$version;
|
||||
},
|
||||
allLanguages() {
|
||||
return this.$locale.getAllLanguageInfos();
|
||||
},
|
||||
isDarkMode() {
|
||||
return this.globalTheme.global.name.value === 'dark';
|
||||
},
|
||||
currentLanguageName() {
|
||||
return this.$locale.getCurrentLanguageDisplayName();
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
|
||||
return {
|
||||
globalTheme: theme
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
requestResetPassword() {
|
||||
const self = this;
|
||||
|
||||
if (!self.email) {
|
||||
self.$refs.snackbar.showMessage('Email address cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
self.requesting = true;
|
||||
|
||||
self.rootStore.requestResetPassword({
|
||||
email: self.email
|
||||
}).then(() => {
|
||||
self.requesting = false;
|
||||
self.$refs.snackbar.showMessage('Password reset email has been sent');
|
||||
}).catch(error => {
|
||||
self.requesting = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
changeLanguage(locale) {
|
||||
const localeDefaultSettings = this.$locale.setLanguage(locale);
|
||||
this.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -95,6 +95,11 @@
|
||||
<a href="javascript:void(0);" @click="showMobileQrCode = true">
|
||||
<span class="nav-item-title">{{ $t('Use on Mobile Device') }}</span>
|
||||
</a>
|
||||
<v-spacer/>
|
||||
<router-link class="text-primary" to="/forgetpassword"
|
||||
:class="{'disabled': !isUserForgetPasswordEnabled}">
|
||||
{{ $t('Forget Password?') }}
|
||||
</router-link>
|
||||
</div>
|
||||
</v-col>
|
||||
|
||||
@@ -176,7 +181,7 @@ import { useSettingsStore } from '@/stores/setting.js';
|
||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
||||
|
||||
import assetConstants from '@/consts/asset.js';
|
||||
import { isUserRegistrationEnabled } from '@/lib/server_settings.js';
|
||||
import { isUserRegistrationEnabled, isUserForgetPasswordEnabled } from '@/lib/server_settings.js';
|
||||
|
||||
import {
|
||||
mdiEyeOutline,
|
||||
@@ -221,6 +226,9 @@ export default {
|
||||
isUserRegistrationEnabled() {
|
||||
return isUserRegistrationEnabled();
|
||||
},
|
||||
isUserForgetPasswordEnabled() {
|
||||
return isUserForgetPasswordEnabled();
|
||||
},
|
||||
inputIsEmpty() {
|
||||
return !this.username || !this.password;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="layout-wrapper">
|
||||
<router-link to="/">
|
||||
<div class="auth-logo d-flex align-start gap-x-3">
|
||||
<img alt="logo" class="login-page-logo" :src="ezBookkeepingLogoPath" />
|
||||
<h1 class="font-weight-medium leading-normal text-2xl">{{ $t('global.app.title') }}</h1>
|
||||
</div>
|
||||
</router-link>
|
||||
<v-row no-gutters class="auth-wrapper">
|
||||
<v-col cols="12" md="8" class="d-none d-md-flex align-center justify-center position-relative">
|
||||
<div class="d-flex auth-img-footer" v-if="!isDarkMode">
|
||||
<v-img src="img/desktop/background.svg"/>
|
||||
</div>
|
||||
<div class="d-flex auth-img-footer" v-if="isDarkMode">
|
||||
<v-img src="img/desktop/background-dark.svg"/>
|
||||
</div>
|
||||
<div class="d-flex align-center justify-center w-100 pt-10">
|
||||
<v-img max-width="600px" src="img/desktop/people4.svg"/>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4" class="auth-card d-flex flex-column">
|
||||
<div class="d-flex align-center justify-center h-100">
|
||||
<v-card variant="flat" class="w-100 mt-0 px-4 pt-12" max-width="500">
|
||||
<v-card-text>
|
||||
<h5 class="text-h5 mb-3">{{ $t('Reset Password') }}</h5>
|
||||
<p class="mb-0">{{ $t('Please input your email address again, and input the new password.') }}</p>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text class="pb-0 mb-6">
|
||||
<v-form>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
autofocus="autofocus"
|
||||
clearable
|
||||
:disabled="updating"
|
||||
:label="$t('E-mail')"
|
||||
:placeholder="$t('Your email address')"
|
||||
v-model="email"
|
||||
@keyup.enter="$refs.passwordInput.focus()"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
autocomplete="new-password"
|
||||
clearable
|
||||
ref="passwordInput"
|
||||
:type="isNewPasswordVisible ? 'text' : 'password'"
|
||||
:disabled="updating"
|
||||
:label="$t('Password')"
|
||||
:placeholder="$t('Your password')"
|
||||
:append-inner-icon="isNewPasswordVisible ? icons.eyeSlash : icons.eye"
|
||||
v-model="newPassword"
|
||||
@click:append-inner="isNewPasswordVisible = !isNewPasswordVisible"
|
||||
@keyup.enter="$refs.confirmPasswordInput.focus()"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
clearable
|
||||
ref="confirmPasswordInput"
|
||||
:type="isConfirmPasswordVisible ? 'text' : 'password'"
|
||||
:disabled="updating"
|
||||
:label="$t('Confirmation Password')"
|
||||
:placeholder="$t('Re-enter the password')"
|
||||
:append-inner-icon="isConfirmPasswordVisible ? icons.eyeSlash : icons.eye"
|
||||
v-model="confirmPassword"
|
||||
@click:append-inner="isConfirmPasswordVisible = !isConfirmPasswordVisible"
|
||||
@keyup.enter="resetPassword"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-btn block :disabled="!email || !newPassword || !confirmPassword || updating" @click="resetPassword">
|
||||
{{ $t('Update Password') }}
|
||||
<v-progress-circular indeterminate size="24" class="ml-2" v-if="updating"></v-progress-circular>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<router-link class="d-flex align-center justify-center" to="/login"
|
||||
:class="{ 'disabled': updating }">
|
||||
<v-icon :icon="icons.left"/>
|
||||
<span>{{ $t('Back to log in') }}</span>
|
||||
</router-link>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
<v-spacer/>
|
||||
<div class="d-flex align-center justify-center">
|
||||
<v-card variant="flat" class="w-100 px-4 pb-4" max-width="500">
|
||||
<v-card-text class="pt-0">
|
||||
<v-row>
|
||||
<v-col cols="12" class="text-center">
|
||||
<v-menu location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn variant="text"
|
||||
:disabled="updating"
|
||||
v-bind="props">{{ currentLanguageName }}</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item v-for="(lang, locale) in allLanguages" :key="locale">
|
||||
<v-list-item-title
|
||||
class="cursor-pointer"
|
||||
@click="changeLanguage(locale)">
|
||||
{{ lang.displayName }}
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="d-flex align-center pt-0">
|
||||
<v-divider />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" class="text-center text-sm">
|
||||
<span>Powered by </span>
|
||||
<a href="https://github.com/mayswind/ezbookkeeping" target="_blank">ezBookkeeping</a> <span>{{ version }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<confirm-dialog ref="confirmDialog"/>
|
||||
<snack-bar ref="snackbar" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useTheme } from 'vuetify';
|
||||
|
||||
import { mapStores } from 'pinia';
|
||||
import { useRootStore } from '@/stores/index.js';
|
||||
import { useSettingsStore } from '@/stores/setting.js';
|
||||
|
||||
import assetConstants from '@/consts/asset.js';
|
||||
|
||||
import {
|
||||
mdiChevronLeft,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline
|
||||
} from '@mdi/js';
|
||||
|
||||
export default {
|
||||
props: [
|
||||
'token'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
newPassword: '',
|
||||
confirmPassword: '',
|
||||
isNewPasswordVisible: false,
|
||||
isConfirmPasswordVisible: false,
|
||||
updating: false,
|
||||
icons: {
|
||||
left: mdiChevronLeft,
|
||||
eye: mdiEyeOutline,
|
||||
eyeSlash: mdiEyeOffOutline
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore),
|
||||
inputProblemMessage() {
|
||||
if (!this.email) {
|
||||
return 'Email address cannot be empty';
|
||||
} else if (!this.newPassword && !this.confirmPassword) {
|
||||
return 'Nothing has been modified';
|
||||
} else if (!this.newPassword && this.confirmPassword) {
|
||||
return 'New password cannot be empty';
|
||||
} else if (this.newPassword && !this.confirmPassword) {
|
||||
return 'Confirmation password cannot be empty';
|
||||
} else if (this.newPassword && this.confirmPassword && this.newPassword !== this.confirmPassword) {
|
||||
return 'Password and confirmation password do not match';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
ezBookkeepingLogoPath() {
|
||||
return assetConstants.ezBookkeepingLogoPath;
|
||||
},
|
||||
version() {
|
||||
return 'v' + this.$version;
|
||||
},
|
||||
allLanguages() {
|
||||
return this.$locale.getAllLanguageInfos();
|
||||
},
|
||||
isDarkMode() {
|
||||
return this.globalTheme.global.name.value === 'dark';
|
||||
},
|
||||
currentLanguageName() {
|
||||
return this.$locale.getCurrentLanguageDisplayName();
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const theme = useTheme();
|
||||
|
||||
return {
|
||||
globalTheme: theme
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
resetPassword() {
|
||||
const self = this;
|
||||
|
||||
const problemMessage = self.inputProblemMessage;
|
||||
|
||||
if (problemMessage) {
|
||||
self.$refs.snackbar.showMessage(problemMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
self.updating = true;
|
||||
|
||||
self.rootStore.resetPassword({
|
||||
token: self.token,
|
||||
email: self.email,
|
||||
password: self.newPassword
|
||||
}).then(() => {
|
||||
self.updating = false;
|
||||
self.$refs.snackbar.showMessage('Password has been updated');
|
||||
}).catch(error => {
|
||||
self.updating = false;
|
||||
|
||||
if (!error.processed) {
|
||||
self.$refs.snackbar.showError(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
changeLanguage(locale) {
|
||||
const localeDefaultSettings = this.$locale.setLanguage(locale);
|
||||
this.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -35,6 +35,9 @@
|
||||
</small>
|
||||
</template>
|
||||
<template #after>
|
||||
<small>
|
||||
<f7-link :class="{'disabled': !isUserForgetPasswordEnabled}" @click="showForgetPasswordSheet = true">{{ $t('Forget Password?') }}</f7-link>
|
||||
</small>
|
||||
</template>
|
||||
</f7-list-item>
|
||||
</f7-list>
|
||||
@@ -127,6 +130,37 @@
|
||||
</div>
|
||||
</f7-page-content>
|
||||
</f7-sheet>
|
||||
|
||||
<f7-sheet
|
||||
style="height:auto"
|
||||
:opened="showForgetPasswordSheet" @sheet:closed="showForgetPasswordSheet = false"
|
||||
>
|
||||
<f7-page-content>
|
||||
<div class="display-flex padding justify-content-space-between align-items-center">
|
||||
<div class="ebk-sheet-title"><b>{{ $t('Forget Password?') }}</b></div>
|
||||
</div>
|
||||
<div class="padding-horizontal padding-bottom">
|
||||
<p class="no-margin">
|
||||
<span>{{ $t('Please input your email address used for registration and we\'ll send you an email with reset password link') }}</span>
|
||||
</p>
|
||||
<f7-list strong class="no-margin">
|
||||
<f7-list-input
|
||||
type="email"
|
||||
autocomplete="email"
|
||||
outline
|
||||
floating-label
|
||||
clear-button
|
||||
class="no-margin no-padding-bottom"
|
||||
:label="$t('E-mail')"
|
||||
:placeholder="$t('Your email address')"
|
||||
v-model:value="forgetPasswordEmail"
|
||||
@keyup.enter="requestResetPassword"
|
||||
></f7-list-input>
|
||||
</f7-list>
|
||||
<f7-button large fill :class="{ 'disabled': !forgetPasswordEmail || requestingForgetPassword }" :text="$t('Send Reset Link')" @click="requestResetPassword"></f7-button>
|
||||
</div>
|
||||
</f7-page-content>
|
||||
</f7-sheet>
|
||||
</f7-page>
|
||||
</template>
|
||||
|
||||
@@ -137,7 +171,7 @@ import { useSettingsStore } from '@/stores/setting.js';
|
||||
import { useExchangeRatesStore } from '@/stores/exchangeRates.js';
|
||||
|
||||
import assetConstants from '@/consts/asset.js';
|
||||
import { isUserRegistrationEnabled } from '@/lib/server_settings.js';
|
||||
import { isUserRegistrationEnabled, isUserForgetPasswordEnabled } from '@/lib/server_settings.js';
|
||||
import { getDesktopVersionPath } from '@/lib/version.js';
|
||||
import { isModalShowing } from '@/lib/ui.mobile.js';
|
||||
|
||||
@@ -152,9 +186,12 @@ export default {
|
||||
passcode: '',
|
||||
backupCode: '',
|
||||
tempToken: '',
|
||||
forgetPasswordEmail: '',
|
||||
logining: false,
|
||||
verifying: false,
|
||||
requestingForgetPassword: false,
|
||||
show2faSheet: false,
|
||||
showForgetPasswordSheet: false,
|
||||
twoFAVerifyType: 'passcode'
|
||||
};
|
||||
},
|
||||
@@ -175,6 +212,9 @@ export default {
|
||||
isUserRegistrationEnabled() {
|
||||
return isUserRegistrationEnabled();
|
||||
},
|
||||
isUserForgetPasswordEnabled() {
|
||||
return isUserForgetPasswordEnabled();
|
||||
},
|
||||
inputIsEmpty() {
|
||||
return !this.username || !this.password;
|
||||
},
|
||||
@@ -308,6 +348,33 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
requestResetPassword() {
|
||||
const self = this;
|
||||
|
||||
if (!self.forgetPasswordEmail) {
|
||||
self.$alert('Email address cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
self.requestingForgetPassword = true;
|
||||
self.$showLoading(() => self.requestingForgetPassword);
|
||||
|
||||
self.rootStore.requestResetPassword({
|
||||
email: self.forgetPasswordEmail
|
||||
}).then(() => {
|
||||
self.requestingForgetPassword = false;
|
||||
self.$hideLoading();
|
||||
|
||||
self.$toast('Password reset email has been sent');
|
||||
}).catch(error => {
|
||||
self.requestingForgetPassword = false;
|
||||
self.$hideLoading();
|
||||
|
||||
if (!error.processed) {
|
||||
self.$toast(error.message || error);
|
||||
}
|
||||
});
|
||||
},
|
||||
switch2FAVerifyType() {
|
||||
if (this.twoFAVerifyType === 'passcode') {
|
||||
this.twoFAVerifyType = 'backupcode';
|
||||
|
||||
Reference in New Issue
Block a user