add email verification

This commit is contained in:
MaysWind
2023-09-03 23:45:12 +08:00
parent c38b277887
commit e2b81f7b57
35 changed files with 931 additions and 35 deletions
+4
View File
@@ -37,6 +37,10 @@ export function isUserForgetPasswordEnabled() {
return getServerSetting('f') === '1';
}
export function isUserVerifyEmailEnabled() {
return getServerSetting('v') === '1';
}
export function isDataExportingEnabled() {
return getServerSetting('e') === '1';
}
+21
View File
@@ -102,6 +102,22 @@ export default {
firstDayOfWeek
});
},
verifyEmail: ({ token, requestNewToken }) => {
return axios.post('verify_email/by_token.json?token=' + token, {
requestNewToken
}, {
noAuth: true,
ignoreError: true
});
},
resendVerifyEmailByUnloginUser: ({ email, password }) => {
return axios.post('verify_email/resend.json', {
email,
password
}, {
timeout: api.requestForgetPasswordTimeout
});
},
requestResetPassword: ({ email }) => {
return axios.post('forget_password/request.json', {
email
@@ -173,6 +189,11 @@ export default {
shortTimeFormat
});
},
resendVerifyEmailByLoginedUser: () => {
return axios.post('v1/users/verify_email/resend.json', {}, {
timeout: api.requestForgetPasswordTimeout
});
},
get2FAStatus: () => {
return axios.get('v1/users/2fa/status.json');
},
+12 -1
View File
@@ -585,6 +585,7 @@ export default {
'email is empty or invalid': 'Email is empty or invalid',
'new password equals old password': 'New password equals old password',
'email is not verified': 'Email is not verified',
'email is verified': 'Email is verified',
'unauthorized access': 'Unauthorized access',
'current token is invalid': 'Current token is invalid',
'current token is expired': 'Current token is expired',
@@ -597,6 +598,7 @@ export default {
'token is not found': 'Token is not found',
'token is expired': 'Token is expired',
'token is empty': 'Token is empty',
'email verify token is invalid or expired': 'Email verify token is invalid or expired',
'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',
@@ -847,6 +849,14 @@ export default {
'Use a passcode': 'Use a passcode',
'PIN code is invalid': 'PIN code is invalid',
'PIN code is wrong': 'PIN code is wrong',
'Verify your email': 'Verify your email',
'Verifying...': 'Verifying...',
'Account activation link has been sent to your email address:': 'Account activation link has been sent to your email address:',
', If you don\'t receive the mail, fill password and click the button below to resend the verify mail.': ', If you don\'t receive the mail, fill password and click the button below to resend the verify mail.',
'Resend Validation Email': 'Resend Validation Email',
'Validation email has been sent': 'Validation email has been sent',
'Unable to verify email': 'Unable to verify email',
'Unable to resend verify email': 'Unable to resend verify email',
'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',
@@ -1055,8 +1065,9 @@ export default {
'Basic Settings': 'Basic Settings',
'Security Settings': 'Security Settings',
'Two-Factor Authentication Settings': 'Two-Factor Authentication Settings',
'Email has been verified': 'Email has been verified',
'Email has not been verified': 'Email has not been verified',
'Username:': 'Username:',
'Avatar Provider:': 'Avatar Provider:',
'Current Password': 'Current Password',
'New Password': 'New Password',
'Modify Password': 'Modify Password',
+13 -2
View File
@@ -584,7 +584,8 @@ export default {
'email is invalid': '邮箱无效',
'email is empty or invalid': '邮箱为空或无效',
'new password equals old password': '新密码与旧密码相同',
'email is not verified': '邮箱没有验证过',
'email is not verified': '邮箱还未验证过',
'email is verified': '邮箱已经验证过',
'unauthorized access': '未授权的登录',
'current token is invalid': '当前认证令牌无效',
'current token is expired': '当前认证令牌已过期',
@@ -597,6 +598,7 @@ export default {
'token is not found': '认证令牌不存在',
'token is expired': '认证令牌已过期',
'token is empty': '认证令牌为空',
'email verify token is invalid or expired': '邮箱验证令牌无效或已过期',
'password reset token is invalid or expired': '密码重置令牌无效或已过期',
'passcode is invalid': '验证码无效',
'two factor backup code is invalid': '两步验证备用码无效',
@@ -847,6 +849,14 @@ export default {
'Use a passcode': '使用验证码',
'PIN code is invalid': 'PIN码无效',
'PIN code is wrong': 'PIN码错误',
'Verify your email': '验证您的邮箱',
'Verifying...': '正在验证...',
'Account activation link has been sent to your email address:': '账号激活链接已经发送到您的邮箱地址:',
', If you don\'t receive the mail, fill password and click the button below to resend the verify mail.': ',如果您没有收到邮件,输入密码并点击下方的按钮重新发送验证邮件。',
'Resend Validation Email': '重发验证邮件',
'Validation email has been sent': '验证邮件已发送',
'Unable to verify email': '无法验证邮箱',
'Unable to resend verify email': '无法重新发送验证邮件',
'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': '重置密码邮件已发送',
@@ -1055,8 +1065,9 @@ export default {
'Basic Settings': '基本设置',
'Security Settings': '安全设置',
'Two-Factor Authentication Settings': '两步验证设置',
'Email has been verified': '邮箱地址已验证',
'Email has not been verified': '邮箱地址未验证',
'Username:': '用户名:',
'Avatar Provider:': '头像提供方:',
'Current Password': '当前密码',
'New Password': '新密码',
'Modify Password': '修改密码',
+9
View File
@@ -5,6 +5,7 @@ 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 VerifyEmailPage from '@/views/desktop/VerifyEmailPage.vue';
import ForgetPasswordPage from '@/views/desktop/ForgetPasswordPage.vue';
import ResetPasswordPage from '@/views/desktop/ResetPasswordPage.vue';
import UnlockPage from '@/views/desktop/UnlockPage.vue';
@@ -159,6 +160,14 @@ const router = createRouter({
component: SignUpPage,
beforeEnter: checkNotLogin
},
{
path: '/verify_email',
component: VerifyEmailPage,
props: route => ({
email: route.query.email,
token: route.query.token
})
},
{
path: '/forgetpassword',
component: ForgetPasswordPage,
+87
View File
@@ -247,6 +247,69 @@ export const useRootStore = defineStore('root', {
userState.clearWebAuthnConfig();
this.resetAllStates(true);
},
verifyEmail({ token, requestNewToken }) {
return new Promise((resolve, reject) => {
services.verifyEmail({
token,
requestNewToken
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to verify email' });
return;
}
if (data.result.newToken && isString(data.result.newToken)) {
userState.updateToken(data.result.newToken);
}
if (data.result.user && isObject(data.result.user)) {
const userStore = useUserStore();
userStore.storeUserInfo(data.result.user);
}
resolve(data.result);
}).catch(error => {
logger.error('failed to verify 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 verify email' });
}
});
});
},
resendVerifyEmailByUnloginUser({ email, password }) {
return new Promise((resolve, reject) => {
services.resendVerifyEmailByUnloginUser({
email,
password
}).then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to resend verify email' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to resend verify 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 resend verify email' });
}
});
});
},
requestResetPassword({ email }) {
return new Promise((resolve, reject) => {
services.requestResetPassword({
@@ -363,6 +426,30 @@ export const useRootStore = defineStore('root', {
});
});
},
resendVerifyEmailByLoginedUser() {
return new Promise((resolve, reject) => {
services.resendVerifyEmailByLoginedUser().then(response => {
const data = response.data;
if (!data || !data.success || !data.result) {
reject({ message: 'Unable to resend verify email' });
return;
}
resolve(data.result);
}).catch(error => {
logger.error('failed to resend verify 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 resend verify email' });
}
});
});
},
clearUserData({ password }) {
return new Promise((resolve, reject) => {
services.clearData({
+5
View File
@@ -312,6 +312,11 @@ export default {
}).catch(error => {
self.logining = false;
if (error.error && error.error.errorCode === 201020 && error.error.context && error.error.context.email) {
self.$router.push('/verify_email?email=' + encodeURIComponent(error.error.context.email));
return;
}
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
+241
View File
@@ -0,0 +1,241 @@
<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="320px" src="img/desktop/people2.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('Verify your email') }}</h5>
<p class="mb-0" v-if="token && loading">{{ $t('Verifying...') }}</p>
<p class="mb-0" v-if="token && verified">{{ $t('Email has been verified') }}</p>
<p class="mb-0" v-if="token && !verified && errorMessage">{{ errorMessage }}</p>
<p class="mb-0" v-if="!token && !email">{{ $t('Parameter Invalid') }}</p>
<p class="mb-0" v-if="!token && email">
<span>{{ $t('Account activation link has been sent to your email address:') }}</span>
<span class="ml-1">{{ email }}</span>
<span class="ml-1">{{ $t(', If you don\'t receive the mail, fill password and click the button below to resend the verify mail.') }}</span>
</p>
</v-card-text>
<v-card-text class="pb-0 mb-6">
<v-form>
<v-row>
<v-col cols="12" v-if="!loading && !token && email && isUserVerifyEmailEnabled">
<v-text-field
autocomplete="password"
clearable
:type="isPasswordVisible ? 'text' : 'password'"
:disabled="loading || resending"
:label="$t('Password')"
:placeholder="$t('Your password')"
:append-inner-icon="isPasswordVisible ? icons.eyeSlash : icons.eye"
v-model="password"
@click:append-inner="isPasswordVisible = !isPasswordVisible"
@keyup.enter="resendEmail"
/>
</v-col>
<v-col cols="12" v-if="!loading && !token && email && isUserVerifyEmailEnabled">
<v-btn block type="submit" :disabled="loading || resending || !password" @click="resendEmail">
{{ $t('Resend Validation Email') }}
<v-progress-circular indeterminate size="24" class="ml-2" v-if="resending"></v-progress-circular>
</v-btn>
</v-col>
<v-col cols="12">
<router-link class="d-flex align-center justify-center" to="/login"
:class="{ 'disabled': loading || resending }">
<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="resending"
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>&nbsp;<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" @update:show="onSnackbarShowStateChanged" />
</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 { isUserVerifyEmailEnabled } from '@/lib/server_settings.js';
import {
mdiChevronLeft,
mdiEyeOffOutline,
mdiEyeOutline
} from '@mdi/js';
export default {
props: [
'email',
'token'
],
data() {
return {
password: '',
isPasswordVisible: false,
loading: true,
resending: false,
verified: false,
errorMessage: '',
icons: {
left: mdiChevronLeft,
eye: mdiEyeOutline,
eyeSlash: mdiEyeOffOutline
}
};
},
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();
},
isUserVerifyEmailEnabled() {
return isUserVerifyEmailEnabled();
}
},
setup() {
const theme = useTheme();
return {
globalTheme: theme
};
},
created() {
const self = this;
self.verified = false;
self.loading = true;
if (!self.token) {
self.loading = false;
return;
}
self.rootStore.verifyEmail({
token: self.token,
requestNewToken: !self.$user.isUserLogined()
}).then(() => {
self.loading = false;
self.verified = true;
self.$refs.snackbar.showMessage('Email has been verified');
}).catch(error => {
self.loading = false;
self.verified = false;
if (!error.processed) {
self.errorMessage = self.$tError(error.message || error);
self.$refs.snackbar.showError(error);
}
});
},
methods: {
resendEmail() {
const self = this;
self.resending = true;
self.rootStore.resendVerifyEmailByUnloginUser({
email: self.email,
password: self.password
}).then(() => {
self.resending = false;
self.$refs.snackbar.showMessage('Validation email has been sent');
}).catch(error => {
self.resending = false;
if (!error.processed) {
self.$refs.snackbar.showError(error);
}
});
},
onSnackbarShowStateChanged(newValue) {
if (!newValue && this.verified && this.$user.isUserLogined()) {
this.$router.replace('/');
}
},
changeLanguage(locale) {
const localeDefaultSettings = this.$locale.setLanguage(locale);
this.settingsStore.updateLocalizedDefaultSettings(localeDefaultSettings);
}
}
}
</script>
@@ -25,9 +25,9 @@
<span v-if="!loading">{{ oldProfile.username }}</span>
</div>
<div class="d-flex text-body-1">
<span class="me-1">{{ $t('Avatar Provider:') }}</span>
<v-skeleton-loader class="skeleton-no-margin" type="text" style="width: 100px" :loading="true" v-if="loading"></v-skeleton-loader>
<span v-if="!loading">{{ currentUserAvatarProvider }}</span>
<span class="me-1" v-if="!loading && emailVerified">{{ $t('Email has been verified') }}</span>
<span class="me-1" v-if="!loading && !emailVerified">{{ $t('Email has not been verified') }}</span>
<v-skeleton-loader class="skeleton-no-margin mt-2 mb-1" type="text" style="width: 160px" :loading="true" v-if="loading"></v-skeleton-loader>
</div>
</div>
</v-card-text>
@@ -268,6 +268,7 @@ export default {
longTimeFormat: 0,
shortTimeFormat: 0
},
emailVerified: false,
loading: true,
saving: false,
icons: {
@@ -310,13 +311,6 @@ export default {
allTransactionEditScopeTypes() {
return this.$locale.getAllTransactionEditScopeTypes();
},
currentUserAvatarProvider() {
if (this.oldProfile.avatarProvider === 'gravatar') {
return 'Gravatar';
} else {
return this.$t('None');
}
},
inputIsNotChanged() {
return !!this.inputIsNotChangedProblemMessage;
},
@@ -383,6 +377,7 @@ export default {
Promise.all(promises).then(responses => {
const profile = responses[1];
self.setCurrentUserProfile(profile);
self.emailVerified = profile.emailVerified;
self.loading = false;
}).catch(error => {
self.oldProfile.nickname = '';