diff --git a/package-lock.json b/package-lock.json
index 8cb2bb70..773b0f12 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3394,6 +3394,16 @@
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
+ "clipboard": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.6.tgz",
+ "integrity": "sha512-g5zbiixBRk/wyKakSwCKd7vQXDjFnAMGHoEyBogG/bw9kTD9GvdAvaoRR1ALcEzt3pVKxZR0pViekPMIS0QyGg==",
+ "requires": {
+ "good-listener": "^1.2.2",
+ "select": "^1.1.2",
+ "tiny-emitter": "^2.0.0"
+ }
+ },
"clipboardy": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-2.3.0.tgz",
@@ -4387,6 +4397,11 @@
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
},
+ "delegate": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+ "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+ },
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
@@ -5726,6 +5741,14 @@
"slash": "^2.0.0"
}
},
+ "good-listener": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+ "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=",
+ "requires": {
+ "delegate": "^3.1.2"
+ }
+ },
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
@@ -9453,6 +9476,11 @@
"ajv-keywords": "^3.5.2"
}
},
+ "select": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+ "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
+ },
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -10517,6 +10545,11 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
+ "tiny-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+ },
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -11020,6 +11053,14 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
},
+ "vue-clipboard2": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/vue-clipboard2/-/vue-clipboard2-0.3.1.tgz",
+ "integrity": "sha512-H5S/agEDj0kXjUb5GP2c0hCzIXWRBygaWLN3NEFsaI9I3uWin778SFEMt8QRXiPG+7anyjqWiw2lqcxWUSfkYg==",
+ "requires": {
+ "clipboard": "^2.0.0"
+ }
+ },
"vue-eslint-parser": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-7.1.1.tgz",
diff --git a/package.json b/package.json
index f6b57136..631c2d31 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"js-cookie": "^2.2.1",
"moment": "^2.29.1",
"vue": "^2.6.11",
+ "vue-clipboard2": "^0.3.1",
"vue-i18n": "^8.22.0",
"vue-i18n-filter": "^0.1.6",
"vue-moment": "^4.1.0"
diff --git a/pkg/api/twofactor_authorizations.go b/pkg/api/twofactor_authorizations.go
index 1981dc59..fbc177cb 100644
--- a/pkg/api/twofactor_authorizations.go
+++ b/pkg/api/twofactor_authorizations.go
@@ -203,7 +203,29 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.Conte
}
func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (interface{}, *errs.Error) {
+ var disableReq models.TwoFactorDisableRequest
+ err := c.ShouldBindJSON(&disableReq)
+
+ if err != nil {
+ log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] parse request failed, because %s", err.Error())
+ return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
+ }
+
uid := c.GetCurrentUid()
+ user, err := a.users.GetUserById(uid)
+
+ if err != nil {
+ if !errs.IsCustomError(err) {
+ log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorDisableHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
+ }
+
+ return nil, errs.ErrUserNotFound
+ }
+
+ if !a.users.IsPasswordEqualsUserPassword(disableReq.Password, user) {
+ return nil, errs.ErrUserPasswordWrong
+ }
+
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
@@ -235,7 +257,29 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.Context) (i
}
func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *core.Context) (interface{}, *errs.Error) {
+ var regenerateReq models.TwoFactorRegenerateRecoveryCodeRequest
+ err := c.ShouldBindJSON(®enerateReq)
+
+ if err != nil {
+ log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] parse request failed, because %s", err.Error())
+ return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
+ }
+
uid := c.GetCurrentUid()
+ user, err := a.users.GetUserById(uid)
+
+ if err != nil {
+ if !errs.IsCustomError(err) {
+ log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
+ }
+
+ return nil, errs.ErrUserNotFound
+ }
+
+ if !a.users.IsPasswordEqualsUserPassword(regenerateReq.Password, user) {
+ return nil, errs.ErrUserPasswordWrong
+ }
+
enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(uid)
if err != nil {
@@ -254,13 +298,6 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorRecoveryCodeRegenerateHandler(c *c
return nil, errs.Or(err, errs.ErrOperationFailed)
}
- user, err := a.users.GetUserById(uid)
-
- if err != nil {
- log.WarnfWithRequestId(c, "[twofactor_authorizations.TwoFactorRecoveryCodeRegenerateHandler] failed to get user for user \"uid:%d\", because %s", uid, err.Error())
- return nil, errs.ErrUserNotFound
- }
-
err = a.twoFactorAuthorizations.CreateTwoFactorRecoveryCodes(uid, recoveryCodes, user.Salt)
if err != nil {
diff --git a/pkg/models/two_factor.go b/pkg/models/two_factor.go
index 9838f0c6..0922389c 100644
--- a/pkg/models/two_factor.go
+++ b/pkg/models/two_factor.go
@@ -25,6 +25,14 @@ type TwoFactorEnableConfirmResponse struct {
RecoveryCodes []string `json:"recoveryCodes"`
}
+type TwoFactorDisableRequest struct {
+ Password string `json:"password" binding:"omitempty,min=6,max=128"`
+}
+
+type TwoFactorRegenerateRecoveryCodeRequest struct {
+ Password string `json:"password" binding:"omitempty,min=6,max=128"`
+}
+
type TwoFactorStatusResponse struct {
Enable bool `json:"enable"`
CreatedAt int64 `json:"createdAt,omitempty"`
diff --git a/src/lib/services.js b/src/lib/services.js
index 395c0082..bf8a40ba 100644
--- a/src/lib/services.js
+++ b/src/lib/services.js
@@ -135,4 +135,26 @@ export default {
oldPassword
});
},
+ get2FAStatus: () => {
+ return axios.get('v1/users/2fa/status.json');
+ },
+ enable2FA: () => {
+ return axios.post('v1/users/2fa/enable/request.json');
+ },
+ confirmEnable2FA: ({ secret, passcode }) => {
+ return axios.post('v1/users/2fa/enable/confirm.json', {
+ secret,
+ passcode
+ });
+ },
+ disable2FA: ({ password }) => {
+ return axios.post('v1/users/2fa/disable.json', {
+ password
+ });
+ },
+ regenerate2FARecoveryCode: ({ password }) => {
+ return axios.post('v1/users/2fa/recovery/regenerate.json', {
+ password
+ });
+ },
};
diff --git a/src/locales/en.js b/src/locales/en.js
index 0d19bd9d..585dbabc 100644
--- a/src/locales/en.js
+++ b/src/locales/en.js
@@ -46,6 +46,7 @@ export default {
'parameter': {
'username': 'Username',
'password': 'Password',
+ 'passcode': 'Passcode',
'email': 'Email',
'nickname': 'Nickname',
'oldPassword': 'Current Password',
@@ -66,6 +67,11 @@ export default {
'Update': 'Update',
'Done': 'Done',
'Continue': 'Continue',
+ 'Status': 'Status',
+ 'Enable': 'Enable',
+ 'Enabled': 'Enabled',
+ 'Disable': 'Disable',
+ 'Disabled': 'Disabled',
'Version': 'Version',
'User': 'User',
'Application': 'Application',
@@ -128,6 +134,16 @@ export default {
'Other Device': 'Other Device',
'Are you sure you want to logout from this session?': 'Are you sure you want to logout from this session?',
'Unable to logout from this session': 'Unable to logout from this session',
+ 'Regenerate Backup Codes': 'Regenerate Backup Codes',
+ 'Please use two factor authentication app scan the below qrcode and input current passcode': 'Please use two factor authentication app scan the below qrcode and input current passcode',
+ 'Please enter your current password when disable two factor authentication': 'Please enter your current password when disable two factor authentication',
+ 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.',
+ 'Please copy these backup codes to safe place, the below codes can only be shown once. If these codes were lost, you can regenerate backup codes at any time.': 'Please copy these backup codes to safe place, the below codes can only be shown once. If these codes were lost, you can regenerate backup codes at any time.',
+ 'Backup codes copied': 'Backup codes copied',
+ 'Two factor authentication has been disabled': 'Two factor authentication has been disabled',
+ 'Unable to get current two factor authentication status': 'Unable to get current two factor authentication status',
+ 'Unable to enable two factor authentication': 'Unable to enable two factor authentication',
+ 'Unable to disable two factor authentication': 'Unable to disable two factor authentication',
'Log Out': 'Log Out',
'Are you sure you want to log out?': 'Are you sure you want to log out?',
'Unable to logout': 'Unable to logout',
diff --git a/src/locales/zh_Hans.js b/src/locales/zh_Hans.js
index 98fec517..1648dfdf 100644
--- a/src/locales/zh_Hans.js
+++ b/src/locales/zh_Hans.js
@@ -46,6 +46,7 @@ export default {
'parameter': {
'username': '用户名',
'password': '密码',
+ 'passcode': '验证码',
'email': '电子邮箱',
'nickname': '昵称',
'oldPassword': '当前密码',
@@ -66,6 +67,11 @@ export default {
'Update': '更新',
'Done': '完成',
'Continue': '继续',
+ 'Status': '状态',
+ 'Enable': '启用',
+ 'Enabled': '启用',
+ 'Disable': '禁用',
+ 'Disabled': '禁用',
'Version': '版本',
'User': '用户',
'Application': '应用',
@@ -128,6 +134,16 @@ export default {
'Other Device': '其他设备',
'Are you sure you want to logout from this session?': '您确定是否要退出该会话?',
'Unable to logout from this session': '无法退出该会话',
+ 'Regenerate Backup Codes': '重新生成备用码',
+ 'Please use two factor authentication app scan the below qrcode and input current passcode': '请使用两步验证应用扫描下方的二维码并输入当前的验证码',
+ 'Please enter your current password when disable two factor authentication': '禁用两步验证时需要输入您的当前密码',
+ 'Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.': '重新生成两步验证备用码时需要输入您的当前密码。如果您重新生成备用码,之前的备用码将失效。',
+ 'Please copy these backup codes to safe place, the below codes can only be shown once. If these codes were lost, you can regenerate backup codes at any time.': '请将备用码复制到安全的地方,下列备用码只会展示一次。如果这些备用码丢失,您可以随时重新生成备用码。',
+ 'Backup codes copied': '备用码已复制',
+ 'Two factor authentication has been disabled': '两步验证已经禁用',
+ 'Unable to get current two factor authentication status': '无法获取当前两步验证状态',
+ 'Unable to enable two factor authentication': '无法启用两步验证',
+ 'Unable to disable two factor authentication': '无法禁用两步验证',
'Log Out': '退出登录',
'Are you sure you want to log out?': '您确定是否要退出登录?',
'Unable to logout': '无法退出登录',
diff --git a/src/mobile-main.js b/src/mobile-main.js
index 101ccaad..75d98f34 100644
--- a/src/mobile-main.js
+++ b/src/mobile-main.js
@@ -4,6 +4,7 @@ import VueI18nFilter from 'vue-i18n-filter'
import Framework7 from 'framework7/framework7.esm.bundle.js';
import Framework7Vue from 'framework7-vue/framework7-vue.esm.bundle.js';
import VueMoment from 'vue-moment';
+import VueClipboard from 'vue-clipboard2';
import moment from 'moment';
import 'moment/min/locales';
@@ -21,6 +22,7 @@ import App from './Mobile.vue';
Vue.use(VueI18n);
Vue.use(VueI18nFilter);
Vue.use(VueMoment, { moment });
+Vue.use(VueClipboard);
Framework7.use(Framework7Vue);
const i18n = new VueI18n(getI18nOptions());
diff --git a/src/router/mobile.js b/src/router/mobile.js
index e725a50a..b95f7e3c 100644
--- a/src/router/mobile.js
+++ b/src/router/mobile.js
@@ -8,6 +8,7 @@ import SignUpPage from '../views/mobile/Signup.vue';
import SettingsPage from '../views/mobile/Settings.vue';
import AboutPage from "../views/mobile/About.vue";
import UserProfilePage from "../views/mobile/users/UserProfile.vue";
+import TwoFactorAuthPage from "../views/mobile/users/TwoFactorAuth.vue";
import SessionListPage from "../views/mobile/users/SessionList.vue";
function checkLogin(to, from, resolve, reject) {
@@ -73,6 +74,11 @@ const routes = [
component: UserProfilePage,
beforeEnter: checkLogin
},
+ {
+ path: '/user/2fa',
+ component: TwoFactorAuthPage,
+ beforeEnter: checkLogin
+ },
{
path: '/user/sessions',
component: SessionListPage,
diff --git a/src/views/mobile/About.vue b/src/views/mobile/About.vue
index 65160920..314b5780 100644
--- a/src/views/mobile/About.vue
+++ b/src/views/mobile/About.vue
@@ -151,6 +151,12 @@
https://github.com/brockpetrie/vue-moment
License: https://github.com/brockpetrie/vue-moment/blob/master/LICENSE
+ vue-clipboard2
+ Copyright (c) 2017 Inndy <inndy \dot tw \at gmail \dot com>
+ https://github.com/Inndy/vue-clipboard2
+ License: https://github.com/Inndy/vue-clipboard2/blob/master/LICENSE
+
core-js {{ $t('Please use two factor authentication app scan the below qrcode and input current passcode') }} {{ $t('Please enter your current password when disable two factor authentication') }} {{ $t('Please enter your current password when regenerate two factor authentication backup codes. If you regenerate backup codes, the old codes will be invalidated.') }}
+ {{ $t('Please copy these backup codes to safe place, the below codes can only be shown once. If these codes were lost, you can regenerate backup codes at any time.') }}
+
Copyright (c) 2014-2020 Denis Pushkarev
diff --git a/src/views/mobile/Settings.vue b/src/views/mobile/Settings.vue
index 3ad512e2..d3d8f329 100644
--- a/src/views/mobile/Settings.vue
+++ b/src/views/mobile/Settings.vue
@@ -4,7 +4,7 @@
+