mirror of
https://github.com/openimsdk/open-im-server.git
synced 2026-07-09 13:51:12 +08:00
Merge pull request #19 from sok-im/feature/delete_user
Feature/delete user
This commit is contained in:
commit
d0d897b9b8
@ -18,7 +18,7 @@ prometheus:
|
|||||||
|
|
||||||
maxConcurrentWorkers: 3
|
maxConcurrentWorkers: 3
|
||||||
#Use geTui for offline push notifications, or choose fcm or jpush; corresponding configuration settings must be specified.
|
#Use geTui for offline push notifications, or choose fcm or jpush; corresponding configuration settings must be specified.
|
||||||
enable: geTui
|
enable: fcm
|
||||||
geTui:
|
geTui:
|
||||||
pushUrl: https://restapi.getui.com/v2/$appId
|
pushUrl: https://restapi.getui.com/v2/$appId
|
||||||
masterSecret:
|
masterSecret:
|
||||||
@ -28,7 +28,7 @@ geTui:
|
|||||||
channelName:
|
channelName:
|
||||||
fcm:
|
fcm:
|
||||||
# Prioritize using file paths. If the file path is empty, use URL
|
# Prioritize using file paths. If the file path is empty, use URL
|
||||||
filePath: # File path is concatenated with the parameters passed in through - c(`mage` default pass in `config/`) and filePath.
|
filePath: sokim-firebase-adminsdk.json # File path is concatenated with the parameters passed in through - c(`mage` default pass in `config/`) and filePath.
|
||||||
authURL: # Must start with https or http.
|
authURL: # Must start with https or http.
|
||||||
jpush:
|
jpush:
|
||||||
appKey:
|
appKey:
|
||||||
|
|||||||
144
internal/api/delete_user.go
Normal file
144
internal/api/delete_user.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/authverify"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/common/storage/database"
|
||||||
|
"github.com/openimsdk/open-im-server/v3/pkg/rpcli"
|
||||||
|
"github.com/openimsdk/protocol/constant"
|
||||||
|
"github.com/openimsdk/protocol/group"
|
||||||
|
"github.com/openimsdk/protocol/relation"
|
||||||
|
"github.com/openimsdk/protocol/sdkws"
|
||||||
|
"github.com/openimsdk/tools/apiresp"
|
||||||
|
"github.com/openimsdk/tools/errs"
|
||||||
|
"github.com/openimsdk/tools/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeleteUserApi handles real account deletion (hard delete).
|
||||||
|
// It follows the same direct-DB pattern as UserGlobalBlackApi.
|
||||||
|
type DeleteUserApi struct {
|
||||||
|
userDB database.User
|
||||||
|
authClient *rpcli.AuthClient
|
||||||
|
groupClient group.GroupClient
|
||||||
|
friendClient relation.FriendClient
|
||||||
|
imAdminUserIDs []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDeleteUserApi(
|
||||||
|
userDB database.User,
|
||||||
|
authClient *rpcli.AuthClient,
|
||||||
|
groupClient group.GroupClient,
|
||||||
|
friendClient relation.FriendClient,
|
||||||
|
imAdminUserIDs []string,
|
||||||
|
) *DeleteUserApi {
|
||||||
|
return &DeleteUserApi{
|
||||||
|
userDB: userDB,
|
||||||
|
authClient: authClient,
|
||||||
|
groupClient: groupClient,
|
||||||
|
friendClient: friendClient,
|
||||||
|
imAdminUserIDs: imAdminUserIDs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteUserReq struct {
|
||||||
|
UserID string `json:"userID" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser permanently deletes a user account and cleans up associated data.
|
||||||
|
// Steps: force-logout → delete friends → quit/kick groups → hard-delete user doc.
|
||||||
|
// Caller must be the same user as userID, or an IM admin (see CheckAccessV3).
|
||||||
|
func (d *DeleteUserApi) DeleteUser(c *gin.Context) {
|
||||||
|
var req deleteUserReq
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
apiresp.GinError(c, errs.ErrArgs.WrapMsg(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Only the user themselves (or an IM admin) may delete the account.
|
||||||
|
if err := authverify.CheckAccessV3(c, req.UserID, d.imAdminUserIDs); err != nil {
|
||||||
|
apiresp.GinError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Verify user exists
|
||||||
|
users, err := d.userDB.Find(c, []string{req.UserID})
|
||||||
|
if err != nil {
|
||||||
|
apiresp.GinError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(users) == 0 {
|
||||||
|
apiresp.GinError(c, errs.ErrRecordNotFound.WrapMsg("user not found", "userID", req.UserID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Force logout from every platform
|
||||||
|
for platformID := range constant.PlatformID2Name {
|
||||||
|
if int32(platformID) == constant.AdminPlatformID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := d.authClient.ForceLogout(c, req.UserID, int32(platformID)); err != nil {
|
||||||
|
log.ZWarn(c, "DeleteUser: ForceLogout failed", err, "userID", req.UserID, "platformID", platformID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete all friendships (both directions: target→friend and friend→target)
|
||||||
|
friendIDsResp, err := d.friendClient.GetFriendIDs(c, &relation.GetFriendIDsReq{UserID: req.UserID})
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(c, "DeleteUser: GetFriendIDs failed", err, "userID", req.UserID)
|
||||||
|
} else {
|
||||||
|
for _, friendID := range friendIDsResp.FriendIDs {
|
||||||
|
// Remove from target user's friend list
|
||||||
|
if _, err := d.friendClient.DeleteFriend(c, &relation.DeleteFriendReq{
|
||||||
|
OwnerUserID: req.UserID,
|
||||||
|
FriendUserID: friendID,
|
||||||
|
}); err != nil {
|
||||||
|
log.ZWarn(c, "DeleteUser: DeleteFriend (owner→friend) failed", err,
|
||||||
|
"ownerUserID", req.UserID, "friendUserID", friendID)
|
||||||
|
}
|
||||||
|
// Remove from the friend's friend list
|
||||||
|
//if _, err := d.friendClient.DeleteFriend(c, &relation.DeleteFriendReq{
|
||||||
|
// OwnerUserID: friendID,
|
||||||
|
// FriendUserID: req.UserID,
|
||||||
|
//}); err != nil {
|
||||||
|
// log.ZWarn(c, "DeleteUser: DeleteFriend (friend→owner) failed", err,
|
||||||
|
// "ownerUserID", friendID, "friendUserID", req.UserID)
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Quit / kick from all joined groups (paginated, page size 100)
|
||||||
|
pageNumber := int32(1)
|
||||||
|
const pageSize = int32(100)
|
||||||
|
for {
|
||||||
|
groupListResp, err := d.groupClient.GetJoinedGroupList(c, &group.GetJoinedGroupListReq{
|
||||||
|
FromUserID: req.UserID,
|
||||||
|
Pagination: &sdkws.RequestPagination{PageNumber: pageNumber, ShowNumber: pageSize},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.ZWarn(c, "DeleteUser: GetJoinedGroupList failed", err, "userID", req.UserID, "page", pageNumber)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for _, g := range groupListResp.Groups {
|
||||||
|
if _, err := d.groupClient.QuitGroup(c, &group.QuitGroupReq{
|
||||||
|
GroupID: g.GroupID,
|
||||||
|
UserID: req.UserID,
|
||||||
|
}); err != nil {
|
||||||
|
log.ZWarn(c, "DeleteUser: QuitGroup failed", err, "userID", req.UserID, "groupID", g.GroupID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if int32(len(groupListResp.Groups)) < pageSize {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pageNumber++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Hard-delete user document from MongoDB.
|
||||||
|
// Redis cache will become stale and expire via TTL; the user can no longer
|
||||||
|
// authenticate because their tokens were already invalidated in step 2.
|
||||||
|
if err := d.userDB.Delete(c, []string{req.UserID}); err != nil {
|
||||||
|
apiresp.GinError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.ZInfo(c, "DeleteUser: user deleted", "userID", req.UserID)
|
||||||
|
apiresp.GinSuccess(c, nil)
|
||||||
|
}
|
||||||
@ -137,6 +137,7 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), config.Share.IMAdminUserID)
|
m := NewMessageApi(msg.NewMsgClient(msgConn), rpcli.NewUserClient(userConn), config.Share.IMAdminUserID)
|
||||||
cp := NewCaptchaApi(pbcaptcha.NewCaptchaClient(captchaConn))
|
cp := NewCaptchaApi(pbcaptcha.NewCaptchaClient(captchaConn))
|
||||||
bl := NewUserGlobalBlackApi(blacklistCtrl, userDB, config.Share.IMAdminUserID, rpcli.NewAuthClient(authConn))
|
bl := NewUserGlobalBlackApi(blacklistCtrl, userDB, config.Share.IMAdminUserID, rpcli.NewAuthClient(authConn))
|
||||||
|
du := NewDeleteUserApi(userDB, rpcli.NewAuthClient(authConn), group.NewGroupClient(groupConn), relation.NewFriendClient(friendConn), config.Share.IMAdminUserID)
|
||||||
phoneSN := NewPhoneSNApi(phoneSNDB)
|
phoneSN := NewPhoneSNApi(phoneSNDB)
|
||||||
userRouterGroup := r.Group("/user")
|
userRouterGroup := r.Group("/user")
|
||||||
{
|
{
|
||||||
@ -178,6 +179,8 @@ func newGinRouter(ctx context.Context, client discovery.SvcDiscoveryRegistry, co
|
|||||||
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
userRouterGroup.POST("/add_global_blacklist", bl.AddGlobalBlacklist)
|
||||||
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
|
userRouterGroup.POST("/remove_global_blacklist", bl.RemoveGlobalBlacklist)
|
||||||
userRouterGroup.POST("/get_global_blacklist", bl.GetGlobalBlacklist)
|
userRouterGroup.POST("/get_global_blacklist", bl.GetGlobalBlacklist)
|
||||||
|
|
||||||
|
userRouterGroup.POST("/delete_user", du.DeleteUser)
|
||||||
}
|
}
|
||||||
// friend routing group
|
// friend routing group
|
||||||
{
|
{
|
||||||
|
|||||||
@ -375,6 +375,14 @@ func (u *UserMgo) CountRangeEverydayTotal(ctx context.Context, start time.Time,
|
|||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UserMgo) Delete(ctx context.Context, userIDs []string) error {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := u.coll.DeleteMany(ctx, bson.M{"user_id": bson.M{"$in": userIDs}})
|
||||||
|
return errs.Wrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UserMgo) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) {
|
func (u *UserMgo) SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error) {
|
||||||
if len(userIDName) == 0 {
|
if len(userIDName) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@ -45,6 +45,9 @@ type User interface {
|
|||||||
|
|
||||||
SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error)
|
SortQuery(ctx context.Context, userIDName map[string]string, asc bool) ([]*model.User, error)
|
||||||
|
|
||||||
|
// Delete permanently removes user documents by userID.
|
||||||
|
Delete(ctx context.Context, userIDs []string) error
|
||||||
|
|
||||||
// CRUD user command
|
// CRUD user command
|
||||||
AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error
|
AddUserCommand(ctx context.Context, userID string, Type int32, UUID string, value string, ex string) error
|
||||||
DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error
|
DeleteUserCommand(ctx context.Context, userID string, Type int32, UUID string) error
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user