diff --git a/pkg/api/data_managements.go b/pkg/api/data_managements.go index 9ebb3aa6..08c68564 100644 --- a/pkg/api/data_managements.go +++ b/pkg/api/data_managements.go @@ -147,6 +147,10 @@ func (a *DataManagementsApi) ClearDataHandler(c *core.WebContext) (any, *errs.Er return nil, errs.ErrUserPasswordWrong } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + err = a.templates.DeleteAllTemplates(c, uid) if err != nil { @@ -204,6 +208,10 @@ func (a *DataManagementsApi) getExportedFileContent(c *core.WebContext, fileType return nil, "", errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_EXPORT_TRANSACTION) { + return nil, "", errs.ErrNotPermittedToPerformThisAction + } + accounts, err := a.accounts.GetAllAccountsByUid(c, uid) if err != nil { diff --git a/pkg/api/forget_passwords.go b/pkg/api/forget_passwords.go index f49063cb..bdf62fb2 100644 --- a/pkg/api/forget_passwords.go +++ b/pkg/api/forget_passwords.go @@ -56,6 +56,10 @@ func (a *ForgetPasswordsApi) UserForgetPasswordRequestHandler(c *core.WebContext return nil, errs.ErrUserIsDisabled } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified { log.Warnf(c, "[forget_passwords.UserForgetPasswordRequestHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified @@ -109,6 +113,10 @@ func (a *ForgetPasswordsApi) UserResetPasswordHandler(c *core.WebContext) (any, return nil, errs.ErrUserIsDisabled } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if a.CurrentConfig().ForgetPasswordRequireVerifyEmail && !user.EmailVerified { log.Warnf(c, "[forget_passwords.UserResetPasswordHandler] user \"uid:%d\" has not verified email", user.Uid) return nil, errs.ErrEmailIsNotVerified diff --git a/pkg/api/tokens.go b/pkg/api/tokens.go index 491c9b3b..59de69ad 100644 --- a/pkg/api/tokens.go +++ b/pkg/api/tokens.go @@ -135,6 +135,22 @@ func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) { 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, "[token.TokenRevokeHandler] failed to get user, because %s", err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + } + err = a.tokens.DeleteToken(c, tokenRecord) if err != nil { @@ -170,6 +186,24 @@ func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error) 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, "[token.TokenRevokeAllHandler] failed to get user, because %s", err.Error()) + } + + return nil, errs.ErrUserNotFound + } + + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + err = a.tokens.DeleteTokens(c, uid, tokens) if err != nil { diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 6e264837..077498ba 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -1077,6 +1077,10 @@ func (a *TransactionsApi) TransactionParseImportFileHandler(c *core.WebContext) return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + accounts, err := a.accounts.GetAllAccountsByUid(c, user.Uid) if err != nil { @@ -1201,6 +1205,10 @@ func (a *TransactionsApi) TransactionImportHandler(c *core.WebContext) (any, *er return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + newTransactions := make([]*models.Transaction, len(transactionImportReq.Transactions)) for i := 0; i < len(transactionImportReq.Transactions); i++ { diff --git a/pkg/api/twofactor_authorizations.go b/pkg/api/twofactor_authorizations.go index 223a6cb0..e472ff8e 100644 --- a/pkg/api/twofactor_authorizations.go +++ b/pkg/api/twofactor_authorizations.go @@ -81,6 +81,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableRequestHandler(c *core.WebCo return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + key, err := a.twoFactorAuthorizations.GenerateTwoFactorSecret(c, user) if err != nil { @@ -141,6 +145,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorEnableConfirmHandler(c *core.WebCo return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + twoFactorSetting := &models.TwoFactor{ Uid: uid, Secret: confirmReq.Secret, @@ -229,6 +237,10 @@ func (a *TwoFactorAuthorizationsApi) TwoFactorDisableHandler(c *core.WebContext) return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if !a.users.IsPasswordEqualsUserPassword(disableReq.Password, user) { return nil, errs.ErrUserPasswordWrong } diff --git a/pkg/api/users.go b/pkg/api/users.go index f5db46d0..482cd0cd 100644 --- a/pkg/api/users.go +++ b/pkg/api/users.go @@ -251,6 +251,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro userUpdateReq.Email = strings.TrimSpace(userUpdateReq.Email) userUpdateReq.Nickname = strings.TrimSpace(userUpdateReq.Nickname) + modifyProfileBasicInfo := false anythingUpdate := false userNew := &models.User{ Uid: user.Uid, @@ -258,12 +259,20 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro } if userUpdateReq.Email != "" && userUpdateReq.Email != user.Email { + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + user.Email = userUpdateReq.Email userNew.Email = userUpdateReq.Email anythingUpdate = true } if userUpdateReq.Password != "" { + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if !a.users.IsPasswordEqualsUserPassword(userUpdateReq.OldPassword, user) { return nil, errs.ErrUserPasswordWrong } @@ -277,6 +286,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.Nickname != "" && userUpdateReq.Nickname != user.Nickname { user.Nickname = userUpdateReq.Nickname userNew.Nickname = userUpdateReq.Nickname + modifyProfileBasicInfo = true anythingUpdate = true } @@ -299,12 +309,14 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro user.DefaultAccountId = userUpdateReq.DefaultAccountId userNew.DefaultAccountId = userUpdateReq.DefaultAccountId + modifyProfileBasicInfo = true anythingUpdate = true } if userUpdateReq.TransactionEditScope != nil && *userUpdateReq.TransactionEditScope != user.TransactionEditScope { user.TransactionEditScope = *userUpdateReq.TransactionEditScope userNew.TransactionEditScope = *userUpdateReq.TransactionEditScope + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.TransactionEditScope = models.TRANSACTION_EDIT_SCOPE_INVALID @@ -316,18 +328,21 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro user.Language = userUpdateReq.Language userNew.Language = userUpdateReq.Language modifyUserLanguage = true + modifyProfileBasicInfo = true anythingUpdate = true } if userUpdateReq.DefaultCurrency != "" && userUpdateReq.DefaultCurrency != user.DefaultCurrency { user.DefaultCurrency = userUpdateReq.DefaultCurrency userNew.DefaultCurrency = userUpdateReq.DefaultCurrency + modifyProfileBasicInfo = true anythingUpdate = true } if userUpdateReq.FirstDayOfWeek != nil && *userUpdateReq.FirstDayOfWeek != user.FirstDayOfWeek { user.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek userNew.FirstDayOfWeek = *userUpdateReq.FirstDayOfWeek + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.FirstDayOfWeek = core.WEEKDAY_INVALID @@ -336,6 +351,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.LongDateFormat != nil && *userUpdateReq.LongDateFormat != user.LongDateFormat { user.LongDateFormat = *userUpdateReq.LongDateFormat userNew.LongDateFormat = *userUpdateReq.LongDateFormat + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.LongDateFormat = core.LONG_DATE_FORMAT_INVALID @@ -344,6 +360,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.ShortDateFormat != nil && *userUpdateReq.ShortDateFormat != user.ShortDateFormat { user.ShortDateFormat = *userUpdateReq.ShortDateFormat userNew.ShortDateFormat = *userUpdateReq.ShortDateFormat + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.ShortDateFormat = core.SHORT_DATE_FORMAT_INVALID @@ -352,6 +369,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.LongTimeFormat != nil && *userUpdateReq.LongTimeFormat != user.LongTimeFormat { user.LongTimeFormat = *userUpdateReq.LongTimeFormat userNew.LongTimeFormat = *userUpdateReq.LongTimeFormat + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.LongTimeFormat = core.LONG_TIME_FORMAT_INVALID @@ -360,6 +378,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.ShortTimeFormat != nil && *userUpdateReq.ShortTimeFormat != user.ShortTimeFormat { user.ShortTimeFormat = *userUpdateReq.ShortTimeFormat userNew.ShortTimeFormat = *userUpdateReq.ShortTimeFormat + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.ShortTimeFormat = core.SHORT_TIME_FORMAT_INVALID @@ -368,6 +387,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.DecimalSeparator != nil && *userUpdateReq.DecimalSeparator != user.DecimalSeparator { user.DecimalSeparator = *userUpdateReq.DecimalSeparator userNew.DecimalSeparator = *userUpdateReq.DecimalSeparator + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.DecimalSeparator = core.DECIMAL_SEPARATOR_INVALID @@ -376,6 +396,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.DigitGroupingSymbol != nil && *userUpdateReq.DigitGroupingSymbol != user.DigitGroupingSymbol { user.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol userNew.DigitGroupingSymbol = *userUpdateReq.DigitGroupingSymbol + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.DigitGroupingSymbol = core.DIGIT_GROUPING_SYMBOL_INVALID @@ -384,6 +405,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.DigitGrouping != nil && *userUpdateReq.DigitGrouping != user.DigitGrouping { user.DigitGrouping = *userUpdateReq.DigitGrouping userNew.DigitGrouping = *userUpdateReq.DigitGrouping + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.DigitGrouping = core.DIGIT_GROUPING_TYPE_INVALID @@ -392,6 +414,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.CurrencyDisplayType != nil && *userUpdateReq.CurrencyDisplayType != user.CurrencyDisplayType { user.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType userNew.CurrencyDisplayType = *userUpdateReq.CurrencyDisplayType + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.CurrencyDisplayType = core.CURRENCY_DISPLAY_TYPE_INVALID @@ -400,6 +423,7 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.ExpenseAmountColor != nil && *userUpdateReq.ExpenseAmountColor != user.ExpenseAmountColor { user.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor userNew.ExpenseAmountColor = *userUpdateReq.ExpenseAmountColor + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.ExpenseAmountColor = models.AMOUNT_COLOR_TYPE_INVALID @@ -408,11 +432,16 @@ func (a *UsersApi) UserUpdateProfileHandler(c *core.WebContext) (any, *errs.Erro if userUpdateReq.IncomeAmountColor != nil && *userUpdateReq.IncomeAmountColor != user.IncomeAmountColor { user.IncomeAmountColor = *userUpdateReq.IncomeAmountColor userNew.IncomeAmountColor = *userUpdateReq.IncomeAmountColor + modifyProfileBasicInfo = true anythingUpdate = true } else { userNew.IncomeAmountColor = models.AMOUNT_COLOR_TYPE_INVALID } + if modifyProfileBasicInfo && user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if modifyUserLanguage || userNew.DecimalSeparator != core.DECIMAL_SEPARATOR_INVALID || userNew.DigitGroupingSymbol != core.DIGIT_GROUPING_SYMBOL_INVALID { decimalSeparator := userNew.DecimalSeparator digitGroupingSymbol := userNew.DigitGroupingSymbol @@ -525,6 +554,10 @@ func (a *UsersApi) UserUpdateAvatarHandler(c *core.WebContext) (any, *errs.Error return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + form, err := c.MultipartForm() if err != nil { @@ -588,6 +621,10 @@ func (a *UsersApi) UserRemoveAvatarHandler(c *core.WebContext) (any, *errs.Error return nil, errs.ErrUserNotFound } + if user.FeatureRestriction.Contains(models.USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) { + return nil, errs.ErrNotPermittedToPerformThisAction + } + if user.CustomAvatarType == "" { return nil, errs.ErrNothingWillBeUpdated } diff --git a/pkg/errs/user.go b/pkg/errs/user.go index f61cd436..07d1c221 100644 --- a/pkg/errs/user.go +++ b/pkg/errs/user.go @@ -37,4 +37,5 @@ var ( ErrUserAvatarNotSet = NewNormalError(NormalSubcategoryUser, 28, http.StatusNotFound, "user avatar not set") ErrUserAvatarExtensionInvalid = NewNormalError(NormalSubcategoryUser, 29, http.StatusNotFound, "user avatar file extension invalid") ErrExceedMaxUserAvatarFileSize = NewNormalError(NormalSubcategoryUser, 30, http.StatusBadRequest, "exceed the maximum size of user avatar file") + ErrNotPermittedToPerformThisAction = NewNormalError(NormalSubcategoryUser, 31, http.StatusBadRequest, "not permitted to perform this action") ) diff --git a/pkg/models/user.go b/pkg/models/user.go index 9d267399..c5dbea18 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -2,6 +2,7 @@ package models import ( "fmt" + "strings" "time" "github.com/mayswind/ezbookkeeping/pkg/core" @@ -80,6 +81,94 @@ func (s AmountColorType) String() string { } } +// UserFeatureRestrictions represents all the restrictions of user features +type UserFeatureRestrictions int64 + +// Add returns a new feature restrictions with the specified feature +func (r UserFeatureRestrictions) Add(featureRestrictionType UserFeatureRestrictionType) UserFeatureRestrictions { + typeValue := int64(1 << (featureRestrictionType - 1)) + return UserFeatureRestrictions(int64(r) | typeValue) +} + +// Remove returns a new feature restrictions without the specified feature +func (r UserFeatureRestrictions) Remove(featureRestrictionType UserFeatureRestrictionType) UserFeatureRestrictions { + typeValue := int64(1 << (featureRestrictionType - 1)) + return UserFeatureRestrictions(int64(r) & (^typeValue)) +} + +// Contains returns whether contains the specified feature +func (r UserFeatureRestrictions) Contains(featureRestrictionType UserFeatureRestrictionType) bool { + typeValue := int64(1 << (featureRestrictionType - 1)) + return int64(r)&typeValue == typeValue +} + +// String returns a textual representation of all the restrictions of user features +func (r UserFeatureRestrictions) String() string { + builder := strings.Builder{} + + for restrictionType := USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD; restrictionType <= USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA; restrictionType++ { + if !r.Contains(restrictionType) { + continue + } + + if builder.Len() > 0 { + builder.WriteRune(',') + } + + builder.WriteString(restrictionType.String()) + } + + return builder.String() +} + +// UserFeatureRestrictionType represents the restriction type of user features +type UserFeatureRestrictionType int64 + +// User Feature Restriction Type +const ( + USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD UserFeatureRestrictionType = 1 + USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL UserFeatureRestrictionType = 2 + USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO UserFeatureRestrictionType = 3 + USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR UserFeatureRestrictionType = 4 + USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION UserFeatureRestrictionType = 5 + USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA UserFeatureRestrictionType = 6 + USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA UserFeatureRestrictionType = 7 + USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD UserFeatureRestrictionType = 8 + USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION UserFeatureRestrictionType = 9 + USER_FEATURE_RESTRICTION_TYPE_EXPORT_TRANSACTION UserFeatureRestrictionType = 10 + USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA UserFeatureRestrictionType = 11 +) + +// String returns a textual representation of the restriction type of user features +func (t UserFeatureRestrictionType) String() string { + switch t { + case USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD: + return "Update Password" + case USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL: + return "Update Email" + case USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO: + return "Update Profile Basic Info" + case USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR: + return "Update Avatar" + case USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION: + return "Logout Other Session" + case USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA: + return "Enable Two-Factor Authentication" + case USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA: + return "Disable Enable Two-Factor Authentication" + case USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD: + return "Forget Password" + case USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION: + return "Import Transactions" + case USER_FEATURE_RESTRICTION_TYPE_EXPORT_TRANSACTION: + return "Export Transactions" + case USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA: + return "Clear All Data" + default: + return fmt.Sprintf("Invalid(%d)", int(t)) + } +} + // User represents user data stored in database type User struct { Uid int64 `xorm:"PK"` @@ -104,6 +193,7 @@ type User struct { CurrencyDisplayType core.CurrencyDisplayType `xorm:"TINYINT"` ExpenseAmountColor AmountColorType `xorm:"TINYINT"` IncomeAmountColor AmountColorType `xorm:"TINYINT"` + FeatureRestriction UserFeatureRestrictions Disabled bool Deleted bool `xorm:"NOT NULL"` EmailVerified bool `xorm:"NOT NULL"` diff --git a/pkg/models/user_test.go b/pkg/models/user_test.go index 65a0f369..b58b2d8a 100644 --- a/pkg/models/user_test.go +++ b/pkg/models/user_test.go @@ -10,6 +10,95 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/utils" ) +func TestUserFeatureRestrictionsAdd(t *testing.T) { + var featureRestrictions UserFeatureRestrictions + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) + expectedValue := UserFeatureRestrictions(1) + assert.Equal(t, expectedValue, featureRestrictions) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) + expectedValue = UserFeatureRestrictions(255) + assert.Equal(t, expectedValue, featureRestrictions) +} + +func TestUserFeatureRestrictionsRemove(t *testing.T) { + var featureRestrictions UserFeatureRestrictions + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) + expectedValue := UserFeatureRestrictions(1) + assert.Equal(t, expectedValue, featureRestrictions) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) + featureRestrictions = featureRestrictions.Remove(USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) + expectedValue = UserFeatureRestrictions(153) + assert.Equal(t, expectedValue, featureRestrictions) +} + +func TestUserFeatureRestrictionsContains(t *testing.T) { + var featureRestrictions UserFeatureRestrictions + assert.False(t, featureRestrictions.Contains(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD)) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) + assert.True(t, featureRestrictions.Contains(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD)) + assert.False(t, featureRestrictions.Contains(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO)) +} + +func TestUserFeatureRestrictionsString(t *testing.T) { + var featureRestrictions UserFeatureRestrictions + expectedValue := "" + actualValue := featureRestrictions.String() + assert.Equal(t, expectedValue, actualValue) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PASSWORD) + expectedValue = "Update Password" + actualValue = featureRestrictions.String() + assert.Equal(t, expectedValue, actualValue) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_FORGET_PASSWORD) + expectedValue = "Update Password,Forget Password" + actualValue = featureRestrictions.String() + assert.Equal(t, expectedValue, actualValue) + + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_EMAIL) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_PROFILE_BASIC_INFO) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_UPDATE_AVATAR) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_ENABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_DISABLE_2FA) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_IMPORT_TRANSACTION) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_EXPORT_TRANSACTION) + featureRestrictions = featureRestrictions.Add(USER_FEATURE_RESTRICTION_TYPE_CLEAR_ALL_DATA) + expectedValue = "Update Password," + + "Update Email," + + "Update Profile Basic Info," + + "Update Avatar," + + "Logout Other Session," + + "Enable Two-Factor Authentication," + + "Disable Enable Two-Factor Authentication," + + "Forget Password," + + "Import Transactions," + + "Export Transactions," + + "Clear All Data" + actualValue = featureRestrictions.String() + assert.Equal(t, expectedValue, actualValue) +} + func TestUserCanEditTransactionByTransactionTime_ScopeIsNone(t *testing.T) { user := &User{ TransactionEditScope: TRANSACTION_EDIT_SCOPE_NONE, diff --git a/src/locales/en.json b/src/locales/en.json index 88bb02b4..08a3d071 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1023,6 +1023,7 @@ "user avatar not set": "User avatar is not set", "user avatar file extension invalid": "User avatar file extension is invalid", "exceed the maximum size of user avatar file": "The uploaded user avatar exceeds the maximum allowed file size", + "not permitted to perform this action": "You are not permitted to perform this action", "unauthorized access": "Unauthorized access", "current token is invalid": "Current token is invalid", "current token is expired": "Current token is expired", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index f0958bd0..c3c066ab 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1023,6 +1023,7 @@ "user avatar not set": "用户没有设置头像", "user avatar file extension invalid": "用户头像文件扩展名无效", "exceed the maximum size of user avatar file": "上传的用户头像超出了允许的最大文件大小", + "not permitted to perform this action": "您不能执行该操作", "unauthorized access": "未授权的登录", "current token is invalid": "当前认证令牌无效", "current token is expired": "当前认证令牌已过期",