mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-14 06:57:35 +08:00
generate API token in frontend page
This commit is contained in:
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user