diff --git a/cmd/user_data.go b/cmd/user_data.go index 7cda40a0..020c4894 100644 --- a/cmd/user_data.go +++ b/cmd/user_data.go @@ -260,6 +260,12 @@ var UserData = &cli.Command{ Required: true, Usage: "Specific user name", }, + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + Required: false, + Usage: "Specific token type, supports \"normal\" and \"mcp\", default is \"normal\"", + }, }, }, { @@ -702,7 +708,18 @@ func createNewUserToken(c *core.CliContext) error { } username := c.String("username") - token, tokenString, err := clis.UserData.CreateNewUserToken(c, username) + tokenType := c.String("type") + + if tokenType == "" { + tokenType = "normal" + } + + if tokenType != "normal" && tokenType != "mcp" { + log.CliErrorf(c, "[user_data.createNewUserToken] token type is invalid") + return nil + } + + token, tokenString, err := clis.UserData.CreateNewUserToken(c, username, tokenType) if err != nil { log.CliErrorf(c, "[user_data.createNewUserToken] error occurs when creating user token") diff --git a/cmd/webserver.go b/cmd/webserver.go index 07bad6a7..442975a1 100644 --- a/cmd/webserver.go +++ b/cmd/webserver.go @@ -226,7 +226,7 @@ func startWebServer(c *core.CliContext) error { mcpRoute.Use(bindMiddleware(middlewares.RequestId(config))) mcpRoute.Use(bindMiddleware(middlewares.RequestLog)) mcpRoute.Use(bindMiddleware(middlewares.MCPServerIpLimit(config))) - mcpRoute.Use(bindMiddleware(middlewares.JWTAuthorization)) + mcpRoute.Use(bindMiddleware(middlewares.JWTMCPAuthorization)) { mcpRoute.POST("", bindJSONRPCApi(map[string]core.JSONRPCApiHandlerFunc{ "initialize": api.ModelContextProtocols.InitializeHandler, @@ -289,6 +289,7 @@ func startWebServer(c *core.CliContext) error { { // Tokens apiV1Route.GET("/tokens/list.json", bindApi(api.Tokens.TokenListHandler)) + apiV1Route.POST("/tokens/generate/mcp.json", bindApi(api.Tokens.TokenGenerateMCPHandler)) apiV1Route.POST("/tokens/revoke.json", bindApi(api.Tokens.TokenRevokeHandler)) apiV1Route.POST("/tokens/revoke_all.json", bindApi(api.Tokens.TokenRevokeAllHandler)) apiV1Route.POST("/tokens/refresh.json", bindApiWithTokenUpdate(api.Tokens.TokenRefreshHandler, config)) diff --git a/pkg/api/tokens.go b/pkg/api/tokens.go index 99e64cb9..e6595232 100644 --- a/pkg/api/tokens.go +++ b/pkg/api/tokens.go @@ -46,7 +46,7 @@ var ( // TokenListHandler returns available token list of current user func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) { uid := c.GetCurrentUid() - tokens, err := a.tokens.GetAllUnexpiredNormalTokensByUid(c, uid) + tokens, err := a.tokens.GetAllUnexpiredNormalAndMCPTokensByUid(c, uid) if err != nil { log.Errorf(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error()) @@ -69,6 +69,10 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) { tokenResp.IsCurrent = true } + if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli { + tokenResp.UserAgent = services.TokenUserAgentForMCP + } + tokenResps[i] = tokenResp } @@ -77,6 +81,45 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) { return tokenResps, nil } +// TokenGenerateMCPHandler generates a new MCP token for current user +func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) { + var generateMCPTokenReq models.TokenGenerateMCPRequest + err := c.ShouldBindJSON(&generateMCPTokenReq) + + if err != nil { + log.Warnf(c, "[tokens.TokenGenerateMCPHandler] parse request failed, because %s", err.Error()) + return nil, errs.NewIncompleteOrIncorrectSubmissionError(err) + } + + uid := c.GetCurrentUid() + user, err := a.users.GetUserById(c, uid) + + if err != nil { + log.Warnf(c, "[tokens.TokenGenerateMCPHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error()) + return nil, errs.ErrUserNotFound + } + + if !a.users.IsPasswordEqualsUserPassword(generateMCPTokenReq.Password, user) { + return nil, errs.ErrUserPasswordWrong + } + + token, claims, err := a.tokens.CreateMCPToken(c, user) + + if err != nil { + log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error()) + return nil, errs.Or(err, errs.ErrTokenGenerating) + } + + log.Infof(c, "[tokens.TokenGenerateMCPHandler] user \"uid:%d\" has generated mcp token, new token will be expired at %d", user.Uid, claims.ExpiresAt) + + generateMCPTokenResp := &models.TokenGenerateMCPResponse{ + Token: token, + MCPUrl: a.CurrentConfig().RootUrl + "mcp", + } + + return generateMCPTokenResp, nil +} + // TokenRevokeCurrentHandler revokes current token of current user func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) { _, claims, err := a.tokens.ParseTokenByHeader(c) diff --git a/pkg/cli/user_data.go b/pkg/cli/user_data.go index 9c719c1a..13c8417c 100644 --- a/pkg/cli/user_data.go +++ b/pkg/cli/user_data.go @@ -394,7 +394,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo return nil, err } - tokens, err := l.tokens.GetAllUnexpiredNormalTokensByUid(c, uid) + tokens, err := l.tokens.GetAllUnexpiredNormalAndMCPTokensByUid(c, uid) if err != nil { log.CliErrorf(c, "[user_data.ListUserTokens] failed to get tokens of user \"%s\", because %s", username, err.Error()) @@ -405,7 +405,7 @@ func (l *UserDataCli) ListUserTokens(c *core.CliContext, username string) ([]*mo } // CreateNewUserToken returns a new token for the specified user -func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string) (*models.TokenRecord, string, error) { +func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string) (*models.TokenRecord, string, error) { if username == "" { log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty") return nil, "", errs.ErrUsernameIsEmpty @@ -418,7 +418,16 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string) (* return nil, "", err } - token, tokenRecord, err := l.tokens.CreateTokenViaCli(c, user) + var token string + var tokenRecord *models.TokenRecord + + if tokenType == "mcp" { + token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user) + } else if tokenType == "normal" { + token, tokenRecord, err = l.tokens.CreateTokenViaCli(c, user) + } else { + return nil, "", errs.ErrParameterInvalid + } if err != nil { log.CliErrorf(c, "[user_data.CreateNewUserToken] failed to create token for user \"%s\", because %s", username, err.Error()) diff --git a/pkg/core/token_claims.go b/pkg/core/token_claims.go index 33dca977..2ecadb84 100644 --- a/pkg/core/token_claims.go +++ b/pkg/core/token_claims.go @@ -15,6 +15,7 @@ const ( USER_TOKEN_TYPE_REQUIRE_2FA TokenType = 2 USER_TOKEN_TYPE_EMAIL_VERIFY TokenType = 3 USER_TOKEN_TYPE_PASSWORD_RESET TokenType = 4 + USER_TOKEN_TYPE_MCP TokenType = 5 ) // UserTokenClaims represents user token diff --git a/pkg/middlewares/authorization.go b/pkg/middlewares/authorization.go index f6fcc271..11bc5fd5 100644 --- a/pkg/middlewares/authorization.go +++ b/pkg/middlewares/authorization.go @@ -94,6 +94,25 @@ func JWTResetPasswordAuthorization(c *core.WebContext) { c.Next() } +// JWTMCPAuthorization verifies whether current request is valid by jwt mcp token in header +func JWTMCPAuthorization(c *core.WebContext) { + claims, err := getTokenClaims(c, TOKEN_SOURCE_TYPE_HEADER) + + if err != nil { + utils.PrintJsonErrorResult(c, err) + return + } + + if claims.Type != core.USER_TOKEN_TYPE_MCP { + log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is not mcp token", claims.Uid, claims.Type) + utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType) + return + } + + c.SetTokenClaims(claims) + c.Next() +} + func jwtAuthorization(c *core.WebContext, source TokenSourceType) { claims, err := getTokenClaims(c, source) @@ -109,7 +128,7 @@ func jwtAuthorization(c *core.WebContext, source TokenSourceType) { } if claims.Type != core.USER_TOKEN_TYPE_NORMAL { - log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type is invalid", claims.Uid) + log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is invalid", claims.Uid, claims.Type) utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType) return } diff --git a/pkg/models/token_record.go b/pkg/models/token_record.go index dfc02d5d..2749befb 100644 --- a/pkg/models/token_record.go +++ b/pkg/models/token_record.go @@ -17,11 +17,22 @@ type TokenRecord struct { LastSeenUnixTime int64 } +// TokenGenerateMCPRequest represents all parameters of mcp token generation request +type TokenGenerateMCPRequest struct { + Password string `json:"password" binding:"omitempty,min=6,max=128"` +} + // TokenRevokeRequest represents all parameters of token revoking request type TokenRevokeRequest struct { TokenId string `json:"tokenId" binding:"required,notBlank"` } +// TokenGenerateMCPResponse represents all response parameters of generated mcp token +type TokenGenerateMCPResponse struct { + Token string `json:"token"` + MCPUrl string `json:"mcpUrl"` +} + // TokenRefreshResponse represents all parameters of token refreshing request type TokenRefreshResponse struct { NewToken string `json:"newToken,omitempty"` diff --git a/pkg/services/tokens.go b/pkg/services/tokens.go index e60b6181..a7c99c47 100644 --- a/pkg/services/tokens.go +++ b/pkg/services/tokens.go @@ -19,6 +19,14 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/utils" ) +// TokenUserAgentCreatedViaCli is the user agent of token created via cli +const TokenUserAgentCreatedViaCli = "ezbookkeeping Cli" + +// TokenUserAgentForMCP is the user agent for MCP token +const TokenUserAgentForMCP = "ezbookkeeping MCP" + +const tokenMaxExpiredAtUnixTime = int64(253402300799) // 9999-12-31 23:59:59 UTC + // TokenService represents user token service type TokenService struct { ServiceUsingDB @@ -49,8 +57,8 @@ func (s *TokenService) GetAllTokensByUid(c core.Context, uid int64) ([]*models.T return tokenRecords, err } -// GetAllUnexpiredNormalTokensByUid returns all available token models of given user -func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c core.Context, uid int64) ([]*models.TokenRecord, error) { +// GetAllUnexpiredNormalAndMCPTokensByUid returns all available token models of given user +func (s *TokenService) GetAllUnexpiredNormalAndMCPTokensByUid(c core.Context, uid int64) ([]*models.TokenRecord, error) { if uid <= 0 { return nil, errs.ErrUserIdInvalid } @@ -58,7 +66,7 @@ func (s *TokenService) GetAllUnexpiredNormalTokensByUid(c core.Context, uid int6 now := time.Now().Unix() var tokenRecords []*models.TokenRecord - err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND token_type=? AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, now).Find(&tokenRecords) + err := s.TokenDB(uid).NewSession(c).Cols("uid", "user_token_id", "token_type", "user_agent", "created_unix_time", "expired_unix_time", "last_seen_unix_time").Where("uid=? AND (token_type=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, now).Find(&tokenRecords) return tokenRecords, err } @@ -80,7 +88,7 @@ func (s *TokenService) ParseTokenByCookie(c *core.WebContext, tokenCookieName st // 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) + token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, TokenUserAgentCreatedViaCli, s.CurrentConfig().TokenExpiredTimeDuration) return token, tokenRecord, err } @@ -120,6 +128,20 @@ func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context, return token, claims, err } +// CreateMCPToken generates a new MCP token and saves to database +func (s *TokenService) CreateMCPToken(c *core.WebContext, user *models.User) (string, *core.UserTokenClaims, error) { + tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now()) + token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, s.getUserAgent(c), tokenExpiredTimeDuration) + return token, claims, err +} + +// CreateMCPTokenViaCli generates a new MCP token and saves to database +func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.User) (string, *models.TokenRecord, error) { + tokenExpiredTimeDuration := time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now()) + token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_MCP, TokenUserAgentCreatedViaCli, tokenExpiredTimeDuration) + return token, tokenRecord, err +} + // UpdateTokenLastSeen updates the last seen time of specified token func (s *TokenService) UpdateTokenLastSeen(c core.Context, tokenRecord *models.TokenRecord) error { if tokenRecord.Uid <= 0 { diff --git a/src/lib/services.ts b/src/lib/services.ts index effec309..b55c6db6 100644 --- a/src/lib/services.ts +++ b/src/lib/services.ts @@ -106,6 +106,8 @@ import type { TransactionTemplateInfoResponse } from '@/models/transaction_template.ts'; import type { + TokenGenerateMCPRequest, + TokenGenerateMCPResponse, TokenRefreshResponse, TokenInfoResponse } from '@/models/token.ts'; @@ -289,6 +291,9 @@ export default { getTokens: (): ApiResponsePromise => { return axios.get>('v1/tokens/list.json'); }, + generateMCPToken: (req: TokenGenerateMCPRequest): ApiResponsePromise => { + return axios.post>('v1/tokens/generate/mcp.json', req); + }, revokeToken: ({ tokenId, ignoreError }: { tokenId: string, ignoreError?: boolean }): ApiResponsePromise => { return axios.post>('v1/tokens/revoke.json', { tokenId: tokenId diff --git a/src/lib/session.ts b/src/lib/session.ts index 5262c84a..3d9b3b9f 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -1,6 +1,11 @@ import uaParser from 'ua-parser-js'; -import { TOKEN_CLI_USER_AGENT, type TokenInfoResponse, SessionInfo } from '@/models/token.ts'; +import { + TOKEN_TYPE_MCP, + TOKEN_CLI_USER_AGENT, + type TokenInfoResponse, + SessionInfo +} from '@/models/token.ts'; interface UserAgentInfo { readonly device: { @@ -81,11 +86,14 @@ function parseDeviceInfo(uaInfo: UserAgentInfo): string { } export function parseSessionInfo(token: TokenInfoResponse): SessionInfo { + const isCreateForMCP = token.tokenType === TOKEN_TYPE_MCP; const isCreatedByCli = isSessionUserAgentCreatedByCli(token.userAgent); const uaInfo = parseUserAgent(token.userAgent); let deviceType = ''; - if (isCreatedByCli) { + if (isCreateForMCP) { + deviceType = 'mcp'; + } else if (isCreatedByCli) { deviceType = 'cli'; } else { if (uaInfo && uaInfo.device) { @@ -109,7 +117,7 @@ export function parseSessionInfo(token: TokenInfoResponse): SessionInfo { token.tokenId, token.isCurrent, deviceType, - isCreatedByCli ? token.userAgent : parseDeviceInfo(uaInfo), + isCreateForMCP || isCreatedByCli ? token.userAgent : parseDeviceInfo(uaInfo), isCreatedByCli, token.lastSeen ); diff --git a/src/locales/de.json b/src/locales/de.json index 8802d49d..823c78f7 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1331,6 +1331,7 @@ "Update": "Aktualisieren", "Refresh": "Aktualisieren", "Clear": "Löschen", + "Generate": "Generate", "None": "Keine", "Unspecified": "Nicht angegeben", "Not set": "Nicht festgelegt", @@ -1349,6 +1350,8 @@ "Enabled": "Aktiviert", "Disable": "Deaktivieren", "Disabled": "Deaktiviert", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Kopieren", "Visible": "Sichtbar", "Show": "Anzeigen", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Versteckte Transaktionsvorlagen anzeigen", "Hide Hidden Transaction Templates": "Versteckte Transaktionsvorlagen ausblenden", "Template name cannot be blank": "Vorlagenname darf nicht leer sein", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Sind Sie sicher, dass Sie sich von dieser Sitzung abmelden möchten?", "Unable to logout from this session": "Abmeldung von dieser Sitzung nicht möglich", "Are you sure you want to logout all other sessions?": "Sind Sie sicher, dass Sie alle anderen Sitzungen abmelden möchten?", diff --git a/src/locales/en.json b/src/locales/en.json index 2c7e4567..58e38560 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1331,6 +1331,7 @@ "Update": "Update", "Refresh": "Refresh", "Clear": "Clear", + "Generate": "Generate", "None": "None", "Unspecified": "Unspecified", "Not set": "Not set", @@ -1349,6 +1350,8 @@ "Enabled": "Enabled", "Disable": "Disable", "Disabled": "Disabled", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Copy", "Visible": "Visible", "Show": "Show", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Show Hidden Transaction Templates", "Hide Hidden Transaction Templates": "Hide Hidden Transaction Templates", "Template name cannot be blank": "Template name cannot be blank", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "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", "Are you sure you want to logout all other sessions?": "Are you sure you want to logout all other sessions?", diff --git a/src/locales/es.json b/src/locales/es.json index 82bd2be8..547ebdd3 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1331,6 +1331,7 @@ "Update": "Actualizar", "Refresh": "Refrescar", "Clear": "Claro", + "Generate": "Generate", "None": "Ninguno", "Unspecified": "No especificado", "Not set": "No establecido", @@ -1349,6 +1350,8 @@ "Enabled": "Activado", "Disable": "Desactivar", "Disabled": "Desactivado", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Copiar", "Visible": "Visible", "Show": "Mostrar", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Mostrar plantillas de transacciones ocultas", "Hide Hidden Transaction Templates": "Ocultar plantillas de transacciones ocultas", "Template name cannot be blank": "El nombre de la plantilla no puede estar en blanco", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "¿Está seguro de que desea cerrar sesión en esta sesión?", "Unable to logout from this session": "No se puede cerrar sesión en esta sesión", "Are you sure you want to logout all other sessions?": "¿Está seguro de que desea cerrar sesión en todas las demás sesiones?", diff --git a/src/locales/it.json b/src/locales/it.json index bf64ab3f..80bdabc6 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -1331,6 +1331,7 @@ "Update": "Aggiorna", "Refresh": "Aggiorna", "Clear": "Pulisci", + "Generate": "Generate", "None": "Nessuno", "Unspecified": "Non specificato", "Not set": "Non impostato", @@ -1349,6 +1350,8 @@ "Enabled": "Abilitato", "Disable": "Disabilita", "Disabled": "Disabilitato", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Copia", "Visible": "Visibile", "Show": "Mostra", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Mostra modelli transazione nascosti", "Hide Hidden Transaction Templates": "Nascondi modelli transazione nascosti", "Template name cannot be blank": "Il nome del modello non può essere vuoto", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Sei sicuro di voler uscire da questa sessione?", "Unable to logout from this session": "Impossibile uscire da questa sessione", "Are you sure you want to logout all other sessions?": "Sei sicuro di voler uscire da tutte le altre sessioni?", diff --git a/src/locales/ja.json b/src/locales/ja.json index 5b66a858..f132d5af 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1331,6 +1331,7 @@ "Update": "アップデート", "Refresh": "リフレッシュ", "Clear": "消去", + "Generate": "Generate", "None": "なし", "Unspecified": "不特定", "Not set": "セットしていない", @@ -1349,6 +1350,8 @@ "Enabled": "有効になっています", "Disable": "無効", "Disabled": "無効になっています", + "Configuration": "Configuration", + "Token": "Token", "Copy": "コピー", "Visible": "見える", "Show": "表示", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "非表示取引テンプレートを表示します", "Hide Hidden Transaction Templates": "非表示取引テンプレートを非表示にします", "Template name cannot be blank": "テンプレート名は空欄にできません", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "このセッションからログアウトしますか?", "Unable to logout from this session": "このセッションからログアウトできません", "Are you sure you want to logout all other sessions?": "他のすべてのセッションをログアウトしますか?", diff --git a/src/locales/pt_BR.json b/src/locales/pt_BR.json index 1467f35d..d5f09bb1 100644 --- a/src/locales/pt_BR.json +++ b/src/locales/pt_BR.json @@ -1331,6 +1331,7 @@ "Update": "Atualizar", "Refresh": "Atualizar", "Clear": "Limpar", + "Generate": "Generate", "None": "Nenhum", "Unspecified": "Não especificado", "Not set": "Não definido", @@ -1349,6 +1350,8 @@ "Enabled": "Habilitado", "Disable": "Desabilitar", "Disabled": "Desabilitado", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Copiar", "Visible": "Visível", "Show": "Mostrar", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Mostrar Modelos de Transação Ocultos", "Hide Hidden Transaction Templates": "Ocultar Modelos de Transação Ocultos", "Template name cannot be blank": "O nome do modelo não pode estar em branco", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Tem certeza de que deseja sair desta sessão?", "Unable to logout from this session": "Não foi possível sair desta sessão", "Are you sure you want to logout all other sessions?": "Tem certeza de que deseja sair de todas as outras sessões?", diff --git a/src/locales/ru.json b/src/locales/ru.json index 02252dd9..705be009 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1331,6 +1331,7 @@ "Update": "Обновить", "Refresh": "Обновить", "Clear": "Очистить", + "Generate": "Generate", "None": "Нет", "Unspecified": "Не указано", "Not set": "Не установлено", @@ -1349,6 +1350,8 @@ "Enabled": "Включено", "Disable": "Отключить", "Disabled": "Отключено", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Копировать", "Visible": "Видимый", "Show": "Показать", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Показать скрытые шаблоны транзакций", "Hide Hidden Transaction Templates": "Скрыть скрытые шаблоны транзакций", "Template name cannot be blank": "Название шаблона не может быть пустым", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Вы уверены, что хотите выйти из этой сессии?", "Unable to logout from this session": "Не удалось выйти из этой сессии", "Are you sure you want to logout all other sessions?": "Вы уверены, что хотите выйти из всех других сессий?", diff --git a/src/locales/uk.json b/src/locales/uk.json index 8a7b2430..565f772c 100644 --- a/src/locales/uk.json +++ b/src/locales/uk.json @@ -1331,6 +1331,7 @@ "Update": "Оновити", "Refresh": "Оновити", "Clear": "Очистити", + "Generate": "Generate", "None": "Немає", "Unspecified": "Не вказано", "Not set": "Не встановлено", @@ -1349,6 +1350,8 @@ "Enabled": "Увімкнено", "Disable": "Вимкнути", "Disabled": "Вимкнено", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Копіювати", "Visible": "Видимий", "Show": "Показати", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Показати приховані шаблони транзакцій", "Hide Hidden Transaction Templates": "Приховати приховані шаблони транзакцій", "Template name cannot be blank": "Назва шаблону не може бути порожньою", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Ви впевнені, що хочете вийти з цієї сесії?", "Unable to logout from this session": "Не вдалося вийти з цієї сесії", "Are you sure you want to logout all other sessions?": "Ви впевнені, що хочете вийти з усіх інших сесій?", diff --git a/src/locales/vi.json b/src/locales/vi.json index 69aa93f7..8b89e8ec 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1331,6 +1331,7 @@ "Update": "Cập nhật", "Refresh": "Làm mới", "Clear": "Xóa", + "Generate": "Generate", "None": "Không có", "Unspecified": "Không xác định", "Not set": "Not set", @@ -1349,6 +1350,8 @@ "Enabled": "Đã bật", "Disable": "Tắt", "Disabled": "Đã tắt", + "Configuration": "Configuration", + "Token": "Token", "Copy": "Sao chép", "Visible": "Hiển thị", "Show": "Hiển thị", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "Hiển thị mẫu giao dịch ẩn", "Hide Hidden Transaction Templates": "Ẩn mẫu giao dịch ẩn", "Template name cannot be blank": "Tên mẫu không được để trống", + "Generate MCP token": "Generate MCP token", + "Unable to generate token": "Unable to generate token", "Are you sure you want to logout from this session?": "Bạn có chắc chắn muốn đăng xuất khỏi phiên này không?", "Unable to logout from this session": "Không thể đăng xuất khỏi phiên này", "Are you sure you want to logout all other sessions?": "Bạn có chắc chắn muốn đăng xuất khỏi tất cả các phiên khác không?", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index dc365f11..f210a0d2 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1331,6 +1331,7 @@ "Update": "更新", "Refresh": "刷新", "Clear": "清除", + "Generate": "生成", "None": "无", "Unspecified": "未指定", "Not set": "未设置", @@ -1349,6 +1350,8 @@ "Enabled": "启用", "Disable": "禁用", "Disabled": "禁用", + "Configuration": "配置", + "Token": "令牌", "Copy": "复制", "Visible": "可见", "Show": "显示", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "显示隐藏的模板", "Hide Hidden Transaction Templates": "不显示隐藏的模板", "Template name cannot be blank": "模板名不能为空", + "Generate MCP token": "生成 MCP 令牌", + "Unable to generate token": "无法生成令牌", "Are you sure you want to logout from this session?": "您确定要退出该会话?", "Unable to logout from this session": "无法退出该会话", "Are you sure you want to logout all other sessions?": "您确定要退出其他所有会话?", diff --git a/src/locales/zh_Hant.json b/src/locales/zh_Hant.json index 159a5e45..50ba36e6 100644 --- a/src/locales/zh_Hant.json +++ b/src/locales/zh_Hant.json @@ -1331,6 +1331,7 @@ "Update": "更新", "Refresh": "重新載入", "Clear": "清除", + "Generate": "產生", "None": "無", "Unspecified": "未指定", "Not set": "未設置", @@ -1349,6 +1350,8 @@ "Enabled": "啟用", "Disable": "停用", "Disabled": "停用", + "Configuration": "設定", + "Token": "令牌", "Copy": "複製", "Visible": "可見", "Show": "顯示", @@ -2052,6 +2055,8 @@ "Show Hidden Transaction Templates": "顯示隱藏的範本", "Hide Hidden Transaction Templates": "不顯示隱藏的範本", "Template name cannot be blank": "範本名稱不能為空", + "Generate MCP token": "產生 MCP 令牌", + "Unable to generate token": "無法產生令牌", "Are you sure you want to logout from this session?": "您確定要登出此會話?", "Unable to logout from this session": "無法登出此會話", "Are you sure you want to logout all other sessions?": "您確定要登出其他所有會話?", diff --git a/src/models/token.ts b/src/models/token.ts index 3fdb8046..02d6ef3b 100644 --- a/src/models/token.ts +++ b/src/models/token.ts @@ -2,8 +2,19 @@ import type { ApplicationCloudSetting } from '@/core/setting.ts'; import type { UserBasicInfo } from './user.ts'; +export const TOKEN_TYPE_MCP: number = 5; + export const TOKEN_CLI_USER_AGENT: string = 'ezbookkeeping Cli'; +export interface TokenGenerateMCPRequest { + readonly password: string; +} + +export interface TokenGenerateMCPResponse { + readonly token: string; + readonly mcpUrl: string; +} + export interface TokenRefreshResponse { readonly newToken?: string; readonly oldTokenId?: string; diff --git a/src/stores/token.ts b/src/stores/token.ts index 8133300d..348d8133 100644 --- a/src/stores/token.ts +++ b/src/stores/token.ts @@ -3,7 +3,7 @@ import { defineStore } from 'pinia'; import { useSettingsStore } from './setting.ts'; import { useUserStore } from './user.ts'; -import type { TokenRefreshResponse, TokenInfoResponse } from '@/models/token.ts'; +import type { TokenGenerateMCPResponse, TokenRefreshResponse, TokenInfoResponse } from '@/models/token.ts'; import { isObject } from '@/lib/common.ts'; import { updateCurrentToken } from '@/lib/userstate.ts'; @@ -69,6 +69,31 @@ export const useTokensStore = defineStore('tokens', () => { }); } + function generateMCPToken({ password }: { password: string }): Promise { + return new Promise((resolve, reject) => { + services.generateMCPToken({ password }).then(response => { + const data = response.data; + + if (!data || !data.success || !data.result) { + reject({ message: 'Unable to generate token' }); + return; + } + + resolve(data.result); + }).catch(error => { + logger.error('failed to generate token', error); + + if (error.response && error.response.data && error.response.data.errorMessage) { + reject({ error: error.response.data }); + } else if (!error.processed) { + reject({ message: 'Unable to generate token' }); + } else { + reject(error); + } + }); + }); + } + function revokeToken({ tokenId, ignoreError }: { tokenId: string, ignoreError?: boolean }): Promise { return new Promise((resolve, reject) => { services.revokeToken({ tokenId, ignoreError }).then(response => { @@ -123,6 +148,7 @@ export const useTokensStore = defineStore('tokens', () => { // functions getAllTokens, refreshTokenAndRevokeOldToken, + generateMCPToken, revokeToken, revokeAllTokens }; diff --git a/src/views/desktop/user/settings/dialogs/UserGenerateMCPTokenDialog.vue b/src/views/desktop/user/settings/dialogs/UserGenerateMCPTokenDialog.vue new file mode 100644 index 00000000..2693737b --- /dev/null +++ b/src/views/desktop/user/settings/dialogs/UserGenerateMCPTokenDialog.vue @@ -0,0 +1,162 @@ + + + diff --git a/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue b/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue index ddaa73e6..a82086c1 100644 --- a/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue +++ b/src/views/desktop/user/settings/tabs/UserSecuritySettingTab.vue @@ -68,6 +68,8 @@