generate API token in frontend page

This commit is contained in:
MaysWind
2025-11-03 01:27:45 +08:00
parent bb84e8af13
commit b0e01d36ab
38 changed files with 770 additions and 303 deletions
+1
View File
@@ -39,6 +39,7 @@ func (a *ServerSettingsApi) ServerSettingsJavascriptHandler(c *core.WebContext)
a.appendBooleanSetting(builder, "o", config.EnableOAuth2Login)
a.appendBooleanSetting(builder, "r", config.EnableInternalAuth && config.EnableUserRegister)
a.appendBooleanSetting(builder, "f", config.EnableInternalAuth && config.EnableUserForgetPassword)
a.appendBooleanSetting(builder, "t", config.EnableGenerateAPIToken)
a.appendBooleanSetting(builder, "v", config.EnableUserVerifyEmail)
a.appendBooleanSetting(builder, "p", config.EnableTransactionPictures)
a.appendBooleanSetting(builder, "s", config.EnableScheduledTransaction)
+50 -2
View File
@@ -69,7 +69,9 @@ 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 {
if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != services.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForAPI
} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != services.TokenUserAgentCreatedViaCli {
tokenResp.UserAgent = services.TokenUserAgentForMCP
}
@@ -81,6 +83,52 @@ func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
return tokenResps, nil
}
// TokenGenerateAPIHandler generates a new API token for current user
func (a *TokensApi) TokenGenerateAPIHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableGenerateAPIToken {
return nil, errs.ErrNotAllowedToGenerateAPIToken
}
var generateAPITokenReq models.TokenGenerateAPIRequest
err := c.ShouldBindJSON(&generateAPITokenReq)
if err != nil {
log.Warnf(c, "[tokens.TokenGenerateAPIHandler] 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.TokenGenerateAPIHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
return nil, errs.ErrUserNotFound
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
return false, errs.ErrNotPermittedToPerformThisAction
}
if !a.users.IsPasswordEqualsUserPassword(generateAPITokenReq.Password, user) {
return nil, errs.ErrUserPasswordWrong
}
token, claims, err := a.tokens.CreateAPIToken(c, user, generateAPITokenReq.ExpiredInSeconds)
if err != nil {
log.Errorf(c, "[tokens.TokenGenerateAPIHandler] failed to create api token for user \"uid:%d\", because %s", user.Uid, err.Error())
return nil, errs.Or(err, errs.ErrTokenGenerating)
}
log.Infof(c, "[tokens.TokenGenerateAPIHandler] user \"uid:%d\" has generated api token, new token will be expired at %d", user.Uid, claims.ExpiresAt)
generateAPITokenResp := &models.TokenGenerateAPIResponse{
Token: token,
}
return generateAPITokenResp, nil
}
// TokenGenerateMCPHandler generates a new MCP token for current user
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
if !a.CurrentConfig().EnableMCPServer {
@@ -111,7 +159,7 @@ func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Erro
return nil, errs.ErrUserPasswordWrong
}
token, claims, err := a.tokens.CreateMCPToken(c, user)
token, claims, err := a.tokens.CreateMCPToken(c, user, generateMCPTokenReq.ExpiredInSeconds)
if err != nil {
log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
+13 -5
View File
@@ -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, tokenType string) (*models.TokenRecord, string, error) {
func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, tokenType string, expiresInSeconds int64) (*models.TokenRecord, string, error) {
if username == "" {
log.CliErrorf(c, "[user_data.CreateNewUserToken] user name is empty")
return nil, "", errs.ErrUsernameIsEmpty
@@ -421,7 +421,17 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
var token string
var tokenRecord *models.TokenRecord
if tokenType == "mcp" {
if tokenType == "api" {
if !l.CurrentConfig().EnableGenerateAPIToken {
return nil, "", errs.ErrNotAllowedToGenerateAPIToken
}
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
return nil, "", errs.ErrNotPermittedToPerformThisAction
}
token, tokenRecord, err = l.tokens.CreateAPITokenViaCli(c, user, expiresInSeconds)
} else if tokenType == "mcp" {
if !l.CurrentConfig().EnableMCPServer {
return nil, "", errs.ErrMCPServerNotEnabled
}
@@ -430,9 +440,7 @@ func (l *UserDataCli) CreateNewUserToken(c *core.CliContext, username string, to
return nil, "", errs.ErrNotPermittedToPerformThisAction
}
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user)
} else if tokenType == "normal" {
token, tokenRecord, err = l.tokens.CreateTokenViaCli(c, user)
token, tokenRecord, err = l.tokens.CreateMCPTokenViaCli(c, user, expiresInSeconds)
} else {
return nil, "", errs.ErrParameterInvalid
}
+5
View File
@@ -32,6 +32,11 @@ func (c *CliContext) Int(name string) int {
return c.command.Int(name)
}
// Int64 returns the long integer value of parameter
func (c *CliContext) Int64(name string) int64 {
return c.command.Int64(name)
}
// String returns the string value of parameter
func (c *CliContext) String(name string) string {
return c.command.String(name)
+1
View File
@@ -18,6 +18,7 @@ const (
USER_TOKEN_TYPE_MCP TokenType = 5
USER_TOKEN_TYPE_OAUTH2_CALLBACK_REQUIRE_VERIFY TokenType = 6
USER_TOKEN_TYPE_OAUTH2_CALLBACK TokenType = 7
USER_TOKEN_TYPE_API TokenType = 8
)
// UserTokenClaims represents user token
+4 -1
View File
@@ -92,10 +92,11 @@ const (
USER_FEATURE_RESTRICTION_TYPE_CREATE_TRANSACTION_FROM_AI_IMAGE_RECOGNITION UserFeatureRestrictionType = 14
USER_FEATURE_RESTRICTION_TYPE_OAUTH2_LOGIN UserFeatureRestrictionType = 15
USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN UserFeatureRestrictionType = 16
USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN UserFeatureRestrictionType = 17
)
const userFeatureRestrictionTypeMinValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD
const userFeatureRestrictionTypeMaxValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN
const userFeatureRestrictionTypeMaxValue UserFeatureRestrictionType = USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN
// String returns a textual representation of the restriction type of user features
func (t UserFeatureRestrictionType) String() string {
@@ -132,6 +133,8 @@ func (t UserFeatureRestrictionType) String() string {
return "OAuth 2.0 Login"
case USER_FEATURE_RESTRICTION_TYPE_UNLINK_THIRD_PARTY_LOGIN:
return "Unlink Third-Party Login"
case USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN:
return "Generate API Token"
default:
return fmt.Sprintf("Invalid(%d)", int(t))
}
+1
View File
@@ -21,4 +21,5 @@ var (
ErrTokenIsEmpty = NewNormalError(NormalSubcategoryToken, 12, http.StatusBadRequest, "token is empty")
ErrEmailVerifyTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 13, http.StatusBadRequest, "email verify token is invalid or expired")
ErrPasswordResetTokenIsInvalidOrExpired = NewNormalError(NormalSubcategoryToken, 14, http.StatusBadRequest, "password reset token is invalid or expired")
ErrNotAllowedToGenerateAPIToken = NewNormalError(NormalSubcategoryToken, 15, http.StatusForbidden, "not allowed to generate api token")
)
+1 -1
View File
@@ -149,7 +149,7 @@ func jwtAuthorization(c *core.WebContext, source TokenSourceType) {
return
}
if claims.Type != core.USER_TOKEN_TYPE_NORMAL {
if claims.Type != core.USER_TOKEN_TYPE_NORMAL && claims.Type != core.USER_TOKEN_TYPE_API {
log.Warnf(c, "[authorization.jwtAuthorization] user \"uid:%d\" token type (%d) is invalid", claims.Uid, claims.Type)
utils.PrintJsonErrorResult(c, errs.ErrCurrentInvalidTokenType)
return
+13 -1
View File
@@ -25,9 +25,16 @@ type OAuth2CallbackTokenContext struct {
ExternalEmail string `json:"externalEmail"`
}
// TokenGenerateAPIRequest represents all parameters of api token generation request
type TokenGenerateAPIRequest struct {
ExpiredInSeconds int64 `json:"expiresInSeconds" binding:"omitempty,min=0,max=4294967295"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
}
// TokenGenerateMCPRequest represents all parameters of mcp token generation request
type TokenGenerateMCPRequest struct {
Password string `json:"password" binding:"omitempty,min=6,max=128"`
ExpiredInSeconds int64 `json:"expiresInSeconds" binding:"omitempty,min=0,max=4294967295"`
Password string `json:"password" binding:"omitempty,min=6,max=128"`
}
// TokenRevokeRequest represents all parameters of token revoking request
@@ -35,6 +42,11 @@ type TokenRevokeRequest struct {
TokenId string `json:"tokenId" binding:"required,notBlank"`
}
// TokenGenerateAPIResponse represents all response parameters of generated api token
type TokenGenerateAPIResponse struct {
Token string `json:"token"`
}
// TokenGenerateMCPResponse represents all response parameters of generated mcp token
type TokenGenerateMCPResponse struct {
Token string `json:"token"`
+50 -11
View File
@@ -23,6 +23,9 @@ import (
// TokenUserAgentCreatedViaCli is the user agent of token created via cli
const TokenUserAgentCreatedViaCli = "ezbookkeeping Cli"
// TokenUserAgentForAPI is the user agent for API token
const TokenUserAgentForAPI = "ezbookkeeping API"
// TokenUserAgentForMCP is the user agent for MCP token
const TokenUserAgentForMCP = "ezbookkeeping MCP"
@@ -67,7 +70,7 @@ func (s *TokenService) GetAllUnexpiredNormalAndMCPTokensByUid(c core.Context, ui
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=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, 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=? OR token_type=?) AND expired_unix_time>?", uid, core.USER_TOKEN_TYPE_NORMAL, core.USER_TOKEN_TYPE_MCP, core.USER_TOKEN_TYPE_API, now).Find(&tokenRecords)
return tokenRecords, err
}
@@ -77,12 +80,6 @@ func (s *TokenService) ParseToken(c core.Context, token string) (*jwt.Token, *co
return s.parseToken(c, token)
}
// 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, TokenUserAgentCreatedViaCli, "", 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) {
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_NORMAL, s.getUserAgent(c), "", s.CurrentConfig().TokenExpiredTimeDuration)
@@ -119,16 +116,58 @@ func (s *TokenService) CreatePasswordResetTokenWithoutUserAgent(c core.Context,
return token, claims, err
}
// CreateAPIToken generates a new API token and saves to database
func (s *TokenService) CreateAPIToken(c *core.WebContext, user *models.User, expiresInSeconds int64) (string, *core.UserTokenClaims, error) {
var tokenExpiredTimeDuration time.Duration
if expiresInSeconds > 0 {
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
} else {
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
}
token, claims, _, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, s.getUserAgent(c), "", tokenExpiredTimeDuration)
return token, claims, err
}
// CreateAPITokenViaCli generates a new API token and saves to database
func (s *TokenService) CreateAPITokenViaCli(c *core.CliContext, user *models.User, expiresInSeconds int64) (string, *models.TokenRecord, error) {
var tokenExpiredTimeDuration time.Duration
if expiresInSeconds > 0 {
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
} else {
tokenExpiredTimeDuration = time.Unix(tokenMaxExpiredAtUnixTime, 0).Sub(time.Now())
}
token, _, tokenRecord, err := s.createToken(c, user, core.USER_TOKEN_TYPE_API, TokenUserAgentCreatedViaCli, "", tokenExpiredTimeDuration)
return token, tokenRecord, 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())
func (s *TokenService) CreateMCPToken(c *core.WebContext, user *models.User, expiresInSeconds int64) (string, *core.UserTokenClaims, error) {
var tokenExpiredTimeDuration time.Duration
if expiresInSeconds > 0 {
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
} else {
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())
func (s *TokenService) CreateMCPTokenViaCli(c *core.CliContext, user *models.User, expiresInSeconds int64) (string, *models.TokenRecord, error) {
var tokenExpiredTimeDuration time.Duration
if expiresInSeconds > 0 {
tokenExpiredTimeDuration = time.Duration(expiresInSeconds) * time.Second
} else {
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
}
+3
View File
@@ -357,6 +357,7 @@ type Config struct {
EmailVerifyTokenExpiredTimeDuration time.Duration
PasswordResetTokenExpiredTime uint32
PasswordResetTokenExpiredTimeDuration time.Duration
EnableGenerateAPIToken bool
MaxFailuresPerIpPerMinute uint32
MaxFailuresPerUserPerMinute uint32
@@ -977,6 +978,8 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName
config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second
config.EnableGenerateAPIToken = getConfigItemBoolValue(configFile, sectionName, "enable_generate_api_token", false)
config.MaxFailuresPerIpPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_ip_per_minute", defaultMaxFailuresPerIpPerMinute)
config.MaxFailuresPerUserPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_user_per_minute", defaultMaxFailuresPerUserPerMinute)