diff --git a/docs/docs.go b/docs/docs.go index da050e10b..109300677 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -295,6 +295,45 @@ const docTemplate = `{ } } }, + "/answer/admin/api/delete/permanently": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "delete permanently", + "parameters": [ + { + "description": "DeletePermanentlyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.DeletePermanentlyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "description": "Get language options", @@ -8158,6 +8197,22 @@ const docTemplate = `{ } } }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, "schema.EditUserProfileReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index bcce7817d..53b95cb8f 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -268,6 +268,45 @@ } } }, + "/answer/admin/api/delete/permanently": { + "delete": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "delete permanently", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "admin" + ], + "summary": "delete permanently", + "parameters": [ + { + "description": "DeletePermanentlyReq", + "name": "data", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/schema.DeletePermanentlyReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.RespBody" + } + } + } + } + }, "/answer/admin/api/language/options": { "get": { "description": "Get language options", @@ -8131,6 +8170,22 @@ } } }, + "schema.DeletePermanentlyReq": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "users", + "questions", + "answers" + ] + } + } + }, "schema.EditUserProfileReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5e22186a9..55b18c5e2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -495,6 +495,17 @@ definitions: name: type: string type: object + schema.DeletePermanentlyReq: + properties: + type: + enum: + - users + - questions + - answers + type: string + required: + - type + type: object schema.EditUserProfileReq: properties: display_name: @@ -3106,6 +3117,30 @@ paths: summary: DashboardInfo tags: - admin + /answer/admin/api/delete/permanently: + delete: + consumes: + - application/json + description: delete permanently + parameters: + - description: DeletePermanentlyReq + in: body + name: data + required: true + schema: + $ref: '#/definitions/schema.DeletePermanentlyReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.RespBody' + security: + - ApiKeyAuth: [] + summary: delete permanently + tags: + - admin /answer/admin/api/language/options: get: description: Get language options diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index df83c6e80..8391a1d6c 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -169,6 +169,8 @@ backend: other: No permission to update. question_closed_cannot_add: other: Questions are closed and cannot be added. + content_cannot_empty: + other: Answer content cannot be empty. comment: edit_without_permission: other: Comment are not allowed to edit. @@ -176,6 +178,8 @@ backend: other: Comment not found. cannot_edit_after_deadline: other: The comment time has been too long to modify. + content_cannot_empty: + other: Comment content cannot be empty. email: duplicate: other: Email already exists. @@ -225,6 +229,8 @@ backend: other: No permission to close. cannot_update: other: No permission to update. + content_cannot_empty: + other: Content cannot be empty. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. @@ -1498,6 +1504,7 @@ ui: normal: Normal closed: Closed deleted: Deleted + deleted_permanently: Deleted permanently pending: Pending more: More view: View @@ -1536,6 +1543,9 @@ ui: cannot_vote_for_self: You can't vote for your own post. modal_confirm: title: Error... + delete_permanently: + title: Delete permanently + content: Are you sure you want to delete permanently? account_result: success: Your new account is confirmed; you will be redirected to the home page. link: Continue to homepage @@ -2294,5 +2304,6 @@ ui: user_deleted: This user has been deleted. badge_activated: This badge has been activated. badge_inactivated: This badge has been inactivated. - - + users_deleted: These users have been deleted. + posts_deleted: These questions have been deleted. + answers_deleted: These answers have been deleted. diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 2ce84a026..17ba2e47d 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -168,6 +168,8 @@ backend: other: 没有更新权限。 question_closed_cannot_add: other: 问题已关闭,无法添加。 + content_cannot_empty: + other: 回答内容不能为空。 comment: edit_without_permission: other: 不允许编辑评论。 @@ -175,6 +177,8 @@ backend: other: 评论未找到。 cannot_edit_after_deadline: other: 评论时间太久,无法修改。 + content_cannot_empty: + other: 评论内容不能为空。 email: duplicate: other: 邮箱已存在。 @@ -224,6 +228,8 @@ backend: other: 没有关闭权限。 cannot_update: other: 没有更新权限。 + content_cannot_empty: + other: 内容不能为空。 rank: fail_to_meet_the_condition: other: 声望值未达到要求。 @@ -1226,6 +1232,9 @@ ui: modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? modal_cancel: 更改邮箱 modal_confirm: 连接到已有账户 + delete_permanently: + title: 永久删除 + content: 你确定要永久删除吗? password_reset: page_title: 密码重置 btn_name: 重置我的密码 @@ -1465,6 +1474,7 @@ ui: normal: 正常 closed: 已关闭 deleted: 已删除 + deleted_permanently: 永久删除 pending: 等待处理 more: 更多 view: 视图 @@ -2256,5 +2266,6 @@ ui: user_deleted: 此用户已被删除 badge_activated: 此徽章已被激活。 badge_inactivated: 此徽章已被禁用。 - - + users_deleted: 这些用户已被删除。 + posts_deleted: 这些帖子已被删除。 + answers_deleted: 这些回答已被删除。 diff --git a/internal/base/constant/user.go b/internal/base/constant/user.go index d453e4436..80774e0df 100644 --- a/internal/base/constant/user.go +++ b/internal/base/constant/user.go @@ -30,6 +30,12 @@ const ( EmailStatusToBeVerified = 2 ) +const ( + DeletePermanentlyUsers = "users" + DeletePermanentlyQuestions = "questions" + DeletePermanentlyAnswers = "answers" +) + func ConvertUserStatus(status, mailStatus int) string { switch status { case 1: diff --git a/internal/base/middleware/accept_language.go b/internal/base/middleware/accept_language.go index 6cb7d95d4..3eaad75e2 100644 --- a/internal/base/middleware/accept_language.go +++ b/internal/base/middleware/accept_language.go @@ -22,36 +22,32 @@ package middleware import ( "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/handler" + "github.com/apache/incubator-answer/internal/base/translator" "github.com/gin-gonic/gin" "github.com/segmentfault/pacman/i18n" -) - -var ( - langMapping = map[i18n.Language]bool{ - i18n.LanguageChinese: true, - i18n.LanguageChineseTraditional: true, - i18n.LanguageEnglish: true, - i18n.LanguageGerman: true, - i18n.LanguageSpanish: true, - i18n.LanguageFrench: true, - i18n.LanguageItalian: true, - i18n.LanguageJapanese: true, - i18n.LanguageKorean: true, - i18n.LanguagePortuguese: true, - i18n.LanguageRussian: true, - i18n.LanguageVietnamese: true, - } + "golang.org/x/text/language" + "strings" ) // ExtractAndSetAcceptLanguage extract accept language from header and set to context func ExtractAndSetAcceptLanguage(ctx *gin.Context) { // The language of our front-end configuration, like en_US lang := handler.GetLang(ctx) - if langMapping[lang] { - ctx.Set(constant.AcceptLanguageFlag, lang) + tag, _, err := language.ParseAcceptLanguage(string(lang)) + if err != nil || len(tag) == 0 { + ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) return } + acceptLang := strings.ReplaceAll(tag[0].String(), "-", "_") + + for _, option := range translator.LanguageOptions { + if option.Value == acceptLang { + ctx.Set(constant.AcceptLanguageFlag, i18n.Language(acceptLang)) + return + } + } + // default language ctx.Set(constant.AcceptLanguageFlag, i18n.LanguageEnglish) } diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 24d7ab5f9..f318e3359 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -46,12 +46,15 @@ const ( QuestionCannotUpdate = "error.question.cannot_update" QuestionAlreadyDeleted = "error.question.already_deleted" QuestionUnderReview = "error.question.under_review" + QuestionContentCannotEmpty = "error.question.content_cannot_empty" AnswerNotFound = "error.answer.not_found" AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotUpdate = "error.answer.cannot_update" AnswerCannotAddByClosedQuestion = "error.answer.question_closed_cannot_add" AnswerRestrictAnswer = "error.answer.restrict_answer" + AnswerContentCannotEmpty = "error.answer.content_cannot_empty" CommentEditWithoutPermission = "error.comment.edit_without_permission" + CommentContentCannotEmpty = "error.comment.content_cannot_empty" DisallowVote = "error.object.disallow_vote" DisallowFollow = "error.object.disallow_follow" DisallowVoteYourSelf = "error.object.disallow_vote_your_self" diff --git a/internal/controller_admin/user_backyard_controller.go b/internal/controller_admin/user_backyard_controller.go index a2ab3f2ec..1d9fb612f 100644 --- a/internal/controller_admin/user_backyard_controller.go +++ b/internal/controller_admin/user_backyard_controller.go @@ -242,3 +242,23 @@ func (uc *UserAdminController) SendUserActivation(ctx *gin.Context) { err := uc.userService.SendUserActivation(ctx, req) handler.HandleResponse(ctx, err, nil) } + +// DeletePermanently delete permanently +// @Summary delete permanently +// @Description delete permanently +// @Security ApiKeyAuth +// @Tags admin +// @Accept json +// @Produce json +// @Param data body schema.DeletePermanentlyReq true "DeletePermanentlyReq" +// @Success 200 {object} handler.RespBody +// @Router /answer/admin/api/delete/permanently [delete] +func (uc *UserAdminController) DeletePermanently(ctx *gin.Context) { + req := &schema.DeletePermanentlyReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + err := uc.userService.DeletePermanently(ctx, req) + handler.HandleResponse(ctx, err, nil) +} diff --git a/internal/repo/activity/vote_repo.go b/internal/repo/activity/vote_repo.go index 7e8b02e64..37d907b05 100644 --- a/internal/repo/activity/vote_repo.go +++ b/internal/repo/activity/vote_repo.go @@ -240,6 +240,9 @@ func (vr *VoteRepo) setActivityRankToZeroIfUserReachLimit(ctx context.Context, s op *schema.VoteOperationInfo, userInfoMapping map[string]*entity.User, maxDailyRank int) (err error) { // check if user reach daily rank limit for _, activity := range op.Activities { + if userInfoMapping[activity.ActivityUserID] == nil { + continue + } if activity.Rank > 0 { // check if reach max daily rank reach, err := vr.userRankRepo.CheckReachLimit(ctx, session, activity.ActivityUserID, maxDailyRank) diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index f59d60063..f4bba8cc5 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -527,3 +527,11 @@ func (ar *answerRepo) updateSearch(ctx context.Context, answerID string) (err er err = s.UpdateContent(ctx, content) return } + +func (ar *answerRepo) DeletePermanentlyAnswers(ctx context.Context) error { + _, err := ar.data.DB.Context(ctx).Where("status = ?", entity.AnswerStatusDeleted).Delete(&entity.Answer{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} diff --git a/internal/repo/question/question_repo.go b/internal/repo/question/question_repo.go index 9b1d212ee..7000e75f2 100644 --- a/internal/repo/question/question_repo.go +++ b/internal/repo/question/question_repo.go @@ -167,6 +167,14 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex return nil } +func (qr *questionRepo) DeletePermanentlyQuestions(ctx context.Context) (err error) { + _, err = qr.data.DB.Context(ctx).Where("status = ?", entity.QuestionStatusDeleted).Delete(&entity.Question{}) + if err != nil { + return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return nil +} + func (qr *questionRepo) RecoverQuestion(ctx context.Context, questionID string) (err error) { questionID = uid.DeShortID(questionID) _, err = qr.data.DB.Context(ctx).ID(questionID).Cols("status").Update(&entity.Question{Status: entity.QuestionStatusAvailable}) diff --git a/internal/repo/user/user_backyard_repo.go b/internal/repo/user/user_backyard_repo.go index 44a05cfbf..abcbb09f9 100644 --- a/internal/repo/user/user_backyard_repo.go +++ b/internal/repo/user/user_backyard_repo.go @@ -175,3 +175,12 @@ func (ur *userAdminRepo) GetUserPage(ctx context.Context, page, pageSize int, us tryToDecorateUserListFromUserCenter(ctx, ur.data, users) return } + +// DeletePermanentlyUsers delete permanently users +func (ur *userAdminRepo) DeletePermanentlyUsers(ctx context.Context) (err error) { + _, err = ur.data.DB.Context(ctx).Where("status = ?", entity.UserStatusDeleted).Delete(&entity.User{}) + if err != nil { + err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack() + } + return +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index 2927afef6..b090f9c77 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -328,6 +328,8 @@ func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) { r.PUT("/user/password", a.adminUserController.UpdateUserPassword) r.PUT("/user/profile", a.adminUserController.EditUserProfile) + r.DELETE("/delete/permanently", a.adminUserController.DeletePermanently) + // reason r.GET("/reasons", a.reasonController.Reasons) diff --git a/internal/schema/answer_schema.go b/internal/schema/answer_schema.go index 01bc64a29..6c6e0881b 100644 --- a/internal/schema/answer_schema.go +++ b/internal/schema/answer_schema.go @@ -20,8 +20,10 @@ package schema import ( + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/pkg/converter" + "github.com/segmentfault/pacman/errors" ) // RemoveAnswerReq delete answer request @@ -60,6 +62,12 @@ type AnswerAddReq struct { func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }), errors.BadRequest(reason.AnswerContentCannotEmpty) + } return nil, nil } @@ -79,6 +87,12 @@ type AnswerUpdateReq struct { func (req *AnswerUpdateReq) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }), errors.BadRequest(reason.AnswerContentCannotEmpty) + } return nil, nil } diff --git a/internal/schema/backyard_user_schema.go b/internal/schema/backyard_user_schema.go index 7c690aee3..966665a46 100644 --- a/internal/schema/backyard_user_schema.go +++ b/internal/schema/backyard_user_schema.go @@ -133,6 +133,11 @@ type AddUsersReq struct { Users []*AddUserReq `json:"-"` } +// DeletePermanentlyReq delete permanently request +type DeletePermanentlyReq struct { + Type string `validate:"required,oneof=users questions answers" json:"type"` +} + type AddUsersErrorData struct { // optional. error field name. Field string `json:"field"` diff --git a/internal/schema/comment_schema.go b/internal/schema/comment_schema.go index 275e1b9af..59ed898d9 100644 --- a/internal/schema/comment_schema.go +++ b/internal/schema/comment_schema.go @@ -20,10 +20,12 @@ package schema import ( + "github.com/apache/incubator-answer/internal/base/reason" "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/internal/entity" "github.com/apache/incubator-answer/pkg/converter" "github.com/jinzhu/copier" + "github.com/segmentfault/pacman/errors" ) // AddCommentReq add comment request @@ -53,6 +55,12 @@ type AddCommentReq struct { func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) { req.ParsedText = converter.Markdown2HTML(req.OriginalText) + if req.ParsedText == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "original_text", + ErrorMsg: reason.CommentContentCannotEmpty, + }), errors.BadRequest(reason.CommentContentCannotEmpty) + } return nil, nil } @@ -88,6 +96,12 @@ type UpdateCommentReq struct { func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) { req.ParsedText = converter.Markdown2HTML(req.OriginalText) + if req.ParsedText == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "original_text", + ErrorMsg: reason.CommentContentCannotEmpty, + }), errors.BadRequest(reason.CommentContentCannotEmpty) + } return nil, nil } diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index accc3374f..9cbd8cc7f 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -20,6 +20,8 @@ package schema import ( + "github.com/apache/incubator-answer/internal/base/reason" + "github.com/segmentfault/pacman/errors" "strings" "time" @@ -97,6 +99,12 @@ func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err erro tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }), errors.BadRequest(reason.QuestionContentCannotEmpty) + } return nil, nil } @@ -129,6 +137,21 @@ func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } + if req.HTML == "" { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }) + } + if req.AnswerHTML == "" { + errFields = append(errFields, &validator.FormErrorField{ + ErrorField: "answer_content", + ErrorMsg: reason.AnswerContentCannotEmpty, + }) + } + if req.HTML == "" || req.AnswerHTML == "" { + return errFields, errors.BadRequest(reason.QuestionContentCannotEmpty) + } return nil, nil } @@ -203,6 +226,12 @@ type QuestionUpdateInviteUser struct { func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) + if req.HTML == "" { + return append(errFields, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: reason.QuestionContentCannotEmpty, + }), errors.BadRequest(reason.QuestionContentCannotEmpty) + } return nil, nil } diff --git a/internal/service/answer_common/answer.go b/internal/service/answer_common/answer.go index 45b11886a..be40fe485 100644 --- a/internal/service/answer_common/answer.go +++ b/internal/service/answer_common/answer.go @@ -51,6 +51,7 @@ type AnswerRepo interface { GetAnswerCount(ctx context.Context) (count int64, err error) RemoveAllUserAnswer(ctx context.Context, userID string) (err error) SumVotesByQuestionID(ctx context.Context, questionID string) (float64, error) + DeletePermanentlyAnswers(ctx context.Context) (err error) } // AnswerCommon user service diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index fc01159ec..8a70fff65 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -63,6 +63,7 @@ type QuestionRepo interface { GetRecommendQuestionPageByTags(ctx context.Context, userID string, tagIDs, followedQuestionIDs []string, page, pageSize int) (questionList []*entity.Question, total int64, err error) UpdateQuestionStatus(ctx context.Context, questionID string, status int) (err error) UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error) + DeletePermanentlyQuestions(ctx context.Context) (err error) RecoverQuestion(ctx context.Context, questionID string) (err error) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) GetQuestionsByTitle(ctx context.Context, title string, pageSize int) (questionList []*entity.Question, err error) @@ -383,6 +384,7 @@ func (qs *QuestionCommon) FormatQuestionsPage( LastAnswerID: questionInfo.LastAnswerID, Pin: questionInfo.Pin, Show: questionInfo.Show, + Operator: &schema.QuestionPageRespOperator{ID: questionInfo.UserID}, } questionIDs = append(questionIDs, questionInfo.ID) @@ -456,7 +458,6 @@ func (qs *QuestionCommon) FormatQuestionsPage( item.Operator.Status = userInfo.Status } } - } return formattedQuestions, nil } diff --git a/internal/service/user_admin/user_backyard.go b/internal/service/user_admin/user_backyard.go index ebe1ea741..e7f63d5f9 100644 --- a/internal/service/user_admin/user_backyard.go +++ b/internal/service/user_admin/user_backyard.go @@ -63,6 +63,7 @@ type UserAdminRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) AddUsers(ctx context.Context, users []*entity.User) (err error) UpdateUserPassword(ctx context.Context, userID string, password string) (err error) + DeletePermanentlyUsers(ctx context.Context) (err error) } // UserAdminService user service @@ -578,3 +579,15 @@ func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema. go us.emailService.SendAndSaveCode(ctx, userInfo.ID, userInfo.EMail, title, body, code, data.ToJSONString()) return nil } + +func (us *UserAdminService) DeletePermanently(ctx context.Context, req *schema.DeletePermanentlyReq) (err error) { + if req.Type == constant.DeletePermanentlyUsers { + return us.userRepo.DeletePermanentlyUsers(ctx) + } else if req.Type == constant.DeletePermanentlyQuestions { + return us.questionCommonRepo.DeletePermanentlyQuestions(ctx) + } else if req.Type == constant.DeletePermanentlyAnswers { + return us.answerCommonRepo.DeletePermanentlyAnswers(ctx) + } + + return errors.BadRequest(reason.RequestFormatError) +} diff --git a/internal/service/user_common/user.go b/internal/service/user_common/user.go index 7980044e1..d05ffd4b4 100644 --- a/internal/service/user_common/user.go +++ b/internal/service/user_common/user.go @@ -141,6 +141,7 @@ func (us *UserCommon) UpdateQuestionCount(ctx context.Context, userID string, nu } func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, userIDs []string) (map[string]*schema.UserBasicInfo, error) { + userIDs = checker.FilterEmptyString(userIDs) userMap := make(map[string]*schema.UserBasicInfo) if len(userIDs) == 0 { return userMap, nil @@ -155,6 +156,15 @@ func (us *UserCommon) BatchUserBasicInfoByID(ctx context.Context, userIDs []stri info.Avatar = avatarMapping[user.ID].GetURL() userMap[user.ID] = info } + for _, id := range userIDs { + if _, ok := userMap[id]; !ok { + userMap[id] = &schema.UserBasicInfo{ + ID: id, + DisplayName: "user" + converter.DeleteUserDisplay(id), + Status: constant.UserDeleted, + } + } + } return userMap, nil } diff --git a/pkg/checker/zero_string.go b/pkg/checker/zero_string.go index 7b7e3111a..7789cf632 100644 --- a/pkg/checker/zero_string.go +++ b/pkg/checker/zero_string.go @@ -23,3 +23,14 @@ package checker func IsNotZeroString(s string) bool { return len(s) > 0 && s != "0" } + +// FilterEmptyString filter empty string from string slice +func FilterEmptyString(strs []string) []string { + var result []string + for _, str := range strs { + if IsNotZeroString(str) { + result = append(result, str) + } + } + return result +} diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index 082a198aa..eb121c01e 100644 --- a/pkg/converter/markdown.go +++ b/pkg/converter/markdown.go @@ -22,6 +22,7 @@ package converter import ( "bytes" "regexp" + "strings" "github.com/asaskevich/govalidator" "github.com/microcosm-cc/bluemonday" @@ -61,11 +62,11 @@ func Markdown2HTML(source string) string { filter.AllowElements("kbd") filter.AllowAttrs("title").Matching(regexp.MustCompile(`^[\p{L}\p{N}\s\-_',\[\]!\./\\\(\)]*$|^@embed?$`)).Globally() filter.AllowAttrs("start").OnElements("ol") - html = filter.Sanitize(html) + html = strings.TrimSpace(filter.Sanitize(html)) return html } -// Markdown2BasicHTML convert markdown to html ,Only basic syntax can be used +// Markdown2BasicHTML convert markdown to html, Only basic syntax can be used func Markdown2BasicHTML(source string) string { content := Markdown2HTML(source) filter := bluemonday.NewPolicy() @@ -124,7 +125,7 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte, l := n.Lines().Len() for i := 0; i < l; i++ { line := n.Lines().At(i) - r.Writer.SecureWrite(w, r.Filter.SanitizeBytes(line.Value(source))) + r.Writer.SecureWrite(w, line.Value(source)) } } else { if n.HasClosure() { diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 7f7e6fae2..7ab2e80a5 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -18,7 +18,7 @@ */ import { FC } from 'react'; -import { Form, Table, Stack } from 'react-bootstrap'; +import { Form, Table, Stack, Button } from 'react-bootstrap'; import { useSearchParams, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -31,12 +31,14 @@ import { BaseUserCard, Empty, QueryGroup, + Modal, } from '@/components'; import { ADMIN_LIST_STATUS } from '@/common/constants'; import * as Type from '@/common/interface'; -import { useAnswerSearch } from '@/services'; +import { deletePermanently, useAnswerSearch } from '@/services'; import { escapeRemove } from '@/utils'; import { pathFactory } from '@/router/pathFactory'; +import { toastStore } from '@/stores'; import AnswerAction from './components/Action'; @@ -68,6 +70,24 @@ const Answers: FC = () => { }); const count = listData?.count || 0; + const handleDeletePermanently = () => { + Modal.confirm({ + title: t('title', { keyPrefix: 'delete_permanently' }), + content: t('content', { keyPrefix: 'delete_permanently' }), + cancelBtnVariant: 'link', + confirmText: t('ok', { keyPrefix: 'btns' }), + onConfirm: () => { + deletePermanently('answers').then(() => { + toastStore.getState().show({ + msg: t('answers_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + refreshList(); + }); + }, + }); + }; + const handleFilter = (e) => { urlSearchParams.set('query', e.target.value); urlSearchParams.delete('page'); @@ -77,12 +97,22 @@ const Answers: FC = () => { <>