commit
d0d897b9b8
@ -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)
|
||||
}
|
||||
Loading…
Reference in new issue