-
-
Notifications
You must be signed in to change notification settings - Fork 142
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(auth): user accounts merge tool (#854)
- Loading branch information
Showing
4 changed files
with
241 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
package handler | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/ArtalkJS/Artalk/internal/core" | ||
"github.com/ArtalkJS/Artalk/internal/entity" | ||
"github.com/ArtalkJS/Artalk/internal/log" | ||
"github.com/ArtalkJS/Artalk/internal/sync" | ||
"github.com/ArtalkJS/Artalk/server/common" | ||
"github.com/gofiber/fiber/v2" | ||
"github.com/samber/lo" | ||
"gorm.io/gorm" | ||
) | ||
|
||
type RequestAuthDataMergeApply struct { | ||
UserName string `json:"user_name" validate:"required"` | ||
} | ||
|
||
type ResponseAuthDataMergeApply struct { | ||
UpdatedComment int64 `json:"update_comments_count"` | ||
UpdatedNotify int64 `json:"update_notifies_count"` | ||
UpdatedVote int64 `json:"update_votes_count"` | ||
DeletedUser int64 `json:"deleted_user_count"` | ||
UserToken string `json:"user_token"` // Empty if login user is target user no need to re-login | ||
} | ||
|
||
// @Id ApplyDataMerge | ||
// @Summary Apply data merge | ||
// @Description This function is to solve the problem of multiple users with the same email address, should be called after user login and then check, and perform data merge. | ||
// @Tags Auth | ||
// @Security ApiKeyAuth | ||
// @Param data body RequestAuthDataMergeApply true "The data" | ||
// @Success 200 {object} ResponseAuthDataMergeApply | ||
// @Failure 400 {object} Map{msg=string} | ||
// @Failure 500 {object} Map{msg=string} | ||
// @Accept json | ||
// @Produce json | ||
// @Router /auth/merge [post] | ||
func AuthMergeApply(app *core.App, router fiber.Router) { | ||
mutexMap := sync.NewKeyMutex[uint]() | ||
|
||
router.Post("/auth/merge", common.LoginGuard(app, func(c *fiber.Ctx, user entity.User) error { | ||
// Mutex for each user to avoid concurrent merge operation | ||
mutexMap.Lock(user.ID) | ||
defer mutexMap.Unlock(user.ID) | ||
|
||
if user.Email == "" { | ||
return common.RespError(c, 500, "User email is empty") | ||
} | ||
|
||
var p RequestAuthDataMergeApply | ||
if isOK, resp := common.ParamsDecode(c, &p); !isOK { | ||
return resp | ||
} | ||
|
||
// Get all users with same email | ||
sameEmailUsers := app.Dao().FindUsersByEmail(user.Email) | ||
if len(sameEmailUsers) == 0 { | ||
return common.RespError(c, 500, "No user with same email") | ||
} | ||
|
||
targetUser, err := app.Dao().FindCreateUser(p.UserName, user.Email, user.Link) | ||
if err != nil { | ||
return common.RespError(c, 500, "Failed to create user") | ||
} | ||
|
||
// Check target if admin and recover | ||
isAdmin := false | ||
for _, u := range sameEmailUsers { | ||
if u.IsAdmin { | ||
isAdmin = true | ||
break | ||
} | ||
} | ||
if targetUser.IsAdmin != isAdmin { | ||
targetUser.IsAdmin = isAdmin | ||
app.Dao().UpdateUser(&targetUser) | ||
} | ||
|
||
resp := ResponseAuthDataMergeApply{} | ||
otherUsers := lo.Filter(sameEmailUsers, func(u entity.User, _ int) bool { | ||
return u.ID != targetUser.ID | ||
}) | ||
|
||
// Functions for log | ||
getMergeLogSummary := func() string { | ||
getUserInfo := func(u entity.User) string { | ||
return fmt.Sprintf("[%d, %s, %s]", u.ID, strconv.Quote(u.Name), strconv.Quote(u.Email)) | ||
} | ||
getUsersInfo := func(otherUsers []entity.User) string { | ||
return strings.Join(lo.Map(otherUsers, func(u entity.User, _ int) string { return getUserInfo(u) }), ", ") | ||
} | ||
return " | " + getUsersInfo(otherUsers) + " -> " + getUserInfo(targetUser) | ||
} | ||
|
||
// Begin a transaction to Merge all user data to target user | ||
if err := app.Dao().DB().Transaction(func(tx *gorm.DB) error { | ||
// Merge all user data to target user | ||
for _, u := range otherUsers { | ||
// comments | ||
if tx := app.Dao().DB().Model(&entity.Comment{}). | ||
Where("user_id = ?", u.ID).Update("user_id", targetUser.ID); tx.Error != nil { | ||
return tx.Error // if error the whole transaction will be rollback | ||
} else { | ||
resp.UpdatedComment += tx.RowsAffected | ||
} | ||
|
||
// notifies | ||
if tx := app.Dao().DB().Model(&entity.Notify{}). | ||
Where("user_id = ?", u.ID).Update("user_id", targetUser.ID); tx.Error != nil { | ||
return tx.Error | ||
} else { | ||
resp.UpdatedNotify += tx.RowsAffected | ||
} | ||
|
||
// votes | ||
if tx := app.Dao().DB().Model(&entity.Vote{}). | ||
Where("user_id = ?", u.ID).Update("user_id", targetUser.ID); tx.Error != nil { | ||
return tx.Error | ||
} else { | ||
resp.UpdatedVote += tx.RowsAffected | ||
} | ||
} | ||
|
||
return nil | ||
}); err != nil { | ||
log.Error("Failed to merge user data: ", err.Error(), getMergeLogSummary()) | ||
return common.RespError(c, 500, "Failed to merge data") | ||
} | ||
|
||
// Delete other users except target user | ||
for _, u := range otherUsers { | ||
if err := app.Dao().DelUser(&u); err != nil { | ||
log.Error("Failed to delete other user [id=", u.ID, "]: ", err.Error(), getMergeLogSummary()) | ||
} else { | ||
resp.DeletedUser++ | ||
} | ||
} | ||
|
||
// Re-login | ||
jwtToken, err := common.LoginGetUserToken(targetUser, app.Conf().AppKey, app.Conf().LoginTimeout) | ||
if err != nil { | ||
return common.RespError(c, 500, "Failed to re-login") | ||
} | ||
resp.UserToken = jwtToken | ||
|
||
// Log | ||
log.Info("User data merged successfully", getMergeLogSummary(), " | ", | ||
"Updated Comments: ", resp.UpdatedComment, " | ", | ||
"Updated Notifies: ", resp.UpdatedNotify, " | ", | ||
"Updated Votes: ", resp.UpdatedVote, " | ", | ||
"Deleted Users: ", resp.DeletedUser) | ||
|
||
return common.RespData(c, resp) | ||
})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package handler | ||
|
||
import ( | ||
"slices" | ||
|
||
"github.com/ArtalkJS/Artalk/internal/core" | ||
"github.com/ArtalkJS/Artalk/internal/entity" | ||
"github.com/ArtalkJS/Artalk/server/common" | ||
"github.com/gofiber/fiber/v2" | ||
) | ||
|
||
type ResponseAuthDataMergeCheck struct { | ||
NeedMerge bool `json:"need_merge"` | ||
UserNames []string `json:"user_names"` | ||
} | ||
|
||
// @Id CheckDataMerge | ||
// @Summary Check data merge | ||
// @Description Get all users with same email, if there are more than one user with same email, need merge | ||
// @Tags Auth | ||
// @Security ApiKeyAuth | ||
// @Success 200 {object} ResponseAuthDataMergeCheck | ||
// @Failure 400 {object} Map{msg=string} | ||
// @Failure 500 {object} Map{msg=string} | ||
// @Produce json | ||
// @Router /auth/merge [get] | ||
func AuthMergeCheck(app *core.App, router fiber.Router) { | ||
router.Get("/auth/merge", common.LoginGuard(app, func(c *fiber.Ctx, user entity.User) error { | ||
if user.Email == "" { | ||
return common.RespError(c, 500, "User email is empty") | ||
} | ||
|
||
var ( | ||
needMerge = false | ||
userNames = []string{} | ||
) | ||
|
||
// Get all users with same email | ||
sameEmailUsers := app.Dao().FindUsersByEmail(user.Email) | ||
|
||
// If there are more than one user with same email, need merge | ||
if len(sameEmailUsers) > 1 { | ||
needMerge = true | ||
|
||
// Get unique user names for user to choose | ||
for _, u := range sameEmailUsers { | ||
if !slices.Contains(userNames, u.Name) { | ||
userNames = append(userNames, u.Name) | ||
} | ||
} | ||
} | ||
|
||
return common.RespData(c, ResponseAuthDataMergeCheck{ | ||
NeedMerge: needMerge, | ||
UserNames: userNames, | ||
}) | ||
})) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters