From 34726ffa8b8b1b4a9c4837404f9f4caf3882ddf3 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 31 Oct 2020 16:19:08 +0800 Subject: [PATCH] add device & sessions page --- package-lock.json | 18 +++++ package.json | 5 +- pkg/errs/token.go | 22 ++--- pkg/middlewares/authorization.go | 12 +-- src/lib/services.js | 19 ++++- src/locales/en.js | 23 ++++-- src/locales/zh_Hans.js | 23 ++++-- src/mobile-main.js | 8 ++ src/router/mobile.js | 6 ++ src/views/mobile/Settings.vue | 2 + src/views/mobile/users/SessionList.vue | 107 +++++++++++++++++++++++++ 11 files changed, 216 insertions(+), 29 deletions(-) create mode 100644 src/views/mobile/users/SessionList.vue diff --git a/package-lock.json b/package-lock.json index c8669648..1528e9f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7404,6 +7404,11 @@ "minimist": "^1.2.5" } }, + "moment": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", + "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -11046,6 +11051,11 @@ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.22.0.tgz", "integrity": "sha512-2tYS0bYDPJHKAWPy01aDe5h3wcXDGjhJmboHKOfi2OEYR+6gyXaIzdua1smZCQwOeWdlGsLntwdIgkXWrnLjxg==" }, + "vue-i18n-filter": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/vue-i18n-filter/-/vue-i18n-filter-0.1.6.tgz", + "integrity": "sha512-zYo06SmMZtJwTkEd2l+t1jCH8lrtTbkwwoz/6XLvFzNF4tGd5CPOhfIxLLv0wf9vxMVYE14KRuzOZzw26qfRxA==" + }, "vue-loader": { "version": "15.9.3", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.3.tgz", @@ -11148,6 +11158,14 @@ } } }, + "vue-moment": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vue-moment/-/vue-moment-4.1.0.tgz", + "integrity": "sha512-Gzisqpg82ItlrUyiD9d0Kfru+JorW2o4mQOH06lEDZNgxci0tv/fua1Hl0bo4DozDV2JK1r52Atn/8QVCu8qQw==", + "requires": { + "moment": "^2.19.2" + } + }, "vue-style-loader": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.2.tgz", diff --git a/package.json b/package.json index e2fd559b..8511ed96 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,11 @@ "framework7-icons": "^3.0.1", "framework7-vue": "^5.7.13", "js-cookie": "^2.2.1", + "moment": "^2.29.1", "vue": "^2.6.11", - "vue-i18n": "^8.22.0" + "vue-i18n": "^8.22.0", + "vue-i18n-filter": "^0.1.6", + "vue-moment": "^4.1.0" }, "devDependencies": { "@vue/cli-plugin-babel": "~4.5.0", diff --git a/pkg/errs/token.go b/pkg/errs/token.go index 1dd3c4c7..ce6bc36d 100644 --- a/pkg/errs/token.go +++ b/pkg/errs/token.go @@ -5,14 +5,16 @@ import ( ) var ( - ErrTokenGenerating = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 0, http.StatusInternalServerError, "failed to generate token") - ErrUnauthorizedAccess = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 1, http.StatusUnauthorized, "unauthorized access") - ErrTokenExpired = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 2, http.StatusUnauthorized, "token is expired") - ErrInvalidToken = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 3, http.StatusUnauthorized, "token is invalid") - ErrInvalidUserTokenId = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 4, http.StatusUnauthorized, "user token id is invalid") - ErrInvalidTokenId = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 5, http.StatusUnauthorized, "token id is invalid") - ErrTokenRecordNotFound = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 6, http.StatusUnauthorized, "token is not found") - ErrInvalidTokenType = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 7, http.StatusUnauthorized, "token type is invalid") - ErrTokenRequire2FA = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 8, http.StatusUnauthorized, "token requires two factor authorization") - ErrTokenNotRequire2FA = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 9, http.StatusUnauthorized, "token does not require two factor authorization") + ErrTokenGenerating = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 0, http.StatusInternalServerError, "failed to generate token") + ErrUnauthorizedAccess = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 1, http.StatusUnauthorized, "unauthorized access") + ErrCurrentInvalidToken = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 2, http.StatusUnauthorized, "current token is invalid") + ErrCurrentTokenExpired = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 3, http.StatusUnauthorized, "current token is expired") + ErrCurrentInvalidTokenType = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 4, http.StatusUnauthorized, "current token type is invalid") + ErrCurrentTokenRequire2FA = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 5, http.StatusUnauthorized, "current token requires two factor authorization") + ErrCurrentTokenNotRequire2FA = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 6, http.StatusUnauthorized, "current token does not require two factor authorization") + ErrInvalidToken = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 7, http.StatusUnauthorized, "token is invalid") + ErrInvalidTokenId = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 8, http.StatusUnauthorized, "token id is invalid") + ErrInvalidUserTokenId = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 9, http.StatusUnauthorized, "user token id is invalid") + ErrTokenRecordNotFound = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 10, http.StatusUnauthorized, "token is not found") + ErrTokenExpired = NewNormalError(NORMAL_SUBCATEGORY_TOKEN, 11, http.StatusUnauthorized, "token is expired") ) diff --git a/pkg/middlewares/authorization.go b/pkg/middlewares/authorization.go index 924f7320..f24d6639 100644 --- a/pkg/middlewares/authorization.go +++ b/pkg/middlewares/authorization.go @@ -20,13 +20,13 @@ func JWTAuthorization(c *core.Context) { if claims.Type == core.USER_TOKEN_TYPE_REQUIRE_2FA { log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token requires 2fa", claims.Id) - utils.PrintErrorResult(c, errs.ErrTokenRequire2FA) + utils.PrintErrorResult(c, errs.ErrCurrentTokenRequire2FA) return } if claims.Type != core.USER_TOKEN_TYPE_NORMAL { log.WarnfWithRequestId(c, "[authorization.JWTAuthorization] user \"uid:%s\" token type is invalid", claims.Id) - utils.PrintErrorResult(c, errs.ErrInvalidTokenType) + utils.PrintErrorResult(c, errs.ErrCurrentInvalidTokenType) return } @@ -44,7 +44,7 @@ func JWTTwoFactorAuthorization(c *core.Context) { if claims.Type != core.USER_TOKEN_TYPE_REQUIRE_2FA { log.WarnfWithRequestId(c, "[authorization.JWTTwoFactorAuthorization] user \"uid:%s\" token is not need two factor authorization", claims.Id) - utils.PrintErrorResult(c, errs.ErrTokenNotRequire2FA) + utils.PrintErrorResult(c, errs.ErrCurrentTokenNotRequire2FA) return } @@ -62,17 +62,17 @@ func getTokenClaims(c *core.Context) (*core.UserTokenClaims, *errs.Error) { if !token.Valid { log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is invalid") - return nil, errs.ErrInvalidToken + return nil, errs.ErrCurrentInvalidToken } if !claims.VerifyExpiresAt(time.Now().Unix(), true) { log.WarnfWithRequestId(c, "[authorization.getTokenClaims] token is expired") - return nil, errs.ErrTokenExpired + return nil, errs.ErrCurrentTokenExpired } if claims.Id == "" { log.WarnfWithRequestId(c, "[authorization.getTokenClaims] user id in token is empty") - return nil, errs.ErrInvalidToken + return nil, errs.ErrCurrentInvalidToken } return claims, nil diff --git a/src/lib/services.js b/src/lib/services.js index 5fe0e476..5220ed44 100644 --- a/src/lib/services.js +++ b/src/lib/services.js @@ -32,10 +32,15 @@ axios.interceptors.request.use(config => { axios.interceptors.response.use(response => { return response; }, error => { - if (error.response && error.response.data && error.response.data.errorCode) { + if (error.response && !error.response.config.ignoreError && error.response.data && error.response.data.errorCode) { const errorCode = error.response.data.errorCode; - if (202001 <= errorCode && errorCode <= 202008) { // unauthorized access or token is invalid + if (errorCode === 202001 // unauthorized access + && errorCode <= 202002 // current token is invalid + && errorCode <= 202003 // current token is expired + && errorCode <= 202004 // current token type is invalid + && errorCode <= 202005 // current token requires two factor authorization + && errorCode <= 202006) { // current token does not require two factor authorization userState.clearToken(); location.reload(); return Promise.reject({ processed: true }); @@ -98,6 +103,8 @@ export default { if (data.result.oldTokenId) { axios.post('v1/tokens/revoke.json', { tokenId: data.result.oldTokenId + }, { + ignoreError: true }); } } @@ -119,4 +126,12 @@ export default { password }); }, + getTokens: () => { + return axios.get('v1/tokens/list.json'); + }, + revokeToken: ({ tokenId }) => { + return axios.post('v1/tokens/revoke.json', { + tokenId + }); + }, }; diff --git a/src/locales/en.js b/src/locales/en.js index d9b518cc..3c67a56c 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -4,6 +4,11 @@ export default { 'title': 'lab account book', } }, + 'format': { + 'datetime': { + 'long': 'MM/DD/YYYY HH:mm:ss', + } + }, 'error': { 'system error': 'System Error', 'api not found': 'Failed to request api', @@ -11,14 +16,16 @@ export default { 'operation failed': 'Operation failed', 'nothing will be updated': 'Nothing will be updated', 'unauthorized access': 'Unauthorized access', - 'token is expired': 'Token is expired', + 'current token is invalid': 'Current token is invalid', + 'current token is expired': 'Current token is expired', + 'current token type is invalid': 'Current token type is invalid', + 'current token requires two factor authorization': 'Current token requires two factor authorization', + 'current token does not require two factor authorization': 'Current token does not require two factor authorization', 'token is invalid': 'Token is invalid', - 'user token id is invalid': 'User token id is invalid', 'token id is invalid': 'Token id is invalid', + 'user token id is invalid': 'User token id is invalid', 'token is not found': 'Token is not found', - 'token type is invalid': 'Token type is invalid', - 'token requires two factor authorization': 'Token requires two factor authorization', - 'token does not require two factor authorization': 'Token does not require two factor authorization', + 'token is expired': 'Token is expired', 'user id is invalid': 'User id is invalid', 'username is empty': 'Username is empty', 'email is empty': 'Email is empty', @@ -107,6 +114,12 @@ export default { 'Nothing has been modified': 'Nothing has been modified', 'Your profile has been successfully updated': 'Your profile has been successfully updated', 'Unable to update user profile': 'Unable to update user profile', + 'Device & Sessions': 'Device & Sessions', + 'Unable to get session list': 'Unable to get session list', + 'Current': 'Current', + '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', '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 ace1d344..d92c3f3e 100644 --- a/src/locales/zh_Hans.js +++ b/src/locales/zh_Hans.js @@ -4,6 +4,11 @@ export default { 'title': 'lab 轻记账', } }, + 'format': { + 'datetime': { + 'long': 'YYYY年MM月DD日 HH:mm:ss', + } + }, 'error': { 'system error': '系统错误', 'api not found': '接口调用失败', @@ -11,14 +16,16 @@ export default { 'operation failed': '操作失败', 'nothing will be updated': '没有内容更新', 'unauthorized access': '未授权的登录', - 'token is expired': '认证令牌已过期', + 'current token is invalid': '当前认证令牌无效', + 'current token is expired': '当前认证令牌已过期', + 'current token type is invalid': '当前认证令牌类型无效', + 'current token requires two factor authorization': '当前认证令牌需要两步验证', + 'current token does not require two factor authorization': '当前认证令牌不需要两步验证', 'token is invalid': '认证令牌无效', - 'user token id is invalid': '用户认证令牌ID无效', 'token id is invalid': '认证令牌ID无效', + 'user token id is invalid': '用户认证令牌ID无效', 'token is not found': '认证令牌不存在', - 'token type is invalid': '认证令牌类型无效', - 'token requires two factor authorization': '认证令牌需要两步验证', - 'token does not require two factor authorization': '认证令牌不需要两步验证', + 'token is expired': '认证令牌已过期', 'user id is invalid': '用户ID无效', 'username is empty': '用户名为空', 'email is empty': '电子邮箱为空', @@ -107,6 +114,12 @@ export default { 'Nothing has been modified': '没有修改的项目', 'Your profile has been successfully updated': '您的用户信息更新成功', 'Unable to update user profile': '无法更新用户信息', + 'Device & Sessions': '设备和会话', + 'Unable to get session list': '无法获取会话列表', + 'Current': '当前', + 'Other Device': '其他设备', + 'Are you sure you want to logout from this session?': '您确定是否要退出该会话?', + 'Unable to logout from this session': '无法退出该会话', '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 c3731241..612ab1a1 100644 --- a/src/mobile-main.js +++ b/src/mobile-main.js @@ -1,7 +1,12 @@ import Vue from 'vue'; import VueI18n from 'vue-i18n'; +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 moment from 'moment'; +import 'moment/min/locales'; import 'framework7/css/framework7.bundle.css'; import 'framework7-icons'; @@ -13,6 +18,8 @@ import userstate from './lib/userstate.js'; import App from './Mobile.vue'; Vue.use(VueI18n); +Vue.use(VueI18nFilter); +Vue.use(VueMoment, { moment }); Framework7.use(Framework7Vue); const i18n = new VueI18n(getI18nOptions()); @@ -27,6 +34,7 @@ Vue.prototype.$setLanguage = function (locale) { } i18n.locale = locale; + moment.locale(locale); services.setLocale(locale); document.querySelector('html').setAttribute('lang', locale); return locale; diff --git a/src/router/mobile.js b/src/router/mobile.js index 611facf2..ef7eaa69 100644 --- a/src/router/mobile.js +++ b/src/router/mobile.js @@ -7,6 +7,7 @@ import LoginPage from '../views/mobile/Login.vue'; import SignUpPage from '../views/mobile/Signup.vue'; import SettingsPage from '../views/mobile/Settings.vue'; import UserProfilePage from "../views/mobile/users/UserProfile.vue"; +import SessionListPage from "../views/mobile/users/SessionList.vue"; function checkLogin(to, from, resolve, reject) { const router = this; @@ -66,6 +67,11 @@ const routes = [ component: UserProfilePage, beforeEnter: checkLogin }, + { + path: '/user/sessions', + component: SessionListPage, + beforeEnter: checkLogin + }, { path: '(.*)', redirect: '/' diff --git a/src/views/mobile/Settings.vue b/src/views/mobile/Settings.vue index 65efe12c..43d5a944 100644 --- a/src/views/mobile/Settings.vue +++ b/src/views/mobile/Settings.vue @@ -4,6 +4,8 @@ {{ userNickName }} + + {{ $t('Log Out') }} {{ $t('Application') }} diff --git a/src/views/mobile/users/SessionList.vue b/src/views/mobile/users/SessionList.vue new file mode 100644 index 00000000..24c8e6e3 --- /dev/null +++ b/src/views/mobile/users/SessionList.vue @@ -0,0 +1,107 @@ + + +