From b8253b6dccb1e92400d1e21d8c0910900e125b88 Mon Sep 17 00:00:00 2001 From: MaysWind Date: Sat, 9 Nov 2024 23:43:27 +0800 Subject: [PATCH] create user token via cli --- cmd/user_data.go | 34 ++++++++++++ pkg/cli/user_data.go | 24 +++++++++ pkg/services/tokens.go | 34 ++++++++---- src/lib/misc.js | 52 +++++++++++++++++-- .../settings/tabs/UserSecuritySettingTab.vue | 38 ++++++-------- src/views/mobile/users/SessionListPage.vue | 39 ++++++-------- 6 files changed, 158 insertions(+), 63 deletions(-) diff --git a/cmd/user_data.go b/cmd/user_data.go index 14192721..b3a93919 100644 --- a/cmd/user_data.go +++ b/cmd/user_data.go @@ -192,6 +192,19 @@ var UserData = &cli.Command{ }, }, }, + { + Name: "user-session-new", + Usage: "Create new session for user", + Action: bindAction(createNewUserToken), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "username", + Aliases: []string{"n"}, + Required: true, + Usage: "Specific user name", + }, + }, + }, { Name: "user-session-clear", Usage: "Clear user all sessions", @@ -549,6 +562,27 @@ func listUserTokens(c *core.CliContext) error { return nil } +func createNewUserToken(c *core.CliContext) error { + _, err := initializeSystem(c) + + if err != nil { + return err + } + + username := c.String("username") + token, tokenString, err := clis.UserData.CreateNewUserToken(c, username) + + if err != nil { + log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token") + return err + } + + printTokenInfo(token) + fmt.Printf("[NewToken] %s\n", tokenString) + + return nil +} + func clearUserTokens(c *core.CliContext) error { _, err := initializeSystem(c) diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index b12bd82c..cd58ec2a 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -352,6 +352,30 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo return tokens, nil } +// CreateNewUserToken returns a new token for the specified user +func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string) (*models.TokenRecord, string, error) { + if username == "" { + log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty") + return nil, "", errs.ErrUsernameIsEmpty + } + + user, err := l.GetUserByUsername(c, username) + + if err != nil { + log.CliErrorf(c, "[user_data.CreateNewUserToken] error occurs when getting user by user name") + return nil, "", err + } + + token, tokenRecord, err := l.tokens.CreateTokenViaCli(c, user) + + if err != nil { + log.CliErrorf(c, "[user_data.CreateNewUserToken] failed to create token for user \"%s\", because %s", username, err.Error()) + return nil, "", err + } + + return tokenRecord, token, nil +} + // ClearUserTokens clears all tokens of the specified user func (l *UserDataCli) ClearUserTokens(c *core.CliContext, username string) error { if username == "" { diff --git a/pkg/services/tokens.go b/pkg/services/tokens.go index 3f38407a..e60b6181 100644 --- a/pkg/services/tokens.go +++ b/pkg/services/tokens.go @@ -78,34 +78,46 @@ func (s *TokenService) ParseTokenByCookie(c *core.WebContext, tokenCookieName st return s.parseToken(c, utils.CookieExtractor{tokenCookieName}) } +// CreateTokenViaCli generates a new normal token and saves to database +func (s *TokenService) CreateTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) { + token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, "ezbookkeeping Cli", s.CurrentConfig().TokenExpiredTimeDuration) + return token, tokenRecord, err +} + // CreateToken generates a new normal token and saves to database func (s *TokenService) CreateToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), s.CurrentConfig().TokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), s.CurrentConfig().TokenExpiredTimeDuration) + return token, claims, err } // CreateRequire2FAToken generates a new token requiring user to verify 2fa passcode and saves to database func (s *TokenService) CreateRequire2FAToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), s.CurrentConfig().TemporaryTokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_REQUIRE_2FA, s.getUserAgent(c), s.CurrentConfig().TemporaryTokenExpiredTimeDuration) + return token, claims, err } // CreateEmailVerifyToken generates a new email verify token and saves to database func (s *TokenService) CreateEmailVerifyToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, s.getUserAgent(c), s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration) + return token, claims, err } // CreateEmailVerifyTokenWithoutUserAgent generates a new email verify token and saves to database func (s *TokenService) CreateEmailVerifyTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, "", s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_EMAIL_VERIFY, "", s.CurrentConfig().EmailVerifyTokenExpiredTimeDuration) + return token, claims, err } // CreatePasswordResetToken generates a new password reset token and saves to database func (s *TokenService) CreatePasswordResetToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, s.getUserAgent(c), s.CurrentConfig().PasswordResetTokenExpiredTimeDuration) + return token, claims, err } // CreatePasswordResetTokenWithoutUserAgent generates a new password reset token and saves to database func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context, user *models.User) (string, *core.UserTokenClaims, error) { - return s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, "", s.CurrentConfig().PasswordResetTokenExpiredTimeDuration) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_PASSWORD_RESET, "", s.CurrentConfig().PasswordResetTokenExpiredTimeDuration) + return token, claims, err } // UpdateTokenLastSeen updates the last seen time of specified token @@ -350,7 +362,7 @@ func (s *TokenService) parseToken(c *core.WebContext, extractor request.Extracto return token, claims, err } -func (s *TokenService) createToken(c core.Context, user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, error) { +func (s *TokenService) createToken(c core.Context, user *models.User, tokenType core.TokenType, userAgent string, expiryDate time.Duration) (string, *core.UserTokenClaims, *models.TokenRecord, error) { var err error now := time.Now() @@ -365,7 +377,7 @@ func (s *TokenService) createToken(c core.Context, user *models.User, tokenType } if tokenRecord.Secret, err = utils.GetRandomString(10); err != nil { - return "", nil, err + return "", nil, nil, err } claims := &core.UserTokenClaims{ @@ -381,16 +393,16 @@ func (s *TokenService) createToken(c core.Context, user *models.User, tokenType tokenString, err := jwtToken.SignedString([]byte(tokenRecord.Secret)) if err != nil { - return "", nil, err + return "", nil, nil, err } err = s.createTokenRecord(c, tokenRecord) if err != nil { - return "", nil, err + return "", nil, nil, err } - return tokenString, claims, err + return tokenString, claims, tokenRecord, err } func (s *TokenService) getTokenRecord(c core.Context, uid int64, userTokenId int64, createUnixTime int64) (*models.TokenRecord, error) { diff --git a/src/lib/misc.js b/src/lib/misc.js index 98e40499..0232d8b6 100644 --- a/src/lib/misc.js +++ b/src/lib/misc.js @@ -99,6 +99,10 @@ export function generateRandomUUID() { return uuid; } +export function isSessionUserAgentCreatedByCli(ua) { + return ua === 'ezbookkeeping Cli'; +} + export function parseUserAgent(ua) { const uaParseRet = uaParser(ua); @@ -119,13 +123,16 @@ export function parseUserAgent(ua) { }; } -export function parseDeviceInfo(ua) { - const uaInfo = parseUserAgent(ua); +export function parseDeviceInfo(uaInfo) { + if (!uaInfo) { + return ''; + } + let result = ''; - if (uaInfo.device.model) { + if (uaInfo.device && uaInfo.device.model) { result = uaInfo.device.model; - } else if (uaInfo.os.name) { + } else if (uaInfo.os && uaInfo.os.name) { result = uaInfo.os.name; if (uaInfo.os.version) { @@ -133,7 +140,7 @@ export function parseDeviceInfo(ua) { } } - if (uaInfo.browser.name) { + if (uaInfo.browser && uaInfo.browser.name) { let browserInfo = uaInfo.browser.name; if (uaInfo.browser.version) { @@ -154,6 +161,41 @@ export function parseDeviceInfo(ua) { return result; } +export function parseSessionInfo(token) { + const isCreatedByCli = isSessionUserAgentCreatedByCli(token.userAgent); + const uaInfo = parseUserAgent(token.userAgent); + let deviceType = ''; + + if (isCreatedByCli) { + deviceType = 'cli'; + } else { + if (uaInfo && uaInfo.device) { + if (uaInfo.device.type === 'mobile') { + deviceType = 'phone'; + } else if (uaInfo.device.type === 'wearable') { + deviceType = 'wearable'; + } else if (uaInfo.device.type === 'tablet') { + deviceType = 'tablet'; + } else if (uaInfo.device.type === 'smarttv') { + deviceType = 'tv'; + } else { + deviceType = 'default'; + } + } else { + deviceType = 'default'; + } + } + + return { + tokenId: token.tokenId, + isCurrent: token.isCurrent, + deviceType: deviceType, + deviceInfo: isCreatedByCli ? token.userAgent : parseDeviceInfo(uaInfo), + createdByCli: isCreatedByCli, + lastSeen: token.lastSeen + } +} + export function makeButtonCopyToClipboard({ text, el, successCallback, errorCallback }) { const clipboard = new Clipboard(el, { text: function () { diff --git a/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue b/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue index 38aa42a1..a38a7f79 100644 --- a/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue +++ b/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue @@ -106,10 +106,10 @@ v-for="session in sessions"> - {{ session.deviceType }} + {{ $t(session.isCurrent ? 'Current' : 'Other Device') }} {{ session.deviceInfo }} - {{ session.lastSeen }} + {{ session.lastSeenDateTime }} @@ -32,7 +32,7 @@ @@ -48,7 +48,7 @@ import { useUserStore } from '@/stores/user.js'; import { useTokensStore } from '@/stores/token.js'; import { isEquals } from '@/lib/common.js'; -import { parseDeviceInfo, parseUserAgent } from '@/lib/misc.js'; +import { parseSessionInfo } from '@/lib/misc.js'; import { onSwipeoutDeleted } from '@/lib/ui.mobile.js'; @@ -74,16 +74,11 @@ export default { for (let i = 0; i < this.tokens.length; i++) { const token = this.tokens[i]; - - sessions.push({ - tokenId: token.tokenId, - domId: this.getTokenDomId(token.tokenId), - isCurrent: token.isCurrent, - deviceType: this.$t(token.isCurrent ? 'Current' : 'Other Device'), - deviceInfo: parseDeviceInfo(token.userAgent), - icon: this.getTokenIcon(token), - lastSeen: token.lastSeen ? this.$locale.formatUnixTimeToLongDateTime(this.userStore, token.lastSeen) : '-' - }); + const sessionInfo = parseSessionInfo(token); + sessionInfo.domId = this.getTokenDomId(sessionInfo.tokenId); + sessionInfo.icon = this.getTokenIcon(sessionInfo.deviceType); + sessionInfo.lastSeenDateTime = sessionInfo.lastSeen ? this.$locale.formatUnixTimeToLongDateTime(this.userStore, sessionInfo.lastSeen) : '-'; + sessions.push(sessionInfo); } return sessions; @@ -191,21 +186,17 @@ export default { }); }); }, - getTokenIcon(token) { - const ua = parseUserAgent(token.userAgent); - - if (!ua || !ua.device) { - return 'device_desktop'; - } - - if (ua.device.type === 'mobile') { + getTokenIcon(deviceType) { + if (deviceType === 'phone') { return 'device_phone_portrait'; - } else if (ua.device.type === 'wearable') { + } else if (deviceType === 'wearable') { return 'device_phone_portrait'; - } else if (ua.device.type === 'tablet') { + } else if (deviceType === 'tablet') { return 'device_tablet_portrait'; - } else if (ua.device.type === 'smarttv') { + } else if (deviceType === 'tv') { return 'tv'; + } else if (deviceType === 'cli') { + return 'chevron_left_slash_chevron_right'; } else { return 'device_desktop'; }