Skip to content

Commit

Permalink
feat(auth): user accounts merge tool (#854)
Browse files Browse the repository at this point in the history
  • Loading branch information
qwqcode committed May 3, 2024
1 parent a09628c commit 88aec7e
Show file tree
Hide file tree
Showing 4 changed files with 241 additions and 11 deletions.
159 changes: 159 additions & 0 deletions server/handler/auth_merge_apply.go
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)
}))
}
58 changes: 58 additions & 0 deletions server/handler/auth_merge_check.go
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,
})
}))
}
33 changes: 22 additions & 11 deletions server/handler/comment_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,30 @@ func CommentCreate(app *core.App, router fiber.Router) {
}

// find user
user, err := app.Dao().FindCreateUser(p.Name, p.Email, p.Link)
if err != nil || page.Key == "" {
log.Error("Cannot get user or page")
isVerified := true
user, err := common.GetUserByReq(app, c)
if errors.Is(err, common.ErrTokenNotProvided) {
// Anonymous user
isVerified = false
user, err = app.Dao().FindCreateUser(p.Name, p.Email, p.Link)
if err != nil {
log.Error("[CommentCreate] Create user error: ", err)
return common.RespError(c, 500, i18n.T("Comment failed"))
}

// Update user
user.Link = p.Link
user.LastIP = ip
user.LastUA = ua
user.Name = p.Name // for 若用户修改用户名大小写
user.Email = p.Email
app.Dao().UpdateUser(&user)
} else if err != nil {
// Login user error
log.Error("[CommentCreate] Get user error: ", err)
return common.RespError(c, 500, i18n.T("Comment failed"))
}

// update user
user.Link = p.Link
user.LastIP = ip
user.LastUA = ua
user.Name = p.Name // for 若用户修改用户名大小写
user.Email = p.Email
app.Dao().UpdateUser(&user)

comment := entity.Comment{
Content: p.Content,
PageKey: page.Key,
Expand All @@ -137,6 +147,7 @@ func CommentCreate(app *core.App, router fiber.Router) {
IsPending: false,
IsCollapsed: false,
IsPinned: false,
IsVerified: isVerified,
}

// default comment type
Expand Down
2 changes: 2 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func Serve(app *core.App) (*fiber.App, error) {
h.AuthEmailLogin(app, api)
h.AuthEmailRegister(app, api)
h.AuthEmailSend(app, api)
h.AuthMergeApply(app, api)
h.AuthMergeCheck(app, api)

h.AuthSocialLogin(app, api)

Expand Down

0 comments on commit 88aec7e

Please sign in to comment.