From b31d644b92f00914440812e898f93a3581b33029 Mon Sep 17 00:00:00 2001 From: Luffy <52o@qq52o.cn> Date: Tue, 10 Dec 2024 12:04:05 +0800 Subject: [PATCH 1/9] fix: Fix render comments with line breaks --- pkg/converter/markdown.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index 082a198aa..5dbab9fce 100644 --- a/pkg/converter/markdown.go +++ b/pkg/converter/markdown.go @@ -65,7 +65,7 @@ func Markdown2HTML(source string) string { 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 +124,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() { From 6203b5b44192d214e15de3d84662742de27ac840 Mon Sep 17 00:00:00 2001 From: Luffy <52o@qq52o.cn> Date: Wed, 11 Dec 2024 16:37:29 +0800 Subject: [PATCH 2/9] fix: use TrimSpace --- pkg/converter/markdown.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/converter/markdown.go b/pkg/converter/markdown.go index 5dbab9fce..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,7 +62,7 @@ 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 } From 30534ad0d6eea6e18c2f53ef51f5600db93e378e Mon Sep 17 00:00:00 2001 From: Luffy <52o@qq52o.cn> Date: Tue, 10 Dec 2024 17:08:08 +0800 Subject: [PATCH 3/9] feat: Check HTML comments in question, answer or comments --- i18n/en_US.yaml | 6 ++++++ i18n/zh_CN.yaml | 6 ++++++ internal/base/reason/reason.go | 3 +++ internal/schema/answer_schema.go | 14 ++++++++++++++ internal/schema/comment_schema.go | 14 ++++++++++++++ internal/schema/question_schema.go | 29 +++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index f4c67a188..801acc697 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. diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index 07f8d2ea4..0567a10c1 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: 声望值未达到要求。 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/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/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 } From 4a74eed4a7de8589fe50d1f8568d4c141da69850 Mon Sep 17 00:00:00 2001 From: Luffy <52o@qq52o.cn> Date: Wed, 18 Dec 2024 18:34:10 +0800 Subject: [PATCH 4/9] feat: Add permanently delete --- docs/docs.go | 55 +++++++++++++++++++ docs/swagger.json | 55 +++++++++++++++++++ docs/swagger.yaml | 35 ++++++++++++ i18n/en_US.yaml | 9 ++- i18n/zh_CN.yaml | 9 ++- internal/base/constant/user.go | 6 ++ .../user_backyard_controller.go | 20 +++++++ internal/repo/answer/answer_repo.go | 8 +++ internal/repo/question/question_repo.go | 8 +++ internal/repo/user/user_backyard_repo.go | 9 +++ internal/router/answer_api_router.go | 2 + internal/schema/backyard_user_schema.go | 5 ++ internal/service/answer_common/answer.go | 1 + internal/service/question_common/question.go | 1 + internal/service/user_admin/user_backyard.go | 13 +++++ ui/src/pages/Admin/Answers/index.tsx | 46 +++++++++++++--- ui/src/pages/Admin/Questions/index.tsx | 46 +++++++++++++--- ui/src/pages/Admin/Users/index.tsx | 28 ++++++++++ ui/src/services/common.ts | 4 ++ 19 files changed, 340 insertions(+), 20 deletions(-) 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 801acc697..9d9046222 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1504,6 +1504,7 @@ ui: normal: Normal closed: Closed deleted: Deleted + deleted_permanently: Deleted permanently pending: Pending more: More search: @@ -1539,6 +1540,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 @@ -2297,5 +2301,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 0567a10c1..d06733fb7 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1232,6 +1232,9 @@ ui: modal_content: 该电子邮件地址已经注册。你确定要连接到已有账户吗? modal_cancel: 更改邮箱 modal_confirm: 连接到已有账户 + delete_permanently: + title: 永久删除 + content: 你确定要永久删除吗? password_reset: page_title: 密码重置 btn_name: 重置我的密码 @@ -1471,6 +1474,7 @@ ui: normal: 正常 closed: 已关闭 deleted: 已删除 + deleted_permanently: 永久删除 pending: 等待处理 more: 更多 search: @@ -2259,5 +2263,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/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/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..62f7f789f 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("deleted_at IS NOT NULL").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/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/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..c5a4fd155 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) 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/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 = () => { <>