Merge remote-tracking branch 'origin/feat/1.2.0/img' into test

This commit is contained in:
LinkinStars 2023-10-16 18:47:17 +08:00
commit 06d965822a
12 changed files with 137 additions and 26 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/answerdev/answer/internal/repo/comment"
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/limit"
"github.com/answerdev/answer/internal/repo/meta"
notification2 "github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
@ -154,7 +155,9 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)
commentController := controller.NewCommentController(commentService, rankService, captchaService)
limitRepo := limit.NewRateLimitRepo(dataData)
rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo)
commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware)
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
reportService := report2.NewReportService(reportRepo, objService)
reportController := controller.NewReportController(reportService, rankService, captchaService)
@ -181,8 +184,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService)
questionService := service.NewQuestionService(questionRepo, tagCommonService, questionCommon, userCommon, userRepo, revisionService, metaService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService)
answerService := service.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService)
answerController := controller.NewAnswerController(answerService, rankService, captchaService)
questionController := controller.NewQuestionController(questionService, answerService, rankService, siteInfoCommonService, captchaService, rateLimitMiddleware)
answerController := controller.NewAnswerController(answerService, rankService, captchaService, rateLimitMiddleware)
searchParser := search_parser.NewSearchParser(tagCommonService, userCommon)
searchRepo := search_common.NewSearchRepo(dataData, uniqueIDRepo, userCommon, tagCommonService)
searchService := service.NewSearchService(searchParser, searchRepo)

View File

@ -14,6 +14,8 @@ backend:
other: Data server error.
forbidden_error:
other: Forbidden.
duplicate_request_error:
other: Duplicate submission.
action:
report:
other: Flag

View File

@ -26,4 +26,6 @@ const (
NewQuestionNotificationLimitCacheKeyPrefix = "answer:new-question-notification-limit:"
NewQuestionNotificationLimitCacheTime = 7 * 24 * time.Hour
NewQuestionNotificationLimitMax = 50
RateLimitCacheKeyPrefix = "answer:rate-limit:"
RateLimitCacheTime = 5 * time.Minute
)

View File

@ -9,4 +9,5 @@ var ProviderSetMiddleware = wire.NewSet(
NewAuthUserMiddleware,
NewAvatarMiddleware,
NewShortIDMiddleware,
NewRateLimitMiddleware,
)

View File

@ -0,0 +1,44 @@
package middleware
import (
"encoding/json"
"fmt"
"github.com/answerdev/answer/internal/base/handler"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/repo/limit"
"github.com/answerdev/answer/pkg/encryption"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
type RateLimitMiddleware struct {
limitRepo *limit.LimitRepo
}
// NewRateLimitMiddleware new rate limit middleware
func NewRateLimitMiddleware(limitRepo *limit.LimitRepo) *RateLimitMiddleware {
return &RateLimitMiddleware{
limitRepo: limitRepo,
}
}
// DuplicateRequestRejection detects and rejects duplicate requests
// It only works for the requests that post content. Such as add question, add answer, comment etc.
func (rm *RateLimitMiddleware) DuplicateRequestRejection(ctx *gin.Context, req any) bool {
userID := GetLoginUserIDFromContext(ctx)
fullPath := ctx.FullPath()
reqJson, _ := json.Marshal(req)
key := encryption.MD5(fmt.Sprintf("%s:%s:%s", userID, fullPath, string(reqJson)))
reject, err := rm.limitRepo.CheckAndRecord(ctx, key)
if err != nil {
log.Errorf("check and record rate limit error: %s", err.Error())
return false
}
if !reject {
return false
}
log.Debugf("duplicate request: [%s] %s", fullPath, string(reqJson))
handler.HandleResponse(ctx, errors.BadRequest(reason.DuplicateRequestError), nil)
return true
}

View File

@ -13,6 +13,8 @@ const (
DatabaseError = "base.database_error"
// ForbiddenError forbidden error
ForbiddenError = "base.forbidden_error"
// DuplicateRequestError duplicate request error
DuplicateRequestError = "base.duplicate_request_error"
)
const (

View File

@ -21,9 +21,10 @@ import (
// AnswerController answer controller
type AnswerController struct {
answerService *service.AnswerService
rankService *rank.RankService
actionService *action.CaptchaService
answerService *service.AnswerService
rankService *rank.RankService
actionService *action.CaptchaService
rateLimitMiddleware *middleware.RateLimitMiddleware
}
// NewAnswerController new controller
@ -31,11 +32,13 @@ func NewAnswerController(
answerService *service.AnswerService,
rankService *rank.RankService,
actionService *action.CaptchaService,
rateLimitMiddleware *middleware.RateLimitMiddleware,
) *AnswerController {
return &AnswerController{
answerService: answerService,
rankService: rankService,
actionService: actionService,
answerService: answerService,
rankService: rankService,
actionService: actionService,
rateLimitMiddleware: rateLimitMiddleware,
}
}
@ -168,6 +171,9 @@ func (ac *AnswerController) Add(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
if ac.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) {
return
}
req.QuestionID = uid.DeShortID(req.QuestionID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)

View File

@ -19,9 +19,10 @@ import (
// CommentController comment controller
type CommentController struct {
commentService *comment.CommentService
rankService *rank.RankService
actionService *action.CaptchaService
commentService *comment.CommentService
rankService *rank.RankService
actionService *action.CaptchaService
rateLimitMiddleware *middleware.RateLimitMiddleware
}
// NewCommentController new controller
@ -29,11 +30,13 @@ func NewCommentController(
commentService *comment.CommentService,
rankService *rank.RankService,
actionService *action.CaptchaService,
rateLimitMiddleware *middleware.RateLimitMiddleware,
) *CommentController {
return &CommentController{
commentService: commentService,
rankService: rankService,
actionService: actionService,
commentService: commentService,
rankService: rankService,
actionService: actionService,
rateLimitMiddleware: rateLimitMiddleware,
}
}
@ -52,6 +55,9 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
if handler.BindAndCheck(ctx, req) {
return
}
if cc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) {
return
}
req.ObjectID = uid.DeShortID(req.ObjectID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)

View File

@ -22,11 +22,12 @@ import (
// QuestionController question controller
type QuestionController struct {
questionService *service.QuestionService
answerService *service.AnswerService
rankService *rank.RankService
siteInfoService siteinfo_common.SiteInfoCommonService
actionService *action.CaptchaService
questionService *service.QuestionService
answerService *service.AnswerService
rankService *rank.RankService
siteInfoService siteinfo_common.SiteInfoCommonService
actionService *action.CaptchaService
rateLimitMiddleware *middleware.RateLimitMiddleware
}
// NewQuestionController new controller
@ -36,13 +37,15 @@ func NewQuestionController(
rankService *rank.RankService,
siteInfoService siteinfo_common.SiteInfoCommonService,
actionService *action.CaptchaService,
rateLimitMiddleware *middleware.RateLimitMiddleware,
) *QuestionController {
return &QuestionController{
questionService: questionService,
answerService: answerService,
rankService: rankService,
siteInfoService: siteInfoService,
actionService: actionService,
questionService: questionService,
answerService: answerService,
rankService: rankService,
siteInfoService: siteInfoService,
actionService: actionService,
rateLimitMiddleware: rateLimitMiddleware,
}
}
@ -332,6 +335,9 @@ func (qc *QuestionController) AddQuestion(ctx *gin.Context) {
if ctx.IsAborted() {
return
}
if qc.rateLimitMiddleware.DuplicateRequestRejection(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, requireRanks, err := qc.rankService.CheckOperationPermissionsForRanks(ctx, req.UserID, []string{

View File

@ -73,7 +73,7 @@ var migrations = []Migration{
NewMigration("v1.1.1", "update the length of revision content", updateTheLengthOfRevisionContent, false),
NewMigration("v1.1.2", "add notification config", addNoticeConfig, true),
NewMigration("v1.1.3", "set default user notification config", setDefaultUserNotificationConfig, false),
NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, false),
NewMigration("v1.2.0", "add recover answer permission", addRecoverPermission, true),
}
func GetMigrations() []Migration {

View File

@ -0,0 +1,37 @@
package limit
import (
"context"
"github.com/answerdev/answer/internal/base/constant"
"github.com/answerdev/answer/internal/base/data"
"github.com/answerdev/answer/internal/base/reason"
"github.com/segmentfault/pacman/errors"
)
// LimitRepo auth repository
type LimitRepo struct {
data *data.Data
}
// NewRateLimitRepo new repository
func NewRateLimitRepo(data *data.Data) *LimitRepo {
return &LimitRepo{
data: data,
}
}
// CheckAndRecord check
func (lr *LimitRepo) CheckAndRecord(ctx context.Context, key string) (limit bool, err error) {
_, exist, err := lr.data.Cache.GetString(ctx, constant.RateLimitCacheKeyPrefix+key)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
if exist {
return true, nil
}
err = lr.data.Cache.SetString(ctx, constant.RateLimitCacheKeyPrefix+key, "1", constant.RateLimitCacheTime)
if err != nil {
return false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return false, nil
}

View File

@ -11,6 +11,7 @@ import (
"github.com/answerdev/answer/internal/repo/comment"
"github.com/answerdev/answer/internal/repo/config"
"github.com/answerdev/answer/internal/repo/export"
"github.com/answerdev/answer/internal/repo/limit"
"github.com/answerdev/answer/internal/repo/meta"
"github.com/answerdev/answer/internal/repo/notification"
"github.com/answerdev/answer/internal/repo/plugin_config"
@ -75,4 +76,5 @@ var ProviderSetRepo = wire.NewSet(
user_external_login.NewUserExternalLoginRepo,
plugin_config.NewPluginConfigRepo,
user_notification_config.NewUserNotificationConfigRepo,
limit.NewRateLimitRepo,
)