internal/service/user_admin/user_backyard.go (534 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package user_admin import ( "context" "fmt" "net/mail" "strings" "time" "unicode" "github.com/apache/answer/internal/base/constant" "github.com/apache/answer/internal/base/handler" "github.com/apache/answer/internal/base/translator" "github.com/apache/answer/internal/base/validator" answercommon "github.com/apache/answer/internal/service/answer_common" "github.com/apache/answer/internal/service/badge" "github.com/apache/answer/internal/service/comment_common" "github.com/apache/answer/internal/service/export" notificationcommon "github.com/apache/answer/internal/service/notification_common" "github.com/apache/answer/internal/service/plugin_common" questioncommon "github.com/apache/answer/internal/service/question_common" "github.com/apache/answer/pkg/token" "github.com/apache/answer/internal/base/pager" "github.com/apache/answer/internal/base/reason" "github.com/apache/answer/internal/entity" "github.com/apache/answer/internal/schema" "github.com/apache/answer/internal/service/activity" "github.com/apache/answer/internal/service/auth" "github.com/apache/answer/internal/service/role" "github.com/apache/answer/internal/service/siteinfo_common" usercommon "github.com/apache/answer/internal/service/user_common" "github.com/apache/answer/internal/service/user_external_login" "github.com/apache/answer/pkg/checker" "github.com/jinzhu/copier" "github.com/segmentfault/pacman/errors" "github.com/segmentfault/pacman/log" "golang.org/x/crypto/bcrypt" ) // UserAdminRepo user repository type UserAdminRepo interface { UpdateUserStatus(ctx context.Context, userID string, userStatus, mailStatus int, email string) (err error) GetUserInfo(ctx context.Context, userID string) (user *entity.User, exist bool, err error) GetUserInfoByEmail(ctx context.Context, email string) (user *entity.User, exist bool, err error) GetUserPage(ctx context.Context, page, pageSize int, user *entity.User, usernameOrDisplayName string, isStaff bool) (users []*entity.User, total int64, err error) 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 type UserAdminService struct { userRepo UserAdminRepo userRoleRelService *role.UserRoleRelService authService *auth.AuthService userCommonService *usercommon.UserCommon userActivity activity.UserActiveActivityRepo siteInfoCommonService siteinfo_common.SiteInfoCommonService emailService *export.EmailService questionCommonRepo questioncommon.QuestionRepo answerCommonRepo answercommon.AnswerRepo commentCommonRepo comment_common.CommentCommonRepo userExternalLoginRepo user_external_login.UserExternalLoginRepo notificationRepo notificationcommon.NotificationRepo pluginUserConfigRepo plugin_common.PluginUserConfigRepo badgeAwardRepo badge.BadgeAwardRepo } // NewUserAdminService new user admin service func NewUserAdminService( userRepo UserAdminRepo, userRoleRelService *role.UserRoleRelService, authService *auth.AuthService, userCommonService *usercommon.UserCommon, userActivity activity.UserActiveActivityRepo, siteInfoCommonService siteinfo_common.SiteInfoCommonService, emailService *export.EmailService, questionCommonRepo questioncommon.QuestionRepo, answerCommonRepo answercommon.AnswerRepo, commentCommonRepo comment_common.CommentCommonRepo, userExternalLoginRepo user_external_login.UserExternalLoginRepo, notificationRepo notificationcommon.NotificationRepo, pluginUserConfigRepo plugin_common.PluginUserConfigRepo, badgeAwardRepo badge.BadgeAwardRepo, ) *UserAdminService { return &UserAdminService{ userRepo: userRepo, userRoleRelService: userRoleRelService, authService: authService, userCommonService: userCommonService, userActivity: userActivity, siteInfoCommonService: siteInfoCommonService, emailService: emailService, questionCommonRepo: questionCommonRepo, answerCommonRepo: answerCommonRepo, commentCommonRepo: commentCommonRepo, userExternalLoginRepo: userExternalLoginRepo, notificationRepo: notificationRepo, pluginUserConfigRepo: pluginUserConfigRepo, badgeAwardRepo: badgeAwardRepo, } } // UpdateUserStatus update user func (us *UserAdminService) UpdateUserStatus(ctx context.Context, req *schema.UpdateUserStatusReq) (err error) { // Admin cannot modify their status if req.UserID == req.LoginUserID { return errors.BadRequest(reason.AdminCannotModifySelfStatus) } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return } if !exist { return errors.BadRequest(reason.UserNotFound) } // if user status is deleted if userInfo.Status == entity.UserStatusDeleted { return nil } if req.IsInactive() { userInfo.MailStatus = entity.EmailStatusToBeVerified } if req.IsDeleted() { userInfo.Status = entity.UserStatusDeleted userInfo.EMail = fmt.Sprintf("%s.%d", userInfo.EMail, time.Now().Unix()) } if req.IsSuspended() { userInfo.Status = entity.UserStatusSuspended } if req.IsNormal() { userInfo.Status = entity.UserStatusAvailable userInfo.MailStatus = entity.EmailStatusAvailable } err = us.userRepo.UpdateUserStatus(ctx, userInfo.ID, userInfo.Status, userInfo.MailStatus, userInfo.EMail) if err != nil { return err } // remove all content that user created, such as question, answer, comment, etc. if req.RemoveAllContent { us.removeAllUserCreatedContent(ctx, userInfo.ID) } if req.IsDeleted() { us.removeAllUserConfiguration(ctx, userInfo.ID) } // if user reputation is zero means this user is inactive, so try to activate this user. if req.IsNormal() && userInfo.Rank == 0 { return us.userActivity.UserActive(ctx, userInfo.ID) } return nil } // removeAllUserConfiguration remove all user configuration func (us *UserAdminService) removeAllUserConfiguration(ctx context.Context, userID string) { err := us.userExternalLoginRepo.DeleteUserExternalLoginByUserID(ctx, userID) if err != nil { log.Errorf("remove all user external login error: %v", err) } err = us.notificationRepo.DeleteNotification(ctx, userID) if err != nil { log.Errorf("remove all user notification error: %v", err) } err = us.notificationRepo.DeleteUserNotificationConfig(ctx, userID) if err != nil { log.Errorf("remove all user notification config error: %v", err) } err = us.pluginUserConfigRepo.DeleteUserPluginConfig(ctx, userID) if err != nil { log.Errorf("remove all user plugin config error: %v", err) } err = us.badgeAwardRepo.DeleteUserBadgeAward(ctx, userID) if err != nil { log.Errorf("remove all user badge award error: %v", err) } } // removeAllUserCreatedContent remove all user created content func (us *UserAdminService) removeAllUserCreatedContent(ctx context.Context, userID string) { if err := us.questionCommonRepo.RemoveAllUserQuestion(ctx, userID); err != nil { log.Errorf("remove all user question error: %v", err) } if err := us.answerCommonRepo.RemoveAllUserAnswer(ctx, userID); err != nil { log.Errorf("remove all user answer error: %v", err) } if err := us.commentCommonRepo.RemoveAllUserComment(ctx, userID); err != nil { log.Errorf("remove all user comment error: %v", err) } } // UpdateUserRole update user role func (us *UserAdminService) UpdateUserRole(ctx context.Context, req *schema.UpdateUserRoleReq) (err error) { // Users cannot modify their roles if req.UserID == req.LoginUserID { return errors.BadRequest(reason.UserCannotUpdateYourRole) } err = us.userRoleRelService.SaveUserRole(ctx, req.UserID, req.RoleID) if err != nil { return err } us.authService.RemoveUserAllTokens(ctx, req.UserID) return } // AddUser add user func (us *UserAdminService) AddUser(ctx context.Context, req *schema.AddUserReq) (err error) { _, has, err := us.userRepo.GetUserInfoByEmail(ctx, req.Email) if err != nil { return err } if has { return errors.BadRequest(reason.EmailDuplicate) } hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } userInfo := &entity.User{} userInfo.EMail = req.Email userInfo.DisplayName = req.DisplayName userInfo.Pass = string(hashPwd) userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) if err != nil { return err } userInfo.MailStatus = entity.EmailStatusAvailable userInfo.Status = entity.UserStatusAvailable userInfo.Rank = 1 err = us.userRepo.AddUser(ctx, userInfo) if err != nil { return err } return } // AddUsers add users func (us *UserAdminService) AddUsers(ctx context.Context, req *schema.AddUsersReq) ( resp []*validator.FormErrorField, err error) { resp, err = req.ParseUsers(ctx) if err != nil { return resp, err } errData := us.checkUserDuplicateInner(ctx, req.Users) if errData != nil { return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) } users, errData, err := us.formatBulkAddUsers(ctx, req) if err != nil { return resp, err } if errData != nil { return errData.GetErrField(ctx), errors.BadRequest(reason.RequestFormatError) } return nil, us.userRepo.AddUsers(ctx, users) } func (us *UserAdminService) checkUserDuplicateInner(ctx context.Context, users []*schema.AddUserReq) ( errorData *schema.AddUsersErrorData) { lang := handler.GetLangByCtx(ctx) val := validator.GetValidatorByLang(lang) emails := make(map[string]bool) displayNames := make(map[string]bool) for line, user := range users { if errFields, e := val.Check(user); e != nil { errorData = &schema.AddUsersErrorData{} if len(errFields) > 0 { errorData.Field = errFields[0].ErrorField errorData.ExtraMessage = errFields[0].ErrorMsg } errorData.Line = line + 1 errorData.Content = fmt.Sprintf("%s, %s, %s", user.DisplayName, user.Email, user.Password) return errorData } if emails[user.Email] { return &schema.AddUsersErrorData{ Field: "email", Line: line + 1, Content: user.Email, ExtraMessage: translator.Tr(lang, reason.EmailDuplicate), } } if displayNames[user.DisplayName] { return &schema.AddUsersErrorData{ Field: "name", Line: line + 1, Content: user.DisplayName, ExtraMessage: translator.Tr(lang, reason.UsernameDuplicate), } } emails[user.Email] = true displayNames[user.DisplayName] = true } return nil } func (us *UserAdminService) formatBulkAddUsers(ctx context.Context, req *schema.AddUsersReq) ( users []*entity.User, errorData *schema.AddUsersErrorData, err error) { lang := handler.GetLangByCtx(ctx) errorData = &schema.AddUsersErrorData{Line: -1} for line, user := range req.Users { _, has, e := us.userRepo.GetUserInfoByEmail(ctx, user.Email) if e != nil { return nil, nil, e } if has { errorData.Field = "email" errorData.Line = line + 1 errorData.Content = user.Email errorData.ExtraMessage = translator.Tr(lang, reason.EmailDuplicate) return nil, errorData, nil } userInfo := &entity.User{} userInfo.EMail = user.Email userInfo.DisplayName = user.DisplayName hashPwd, _ := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost) userInfo.Pass = string(hashPwd) userInfo.Username, err = us.userCommonService.MakeUsername(ctx, userInfo.DisplayName) if err != nil { errorData.Field = "name" errorData.Line = line + 1 errorData.Content = user.DisplayName errorData.ExtraMessage = translator.Tr(lang, reason.UsernameInvalid) return nil, errorData, nil } userInfo.MailStatus = entity.EmailStatusAvailable userInfo.Status = entity.UserStatusAvailable userInfo.Rank = 1 users = append(users, userInfo) } return users, nil, nil } // UpdateUserPassword update user password func (us *UserAdminService) UpdateUserPassword(ctx context.Context, req *schema.UpdateUserPasswordReq) (err error) { // Users cannot modify their password if req.UserID == req.LoginUserID { return errors.BadRequest(reason.AdminCannotUpdateTheirPassword) } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } hashPwd, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { return err } err = us.userRepo.UpdateUserPassword(ctx, userInfo.ID, string(hashPwd)) if err != nil { return err } // logout this user us.authService.RemoveUserAllTokens(ctx, req.UserID) return } // EditUserProfile edit user profile func (us *UserAdminService) EditUserProfile(ctx context.Context, req *schema.EditUserProfileReq) ( errFields []*validator.FormErrorField, err error) { if req.UserID == req.LoginUserID { return nil, errors.BadRequest(reason.AdminCannotEditTheirProfile) } userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } if checker.IsInvalidUsername(req.Username) || checker.IsUsersIgnorePath(req.Username) { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameInvalid, }), errors.BadRequest(reason.UsernameInvalid) } userInfo, exist, err = us.userCommonService.GetByUsername(ctx, req.Username) if err != nil { return nil, err } if exist && userInfo.ID != req.UserID { return append(errFields, &validator.FormErrorField{ ErrorField: "username", ErrorMsg: reason.UsernameDuplicate, }), errors.BadRequest(reason.UsernameDuplicate) } userInfo, exist, err = us.userCommonService.GetByEmail(ctx, req.Email) if err != nil { return nil, err } if exist && userInfo.ID != req.UserID { return append(errFields, &validator.FormErrorField{ ErrorField: "email", ErrorMsg: reason.EmailDuplicate, }), errors.BadRequest(reason.EmailDuplicate) } user := &entity.User{} user.ID = req.UserID user.DisplayName = req.DisplayName user.Username = req.Username user.EMail = req.Email user.MailStatus = entity.EmailStatusAvailable err = us.userCommonService.UpdateUserProfile(ctx, user) if err != nil { return nil, err } return } // GetUserInfo get user one func (us *UserAdminService) GetUserInfo(ctx context.Context, userID string) (resp *schema.GetUserInfoResp, err error) { user, exist, err := us.userRepo.GetUserInfo(ctx, userID) if err != nil { return } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } resp = &schema.GetUserInfoResp{} _ = copier.Copy(resp, user) return resp, nil } // GetUserPage get user list page func (us *UserAdminService) GetUserPage(ctx context.Context, req *schema.GetUserPageReq) (pageModel *pager.PageModel, err error) { user := &entity.User{} _ = copier.Copy(user, req) if req.IsInactive() { user.MailStatus = entity.EmailStatusToBeVerified user.Status = entity.UserStatusAvailable } else if req.IsSuspended() { user.Status = entity.UserStatusSuspended } else if req.IsDeleted() { user.Status = entity.UserStatusDeleted } else { user.MailStatus = entity.EmailStatusAvailable user.Status = entity.UserStatusAvailable } if len(req.Query) > 0 { if email, e := mail.ParseAddress(req.Query); e == nil { user.EMail = email.Address req.Query = "" } else if strings.HasPrefix(req.Query, "user:") { id := strings.TrimSpace(strings.TrimPrefix(req.Query, "user:")) idSearch := true for _, r := range id { if !unicode.IsDigit(r) { idSearch = false break } } if idSearch { user.ID = id req.Query = "" } else { req.Query = id } } } users, total, err := us.userRepo.GetUserPage(ctx, req.Page, req.PageSize, user, req.Query, req.Staff) if err != nil { return } avatarMapping := us.siteInfoCommonService.FormatListAvatar(ctx, users) resp := make([]*schema.GetUserPageResp, 0) for _, u := range users { t := &schema.GetUserPageResp{ UserID: u.ID, CreatedAt: u.CreatedAt.Unix(), Username: u.Username, EMail: u.EMail, Rank: u.Rank, DisplayName: u.DisplayName, Avatar: avatarMapping[u.ID].GetURL(), } if u.Status == entity.UserStatusDeleted { t.Status = constant.UserDeleted t.DeletedAt = u.DeletedAt.Unix() } else if u.Status == entity.UserStatusSuspended { t.Status = constant.UserSuspended t.SuspendedAt = u.SuspendedAt.Unix() } else if u.MailStatus == entity.EmailStatusToBeVerified { t.Status = constant.UserInactive } else { t.Status = constant.UserNormal } resp = append(resp, t) } us.setUserRoleInfo(ctx, resp) return pager.NewPageModel(total, resp), nil } func (us *UserAdminService) setUserRoleInfo(ctx context.Context, resp []*schema.GetUserPageResp) { var userIDs []string for _, u := range resp { userIDs = append(userIDs, u.UserID) } userRoleMapping, err := us.userRoleRelService.GetUserRoleMapping(ctx, userIDs) if err != nil { log.Error(err) return } for _, u := range resp { r := userRoleMapping[u.UserID] if r == nil { continue } u.RoleID = r.ID u.RoleName = r.Name } } func (us *UserAdminService) GetUserActivation(ctx context.Context, req *schema.GetUserActivationReq) ( resp *schema.GetUserActivationResp, err error) { userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return nil, err } if !exist { return nil, errors.BadRequest(reason.UserNotFound) } general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { return nil, err } data := &schema.EmailCodeContent{ Email: userInfo.EMail, UserID: userInfo.ID, } code := token.GenerateToken() us.emailService.SaveCode(ctx, userInfo.ID, code, data.ToJSONString()) resp = &schema.GetUserActivationResp{ ActivationURL: fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code), } return resp, nil } // SendUserActivation send user activation email func (us *UserAdminService) SendUserActivation(ctx context.Context, req *schema.SendUserActivationReq) (err error) { userInfo, exist, err := us.userRepo.GetUserInfo(ctx, req.UserID) if err != nil { return err } if !exist { return errors.BadRequest(reason.UserNotFound) } general, err := us.siteInfoCommonService.GetSiteGeneral(ctx) if err != nil { return err } data := &schema.EmailCodeContent{ Email: userInfo.EMail, UserID: userInfo.ID, } code := token.GenerateToken() verifyEmailURL := fmt.Sprintf("%s/users/account-activation?code=%s", general.SiteUrl, code) title, body, err := us.emailService.RegisterTemplate(ctx, verifyEmailURL) if err != nil { return err } 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) }