418 lines
14 KiB
Go
418 lines
14 KiB
Go
package api
|
|
|
|
import (
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/mayswind/ezbookkeeping/pkg/avatars"
|
|
"github.com/mayswind/ezbookkeeping/pkg/core"
|
|
"github.com/mayswind/ezbookkeeping/pkg/errs"
|
|
"github.com/mayswind/ezbookkeeping/pkg/log"
|
|
"github.com/mayswind/ezbookkeeping/pkg/models"
|
|
"github.com/mayswind/ezbookkeeping/pkg/services"
|
|
"github.com/mayswind/ezbookkeeping/pkg/settings"
|
|
"github.com/mayswind/ezbookkeeping/pkg/utils"
|
|
)
|
|
|
|
// TokensApi represents token api
|
|
type TokensApi struct {
|
|
ApiUsingConfig
|
|
ApiWithUserInfo
|
|
tokens *services.TokenService
|
|
users *services.UserService
|
|
userAppCloudSettings *services.UserApplicationCloudSettingsService
|
|
}
|
|
|
|
// Initialize a token api singleton instance
|
|
var (
|
|
Tokens = &TokensApi{
|
|
ApiUsingConfig: ApiUsingConfig{
|
|
container: settings.Container,
|
|
},
|
|
ApiWithUserInfo: ApiWithUserInfo{
|
|
ApiUsingConfig: ApiUsingConfig{
|
|
container: settings.Container,
|
|
},
|
|
ApiUsingAvatarProvider: ApiUsingAvatarProvider{
|
|
container: avatars.Container,
|
|
},
|
|
},
|
|
tokens: services.Tokens,
|
|
users: services.Users,
|
|
userAppCloudSettings: services.UserApplicationCloudSettings,
|
|
}
|
|
)
|
|
|
|
// 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.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())
|
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
|
}
|
|
|
|
tokenResps := make(models.TokenInfoResponseSlice, len(tokens))
|
|
claims := c.GetTokenClaims()
|
|
|
|
for i := 0; i < len(tokens); i++ {
|
|
token := tokens[i]
|
|
tokenResp := &models.TokenInfoResponse{
|
|
TokenId: a.tokens.GenerateTokenId(token),
|
|
TokenType: token.TokenType,
|
|
UserAgent: token.UserAgent,
|
|
LastSeen: token.LastSeenUnixTime,
|
|
}
|
|
|
|
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
|
tokenResp.IsCurrent = true
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
tokenResps[i] = tokenResp
|
|
}
|
|
|
|
sort.Sort(tokenResps)
|
|
|
|
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().EnableAPIToken {
|
|
return nil, errs.ErrAPITokenNotEnabled
|
|
}
|
|
|
|
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 {
|
|
return nil, errs.ErrMCPServerNotEnabled
|
|
}
|
|
|
|
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 user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS) {
|
|
return false, errs.ErrNotPermittedToPerformThisAction
|
|
}
|
|
|
|
if !a.users.IsPasswordEqualsUserPassword(generateMCPTokenReq.Password, user) {
|
|
return nil, errs.ErrUserPasswordWrong
|
|
}
|
|
|
|
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())
|
|
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) {
|
|
tokenString := c.GetTokenStringFromHeader()
|
|
|
|
if tokenString == "" {
|
|
return false, errs.ErrTokenIsEmpty
|
|
}
|
|
|
|
_, claims, _, err := a.tokens.ParseToken(c, tokenString)
|
|
|
|
if err != nil {
|
|
return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
|
|
}
|
|
|
|
userTokenId, err := utils.StringToInt64(claims.UserTokenId)
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
|
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
|
}
|
|
|
|
tokenRecord := &models.TokenRecord{
|
|
Uid: claims.Uid,
|
|
UserTokenId: userTokenId,
|
|
CreatedUnixTime: claims.IssuedAt,
|
|
}
|
|
|
|
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
|
|
|
if err != nil {
|
|
log.Errorf(c, "[tokens.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
|
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
|
}
|
|
|
|
log.Infof(c, "[tokens.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
|
|
return true, nil
|
|
}
|
|
|
|
// TokenRevokeHandler revokes specific token of current user
|
|
func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
|
|
var tokenRevokeReq models.TokenRevokeRequest
|
|
err := c.ShouldBindJSON(&tokenRevokeReq)
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
|
|
return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
|
|
}
|
|
|
|
tokenRecord, err := a.tokens.ParseFromTokenId(tokenRevokeReq.TokenId)
|
|
|
|
if err != nil {
|
|
if !errs.IsCustomError(err) {
|
|
log.Errorf(c, "[tokens.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
|
|
}
|
|
|
|
return nil, errs.Or(err, errs.ErrInvalidTokenId)
|
|
}
|
|
|
|
uid := c.GetCurrentUid()
|
|
|
|
if tokenRecord.Uid != uid {
|
|
log.Warnf(c, "[tokens.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
|
|
return nil, errs.ErrInvalidTokenId
|
|
}
|
|
|
|
if utils.Int64ToString(tokenRecord.UserTokenId) != c.GetTokenClaims().UserTokenId || tokenRecord.CreatedUnixTime != c.GetTokenClaims().IssuedAt {
|
|
user, err := a.users.GetUserById(c, uid)
|
|
|
|
if err != nil {
|
|
if !errs.IsCustomError(err) {
|
|
log.Errorf(c, "[tokens.TokenRevokeHandler] failed to get user, because %s", err.Error())
|
|
}
|
|
|
|
return nil, errs.ErrUserNotFound
|
|
}
|
|
|
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
|
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
|
}
|
|
}
|
|
|
|
err = a.tokens.DeleteToken(c, tokenRecord)
|
|
|
|
if err != nil {
|
|
log.Errorf(c, "[tokens.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
|
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
|
}
|
|
|
|
log.Infof(c, "[tokens.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
|
|
return true, nil
|
|
}
|
|
|
|
// TokenRevokeAllHandler revokes all tokens of current user except current token
|
|
func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error) {
|
|
uid := c.GetCurrentUid()
|
|
tokens, err := a.tokens.GetAllTokensByUid(c, uid)
|
|
|
|
if err != nil {
|
|
log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
|
}
|
|
|
|
claims := c.GetTokenClaims()
|
|
currentTokenIndex := 0
|
|
|
|
for i := 0; i < len(tokens); i++ {
|
|
token := tokens[i]
|
|
|
|
if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
|
|
currentTokenIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)
|
|
|
|
if len(tokens) < 1 {
|
|
return nil, errs.ErrTokenRecordNotFound
|
|
}
|
|
|
|
user, err := a.users.GetUserById(c, uid)
|
|
|
|
if err != nil {
|
|
if !errs.IsCustomError(err) {
|
|
log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get user, because %s", err.Error())
|
|
}
|
|
|
|
return nil, errs.ErrUserNotFound
|
|
}
|
|
|
|
if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
|
|
return nil, errs.ErrNotPermittedToPerformThisAction
|
|
}
|
|
|
|
err = a.tokens.DeleteTokens(c, uid, tokens)
|
|
|
|
if err != nil {
|
|
log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
|
|
return nil, errs.Or(err, errs.ErrOperationFailed)
|
|
}
|
|
|
|
log.Infof(c, "[tokens.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
|
|
return true, nil
|
|
}
|
|
|
|
// TokenRefreshHandler refresh current token of current user
|
|
func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
|
|
uid := c.GetCurrentUid()
|
|
user, err := a.users.GetUserById(c, uid)
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
|
|
return nil, errs.ErrUserNotFound
|
|
}
|
|
|
|
now := time.Now().Unix()
|
|
oldTokenClaims := c.GetTokenClaims()
|
|
|
|
if now-oldTokenClaims.IssuedAt < int64(a.CurrentConfig().TokenMinRefreshInterval) {
|
|
log.Infof(c, "[tokens.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)
|
|
|
|
userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
|
|
} else {
|
|
tokenRecord := &models.TokenRecord{
|
|
Uid: oldTokenClaims.Uid,
|
|
UserTokenId: userTokenId,
|
|
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
|
}
|
|
|
|
tokenId := a.tokens.GenerateTokenId(tokenRecord)
|
|
|
|
err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
|
|
}
|
|
}
|
|
|
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
|
|
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
|
|
}
|
|
|
|
refreshResp := &models.TokenRefreshResponse{
|
|
User: a.GetUserBasicInfo(user),
|
|
ApplicationCloudSettings: applicationCloudSettingSlice,
|
|
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
|
}
|
|
|
|
return refreshResp, nil
|
|
}
|
|
|
|
token, claims, err := a.tokens.CreateToken(c, user)
|
|
|
|
if err != nil {
|
|
log.Errorf(c, "[tokens.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
return nil, errs.Or(err, errs.ErrTokenGenerating)
|
|
}
|
|
|
|
oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
|
|
oldTokenRecord := &models.TokenRecord{
|
|
Uid: uid,
|
|
UserTokenId: oldUserTokenId,
|
|
CreatedUnixTime: oldTokenClaims.IssuedAt,
|
|
}
|
|
|
|
c.SetTextualToken(token)
|
|
c.SetTokenClaims(claims)
|
|
c.SetTokenContext("")
|
|
|
|
userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
|
|
var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil
|
|
|
|
if err != nil {
|
|
log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
|
|
} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
|
|
applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
|
|
}
|
|
|
|
log.Infof(c, "[tokens.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)
|
|
|
|
refreshResp := &models.TokenRefreshResponse{
|
|
NewToken: token,
|
|
OldTokenId: a.tokens.GenerateTokenId(oldTokenRecord),
|
|
User: a.GetUserBasicInfo(user),
|
|
ApplicationCloudSettings: applicationCloudSettingSlice,
|
|
NotificationContent: a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
|
|
}
|
|
|
|
return refreshResp, nil
|
|
}
|