diff --git a/conf/ezbookkeeping.ini b/conf/ezbookkeeping.ini index b6b9c211..aa04c18c 100644 --- a/conf/ezbookkeeping.ini +++ b/conf/ezbookkeeping.ini @@ -180,6 +180,12 @@ email_verify_token_expired_time = 3600 # Password reset token expired seconds (60 - 4294967295), default is 3600 (60 minutes) password_reset_token_expired_time = 3600 +# Maximum count of password / token check failures (0 - 4294967295) per IP per minute (use the above duplicate checker), default is 5, set to 0 to disable +max_failures_per_ip_per_minute = 5 + +# Maximum count of password / token check failures (0 - 4294967295) per user per minute (use the above duplicate checker), default is 5, set to 0 to disable +max_failures_per_user_per_minute = 5 + # Add X-Request-Id header to response to track user request or error, default is true request_id_header = true diff --git a/pkg/api/accounts.go b/pkg/api/accounts.go index 6cdd0fbc..edef3ad9 100644 --- a/pkg/api/accounts.go +++ b/pkg/api/accounts.go @@ -28,6 +28,9 @@ var ( container: settings.Container, }, ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, container: duplicatechecker.Container, }, accounts: services.Accounts, diff --git a/pkg/api/authorizations.go b/pkg/api/authorizations.go index 100def5c..efdaa17e 100644 --- a/pkg/api/authorizations.go +++ b/pkg/api/authorizations.go @@ -5,6 +5,7 @@ import ( "github.com/mayswind/ezbookkeeping/pkg/avatars" "github.com/mayswind/ezbookkeeping/pkg/core" + "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" "github.com/mayswind/ezbookkeeping/pkg/errs" "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/models" @@ -15,6 +16,7 @@ import ( // AuthorizationsApi represents authorization api type AuthorizationsApi struct { ApiUsingConfig + ApiUsingDuplicateChecker ApiWithUserInfo users *services.UserService tokens *services.TokenService @@ -27,6 +29,12 @@ var ( ApiUsingConfig: ApiUsingConfig{ container: settings.Container, }, + ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, + container: duplicatechecker.Container, + }, ApiWithUserInfo: ApiWithUserInfo{ ApiUsingConfig: ApiUsingConfig{ container: settings.Container, @@ -51,7 +59,23 @@ func (a *AuthorizationsApi) AuthorizeHandler(c *core.WebContext) (any, *errs.Err return nil, errs.ErrLoginNameOrPasswordInvalid } - user, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password) + err = a.CheckFailureCount(c, 0) + + if err != nil { + log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, err.Error()) + return nil, errs.Or(err, errs.ErrFailureCountLimitReached) + } + + user, uid, err := a.users.GetUserByUsernameOrEmailAndPassword(c, credential.LoginName, credential.Password) + + if errs.IsCustomError(err) { + failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid) + + if failureCheckErr != nil { + log.Warnf(c, "[authorizations.AuthorizeHandler] cannot login for user \"%s\", because %s", credential.LoginName, failureCheckErr.Error()) + return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached) + } + } if err != nil { log.Warnf(c, "[authorizations.AuthorizeHandler] login failed for user \"%s\", because %s", credential.LoginName, err.Error()) @@ -133,6 +157,13 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, } uid := c.GetCurrentUid() + err = a.CheckFailureCount(c, uid) + + if err != nil { + log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrFailureCountLimitReached) + } + twoFactorSetting, err := a.twoFactorAuthorizations.GetUserTwoFactorSettingByUid(c, uid) if err != nil { @@ -142,6 +173,14 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeHandler(c *core.WebContext) (any, if !totp.Validate(credential.Passcode, twoFactorSetting.Secret) { log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] passcode is invalid for user \"uid:%d\"", uid) + + err = a.CheckAndIncreaseFailureCount(c, uid) + + if err != nil { + log.Warnf(c, "[authorizations.TwoFactorAuthorizeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrFailureCountLimitReached) + } + return nil, errs.ErrPasscodeInvalid } @@ -196,6 +235,13 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC } uid := c.GetCurrentUid() + err = a.CheckFailureCount(c, uid) + + if err != nil { + log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, err.Error()) + return nil, errs.Or(err, errs.ErrFailureCountLimitReached) + } + enableTwoFactor, err := a.twoFactorAuthorizations.ExistsTwoFactorSetting(c, uid) if err != nil { @@ -226,6 +272,15 @@ func (a *AuthorizationsApi) TwoFactorAuthorizeByRecoveryCodeHandler(c *core.WebC err = a.twoFactorAuthorizations.GetAndUseUserTwoFactorRecoveryCode(c, uid, credential.RecoveryCode, user.Salt) + if errs.IsCustomError(err) { + failureCheckErr := a.CheckAndIncreaseFailureCount(c, uid) + + if failureCheckErr != nil { + log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] cannot auth for user \"uid:%d\", because %s", uid, failureCheckErr.Error()) + return nil, errs.Or(failureCheckErr, errs.ErrFailureCountLimitReached) + } + } + if err != nil { log.Warnf(c, "[authorizations.TwoFactorAuthorizeByRecoveryCodeHandler] failed to get two-factor recovery code for user \"uid:%d\", because %s", uid, err.Error()) return nil, errs.Or(err, errs.ErrTwoFactorRecoveryCodeNotExist) diff --git a/pkg/api/base.go b/pkg/api/base.go index 1cedc645..e29807f0 100644 --- a/pkg/api/base.go +++ b/pkg/api/base.go @@ -5,9 +5,13 @@ import ( "sort" "github.com/mayswind/ezbookkeeping/pkg/avatars" + "github.com/mayswind/ezbookkeeping/pkg/core" "github.com/mayswind/ezbookkeeping/pkg/duplicatechecker" + "github.com/mayswind/ezbookkeeping/pkg/errs" + "github.com/mayswind/ezbookkeeping/pkg/log" "github.com/mayswind/ezbookkeeping/pkg/models" "github.com/mayswind/ezbookkeeping/pkg/settings" + "github.com/mayswind/ezbookkeeping/pkg/utils" ) const internalTransactionPictureUrlFormat = "%spictures/%d.%s" @@ -100,6 +104,7 @@ func (a *ApiUsingConfig) GetAfterOpenNotificationContent(userLanguage string, cl // ApiUsingDuplicateChecker represents an api that need to use duplicate checker type ApiUsingDuplicateChecker struct { + ApiUsingConfig container *duplicatechecker.DuplicateCheckerContainer } @@ -113,6 +118,67 @@ func (a *ApiUsingDuplicateChecker) SetSubmissionRemark(checkerType duplicatechec a.container.SetSubmissionRemark(checkerType, uid, identification, remark) } +// CheckFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count +func (a *ApiUsingDuplicateChecker) CheckFailureCount(c *core.WebContext, uid int64) error { + if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 { + clientIp := c.ClientIP() + ipFailureCount := a.container.GetFailureCount(clientIp) + + if ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute { + log.Warnf(c, "[base.CheckFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount) + return errs.ErrFailureCountLimitReached + } + } + + if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 { + uidFailureCount := a.container.GetFailureCount(utils.Int64ToString(uid)) + + if uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute { + log.Warnf(c, "[base.CheckFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount) + return errs.ErrFailureCountLimitReached + } + } + + return nil +} + +// CheckAndIncreaseFailureCount returns whether the failure count of the specified IP and user has reached the limit and increases the failure count +func (a *ApiUsingDuplicateChecker) CheckAndIncreaseFailureCount(c *core.WebContext, uid int64) error { + clientIp := c.ClientIP() + ipFailureCount := uint32(0) + uidFailureCount := uint32(0) + + if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 { + ipFailureCount = a.container.GetFailureCount(clientIp) + } + + if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 { + uidFailureCount = a.container.GetFailureCount(utils.Int64ToString(uid)) + } + + if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount < a.CurrentConfig().MaxFailuresPerIpPerMinute { + log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", previous failure count: %d", clientIp, ipFailureCount) + a.container.IncreaseFailureCount(clientIp) + } + + if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount < a.CurrentConfig().MaxFailuresPerUserPerMinute { + log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", previous failure count: %d", uid, uidFailureCount) + a.container.IncreaseFailureCount(utils.Int64ToString(uid)) + } + + if a.CurrentConfig().MaxFailuresPerIpPerMinute > 0 && ipFailureCount >= a.CurrentConfig().MaxFailuresPerIpPerMinute { + log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via IP \"%s\", current failure count: %d reached the limit", clientIp, ipFailureCount) + return errs.ErrFailureCountLimitReached + } + + if a.CurrentConfig().MaxFailuresPerUserPerMinute > 0 && uid > 0 && uidFailureCount >= a.CurrentConfig().MaxFailuresPerUserPerMinute { + log.Warnf(c, "[base.CheckAndIncreaseFailureCount] operation failure via uid \"%d\", current failure count: %d reached the limit", uid, uidFailureCount) + return errs.ErrFailureCountLimitReached + } + + return nil +} + // ApiUsingAvatarProvider represents an api that need to use avatar provider type ApiUsingAvatarProvider struct { container *avatars.AvatarProviderContainer diff --git a/pkg/api/transaction_categories.go b/pkg/api/transaction_categories.go index bd904f90..440a8070 100644 --- a/pkg/api/transaction_categories.go +++ b/pkg/api/transaction_categories.go @@ -29,6 +29,9 @@ var ( container: settings.Container, }, ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, container: duplicatechecker.Container, }, categories: services.TransactionCategories, diff --git a/pkg/api/transaction_pictures.go b/pkg/api/transaction_pictures.go index 2f87b8b3..bdfa292a 100644 --- a/pkg/api/transaction_pictures.go +++ b/pkg/api/transaction_pictures.go @@ -26,6 +26,9 @@ var ( container: settings.Container, }, ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, container: duplicatechecker.Container, }, users: services.Users, diff --git a/pkg/api/transaction_templates.go b/pkg/api/transaction_templates.go index b42894c3..8a3039f4 100644 --- a/pkg/api/transaction_templates.go +++ b/pkg/api/transaction_templates.go @@ -31,6 +31,9 @@ var ( container: settings.Container, }, ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, container: duplicatechecker.Container, }, templates: services.TransactionTemplates, diff --git a/pkg/api/transactions.go b/pkg/api/transactions.go index 89050b6e..774e3178 100644 --- a/pkg/api/transactions.go +++ b/pkg/api/transactions.go @@ -43,6 +43,9 @@ var ( container: settings.Container, }, ApiUsingDuplicateChecker: ApiUsingDuplicateChecker{ + ApiUsingConfig: ApiUsingConfig{ + container: settings.Container, + }, container: duplicatechecker.Container, }, transactions: services.Transactions, diff --git a/pkg/duplicatechecker/duplicate_checker.go b/pkg/duplicatechecker/duplicate_checker.go index cad28377..dfb5a472 100644 --- a/pkg/duplicatechecker/duplicate_checker.go +++ b/pkg/duplicatechecker/duplicate_checker.go @@ -8,4 +8,6 @@ type DuplicateChecker interface { SetSubmissionRemark(checkerType DuplicateCheckerType, uid int64, identification string, remark string) GetOrSetCronJobRunningInfo(jobName string, runningInfo string, runningInterval time.Duration) (bool, string) RemoveCronJobRunningInfo(jobName string) + GetFailureCount(failureKey string) uint32 + IncreaseFailureCount(failureKey string) uint32 } diff --git a/pkg/duplicatechecker/duplicate_checker_container.go b/pkg/duplicatechecker/duplicate_checker_container.go index b8cb20b7..8ab1e388 100644 --- a/pkg/duplicatechecker/duplicate_checker_container.go +++ b/pkg/duplicatechecker/duplicate_checker_container.go @@ -48,3 +48,13 @@ func (c *DuplicateCheckerContainer) GetOrSetCronJobRunningInfo(jobName string, r func (c *DuplicateCheckerContainer) RemoveCronJobRunningInfo(jobName string) { c.Current.RemoveCronJobRunningInfo(jobName) } + +// GetFailureCount returns the failure count of the specified failure key +func (c *DuplicateCheckerContainer) GetFailureCount(failureKey string) uint32 { + return c.Current.GetFailureCount(failureKey) +} + +// IncreaseFailureCount increases the failure count of the specified failure key +func (c *DuplicateCheckerContainer) IncreaseFailureCount(failureKey string) uint32 { + return c.Current.IncreaseFailureCount(failureKey) +} diff --git a/pkg/duplicatechecker/duplicate_checker_type.go b/pkg/duplicatechecker/duplicate_checker_type.go index 032b3f9c..11c470df 100644 --- a/pkg/duplicatechecker/duplicate_checker_type.go +++ b/pkg/duplicatechecker/duplicate_checker_type.go @@ -12,4 +12,5 @@ const ( DUPLICATE_CHECKER_TYPE_NEW_TEMPLATE DuplicateCheckerType = 4 DUPLICATE_CHECKER_TYPE_NEW_PICTURE DuplicateCheckerType = 5 DUPLICATE_CHECKER_TYPE_IMPORT_TRANSACTIONS DuplicateCheckerType = 6 + DUPLICATE_CHECKER_TYPE_FAILURE_CHECK DuplicateCheckerType = 255 ) diff --git a/pkg/duplicatechecker/in_memory_duplicate_checker.go b/pkg/duplicatechecker/in_memory_duplicate_checker.go index 8b1a1531..73365a1f 100644 --- a/pkg/duplicatechecker/in_memory_duplicate_checker.go +++ b/pkg/duplicatechecker/in_memory_duplicate_checker.go @@ -69,6 +69,34 @@ func (c *InMemoryDuplicateChecker) RemoveCronJobRunningInfo(jobName string) { c.cache.Delete(c.getCacheKey(DUPLICATE_CHECKER_TYPE_BACKGROUND_CRON_JOB, 0, jobName)) } +// GetFailureCount returns the failure count of the specified failure key +func (c *InMemoryDuplicateChecker) GetFailureCount(failureKey string) uint32 { + existedFailureCount, found := c.cache.Get(c.getCacheKey(DUPLICATE_CHECKER_TYPE_FAILURE_CHECK, 0, failureKey)) + + if found { + return existedFailureCount.(uint32) + } + + return 0 +} + +// IncreaseFailureCount increases the failure count of the specified failure key +func (c *InMemoryDuplicateChecker) IncreaseFailureCount(failureKey string) uint32 { + c.mutex.Lock() + defer c.mutex.Unlock() + + cacheKey := c.getCacheKey(DUPLICATE_CHECKER_TYPE_FAILURE_CHECK, 0, failureKey) + _, found := c.cache.Get(cacheKey) + + if found { + failureCount, _ := c.cache.IncrementUint32(cacheKey, uint32(1)) + return failureCount + } else { + c.cache.Set(cacheKey, uint32(1), 1*time.Minute) + return 1 + } +} + func (c *InMemoryDuplicateChecker) getCacheKey(checkerType DuplicateCheckerType, uid int64, identification string) string { return fmt.Sprintf("%d|%d|%s", checkerType, uid, identification) } diff --git a/pkg/duplicatechecker/in_memory_duplicate_checker_test.go b/pkg/duplicatechecker/in_memory_duplicate_checker_test.go index 41767947..ceeda703 100644 --- a/pkg/duplicatechecker/in_memory_duplicate_checker_test.go +++ b/pkg/duplicatechecker/in_memory_duplicate_checker_test.go @@ -155,3 +155,77 @@ func TestGetOrSetRunningInfoConcurrent(t *testing.T) { assert.Equal(t, uint32(999), setRunningInfoCount.Load()) } + +func TestGetFailureCount(t *testing.T) { + checker, _ := NewInMemoryDuplicateChecker(&settings.Config{ + DuplicateSubmissionsIntervalDuration: time.Second, + InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second, + }) + + failureKey := "127.0.0.1" + + failureCount := checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(0), failureCount) + + failureCount = checker.IncreaseFailureCount(failureKey) + assert.Equal(t, uint32(1), failureCount) + + failureCount = checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(1), failureCount) +} + +func TestIncreaseFailureCount(t *testing.T) { + checker, _ := NewInMemoryDuplicateChecker(&settings.Config{ + DuplicateSubmissionsIntervalDuration: time.Second, + InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second, + }) + + failureKey := "127.0.0.1" + + failureCount := checker.IncreaseFailureCount(failureKey) + assert.Equal(t, uint32(1), failureCount) + + failureCount = checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(1), failureCount) + + failureCount = checker.IncreaseFailureCount(failureKey) + assert.Equal(t, uint32(2), failureCount) + + failureCount = checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(2), failureCount) + + failureCount = checker.IncreaseFailureCount(failureKey) + assert.Equal(t, uint32(3), failureCount) + + failureCount = checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(3), failureCount) +} + +func TestIncreaseFailureCountConcurrent(t *testing.T) { + checker, _ := NewInMemoryDuplicateChecker(&settings.Config{ + DuplicateSubmissionsIntervalDuration: time.Second, + InMemoryDuplicateCheckerCleanupIntervalDuration: time.Second, + }) + + failureKey := "127.0.0.1" + + concurrentCount := 10 + var waitGroup sync.WaitGroup + + for routineIndex := 0; routineIndex < concurrentCount; routineIndex++ { + waitGroup.Add(1) + + go func(currentRoutineIndex int) { + for cycle := 0; cycle < 10; cycle++ { + checker.IncreaseFailureCount(failureKey) + } + + waitGroup.Done() + }(routineIndex) + } + + waitGroup.Wait() + + failureCount := checker.GetFailureCount(failureKey) + assert.Equal(t, uint32(100), failureCount) +} diff --git a/pkg/errs/global.go b/pkg/errs/global.go index 22b11e73..114b54d7 100644 --- a/pkg/errs/global.go +++ b/pkg/errs/global.go @@ -25,6 +25,7 @@ var ( ErrNoFilesUpload = NewNormalError(NormalSubcategoryGlobal, 15, http.StatusBadRequest, "no files uploaded") ErrUploadedFileEmpty = NewNormalError(NormalSubcategoryGlobal, 16, http.StatusBadRequest, "uploaded file is empty") ErrExceedMaxUploadFileSize = NewNormalError(NormalSubcategoryGlobal, 17, http.StatusBadRequest, "uploaded file size exceeds the maximum allowed size") + ErrFailureCountLimitReached = NewNormalError(NormalSubcategoryGlobal, 18, http.StatusBadRequest, "failure count exceeded maximum limit") ) // GetParameterInvalidMessage returns specific error message for invalid parameter error diff --git a/pkg/services/users.go b/pkg/services/users.go index 7f8cd058..55056445 100644 --- a/pkg/services/users.go +++ b/pkg/services/users.go @@ -58,7 +58,7 @@ var ( ) // GetUserByUsernameOrEmailAndPassword returns the user model according to login name and password -func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginname string, password string) (*models.User, error) { +func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginname string, password string) (*models.User, int64, error) { var user *models.User var err error @@ -71,14 +71,18 @@ func (s *UserService) GetUserByUsernameOrEmailAndPassword(c core.Context, loginn } if err != nil { - return nil, err + return nil, 0, err + } + + if user == nil { + return nil, 0, errs.ErrUserNotFound } if !s.IsPasswordEqualsUserPassword(password, user) { - return nil, errs.ErrUserPasswordWrong + return nil, user.Uid, errs.ErrUserPasswordWrong } - return user, nil + return user, user.Uid, nil } // GetUserById returns the user model according to user uid diff --git a/pkg/settings/setting.go b/pkg/settings/setting.go index 8d583584..bf702537 100644 --- a/pkg/settings/setting.go +++ b/pkg/settings/setting.go @@ -144,6 +144,8 @@ const ( defaultTemporaryTokenExpiredTime uint32 = 300 // 5 minutes defaultEmailVerifyTokenExpiredTime uint32 = 3600 // 60 minutes defaultPasswordResetTokenExpiredTime uint32 = 3600 // 60 minutes + defaultMaxFailuresPerIpPerMinute uint32 = 5 + defaultMaxFailuresPerUserPerMinute uint32 = 5 defaultTransactionPictureFileMaxSize uint32 = 10485760 // 10MB defaultUserAvatarFileMaxSize uint32 = 1048576 // 1MB @@ -286,6 +288,8 @@ type Config struct { EmailVerifyTokenExpiredTimeDuration time.Duration PasswordResetTokenExpiredTime uint32 PasswordResetTokenExpiredTimeDuration time.Duration + MaxFailuresPerIpPerMinute uint32 + MaxFailuresPerUserPerMinute uint32 EnableRequestIdHeader bool // User @@ -768,6 +772,9 @@ func loadSecurityConfiguration(config *Config, configFile *ini.File, sectionName config.PasswordResetTokenExpiredTimeDuration = time.Duration(config.PasswordResetTokenExpiredTime) * time.Second + config.MaxFailuresPerIpPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_ip_per_minute", defaultMaxFailuresPerIpPerMinute) + config.MaxFailuresPerUserPerMinute = getConfigItemUint32Value(configFile, sectionName, "max_failures_per_user_per_minute", defaultMaxFailuresPerUserPerMinute) + config.EnableRequestIdHeader = getConfigItemBoolValue(configFile, sectionName, "request_id_header", true) return nil diff --git a/src/locales/de.json b/src/locales/de.json index 2b979da8..b5b83bc5 100644 --- a/src/locales/de.json +++ b/src/locales/de.json @@ -1167,7 +1167,8 @@ "number invalid": "Nummer ist ungültig", "no files uploaded": "Keine Dateien hochgeladen", "uploaded file is empty": "Hochgeladene Datei ist leer", - "uploaded file size exceeds the maximum allowed size": "Hochgeladene Datei überschreitet die maximal zulässige Größe" + "uploaded file size exceeds the maximum allowed size": "Hochgeladene Datei überschreitet die maximal zulässige Größe", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "ID", diff --git a/src/locales/en.json b/src/locales/en.json index 736daf80..f88956d5 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1167,7 +1167,8 @@ "number invalid": "Number is invalid", "no files uploaded": "No files uploaded", "uploaded file is empty": "Uploaded file is empty", - "uploaded file size exceeds the maximum allowed size": "Uploaded file size exceeds the maximum allowed size" + "uploaded file size exceeds the maximum allowed size": "Uploaded file size exceeds the maximum allowed size", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "ID", diff --git a/src/locales/es.json b/src/locales/es.json index 212003a1..edf54dc3 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -1167,7 +1167,8 @@ "number invalid": "El número no es válido", "no files uploaded": "No se subieron archivos", "uploaded file is empty": "El archivo subido está vacío", - "uploaded file size exceeds the maximum allowed size": "El tamaño del archivo cargado excede el tamaño máximo permitido" + "uploaded file size exceeds the maximum allowed size": "El tamaño del archivo cargado excede el tamaño máximo permitido", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "IDENTIFICACIÓN", diff --git a/src/locales/ja.json b/src/locales/ja.json index e347c5b3..2942d52b 100644 --- a/src/locales/ja.json +++ b/src/locales/ja.json @@ -1167,7 +1167,8 @@ "number invalid": "番号が無効です", "no files uploaded": "アップロードされたファイルはありません", "uploaded file is empty": "アップロードされたファイルは空です", - "uploaded file size exceeds the maximum allowed size": "アップロードされたファイルが最大許容サイズを超えています" + "uploaded file size exceeds the maximum allowed size": "アップロードされたファイルが最大許容サイズを超えています", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "ID", diff --git a/src/locales/ru.json b/src/locales/ru.json index 70b15927..4c0cf06e 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1167,7 +1167,8 @@ "number invalid": "Число недействительно", "no files uploaded": "Файлы не загружены", "uploaded file is empty": "Загруженный файл пуст", - "uploaded file size exceeds the maximum allowed size": "Размер загруженного файла превышает максимально допустимый размер" + "uploaded file size exceeds the maximum allowed size": "Размер загруженного файла превышает максимально допустимый размер", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "ID", diff --git a/src/locales/vi.json b/src/locales/vi.json index 2e32c968..71b4a4e1 100644 --- a/src/locales/vi.json +++ b/src/locales/vi.json @@ -1167,7 +1167,8 @@ "number invalid": "Số không hợp lệ", "no files uploaded": "Không có tệp nào được tải lên", "uploaded file is empty": "Tệp đã tải lên trống", - "uploaded file size exceeds the maximum allowed size": "Kích thước tệp đã tải lên vượt quá kích thước tối đa cho phép" + "uploaded file size exceeds the maximum allowed size": "Kích thước tệp đã tải lên vượt quá kích thước tối đa cho phép", + "failure count exceeded maximum limit": "Failure count exceeded maximum limit, please try again after some time" }, "parameter": { "id": "ID", diff --git a/src/locales/zh_Hans.json b/src/locales/zh_Hans.json index abecc9e8..f2b07914 100644 --- a/src/locales/zh_Hans.json +++ b/src/locales/zh_Hans.json @@ -1167,7 +1167,8 @@ "number invalid": "数字错误", "no files uploaded": "没有上传文件", "uploaded file is empty": "上传的文件为空", - "uploaded file size exceeds the maximum allowed size": "上传的文件大小超出了允许的最大大小" + "uploaded file size exceeds the maximum allowed size": "上传的文件大小超出了允许的最大大小", + "failure count exceeded maximum limit": "失败次数超出最大限制,请稍后重试" }, "parameter": { "id": "ID",