package controller import ( "errors" "net/http" "strconv" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" user_migration "github.com/QuantumNous/new-api/service/user_migration" "github.com/gin-gonic/gin" ) func ListMigrationUsers(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "0")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) keyword := c.Query("keyword") if pageSize <= 0 || pageSize > 200 { pageSize = 50 } users, total, err := model.QueryLocalUsersForMigration(page, pageSize, keyword) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "database error"}) return } snapshots := make([]user_migration.RemoteUserSnapshot, 0, len(users)) for _, user := range users { bindings, _ := model.GetUserOAuthBindingsByUserId(user.Id) snapshots = append(snapshots, buildRemoteUserSnapshot(user, bindings)) } c.JSON(http.StatusOK, gin.H{"success": true, "data": snapshots, "total": total}) } func QueryMigrationUsers(c *gin.Context) { var req user_migration.QueryRemoteUsersRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid request body"}) return } if req.SelectionMode != user_migration.SelectionModeExplicitIDs { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid selection_mode"}) return } if req.PageSize <= 0 || req.PageSize > 200 { req.PageSize = 50 } indexed := make(map[int]struct{}, len(req.SourceUserIDs)) orderedIDs := make([]int, 0, len(req.SourceUserIDs)) for _, id := range req.SourceUserIDs { if id <= 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid source_user_ids"}) return } if _, ok := indexed[id]; ok { continue } indexed[id] = struct{}{} orderedIDs = append(orderedIDs, id) } summary := &user_migration.SelectionSummary{ Requested: len(orderedIDs), Excluded: make([]user_migration.SelectionExcluded, 0), } snapshots := make([]user_migration.RemoteUserSnapshot, 0, len(orderedIDs)) for _, userID := range orderedIDs { user, err := model.GetUserById(userID, true) if err != nil || user == nil { summary.Excluded = append(summary.Excluded, user_migration.SelectionExcluded{UserID: userID, Reason: "not_found"}) continue } if user.Role == common.RoleRootUser { summary.Excluded = append(summary.Excluded, user_migration.SelectionExcluded{UserID: userID, Reason: "root_user_not_migratable"}) continue } if user.Source == common.UserSourceSynced { summary.Excluded = append(summary.Excluded, user_migration.SelectionExcluded{UserID: userID, Reason: "already_synced"}) continue } bindings, _ := model.GetUserOAuthBindingsByUserId(userID) snapshots = append(snapshots, buildRemoteUserSnapshot(user, bindings)) } summary.Matched = len(snapshots) start := req.Page * req.PageSize if start > len(snapshots) { start = len(snapshots) } end := start + req.PageSize if end > len(snapshots) { end = len(snapshots) } c.JSON(http.StatusOK, gin.H{ "success": true, "data": snapshots[start:end], "total": len(snapshots), "selection_summary": summary, }) } func GetMigrationUser(c *gin.Context) { userId, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid user id"}) return } user, err := model.GetUserById(userId, true) if err != nil || user == nil { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "user not found"}) return } if user.Role == common.RoleRootUser { c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "user not found"}) return } bindings, _ := model.GetUserOAuthBindingsByUserId(userId) c.JSON(http.StatusOK, gin.H{"success": true, "data": buildRemoteUserSnapshot(user, bindings)}) } func ConvertMigrationUserToSynced(c *gin.Context) { userId, err := strconv.Atoi(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid user id"}) return } var req user_migration.ConvertRemoteUserRequest if err := c.ShouldBindJSON(&req); err != nil || req.RemoteUserId == 0 { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid request body"}) return } if err := model.ConvertUserToSynced(userId, req.RemoteUserId, req.SyncedQuota); err != nil { if errors.Is(err, model.ErrConvertRootUserToSynced) || errors.Is(err, model.ErrSyncedUserRemoteUserIDImmutable) { c.JSON(http.StatusConflict, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true}) } func CheckSyncedCopy(c *gin.Context) { cnUserId, err := strconv.Atoi(c.Query("cn_user_id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "invalid cn_user_id"}) return } hasCopy, err := model.HasOVSyncedCopyByRemoteId(cnUserId) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"success": true, "has_copy": hasCopy}) } func buildRemoteUserSnapshot(user *model.User, bindings []*model.UserOAuthBinding) user_migration.RemoteUserSnapshot { snapshot := user_migration.RemoteUserSnapshot{ Id: user.Id, Username: user.Username, Password: user.Password, Email: user.Email, DisplayName: user.DisplayName, Status: user.Status, Role: user.Role, Group: user.Group, Quota: user.Quota, AffCode: user.AffCode, CreatedAt: user.CreatedAt, Setting: user.Setting, GitHubId: user.GitHubId, DiscordId: user.DiscordId, OidcId: user.OidcId, WeChatId: user.WeChatId, TelegramId: user.TelegramId, LinuxDOId: user.LinuxDOId, Source: user.Source, RemoteUserId: user.RemoteUserId, SyncedQuota: user.SyncedQuota, } for _, binding := range bindings { provider, err := model.GetCustomOAuthProviderById(binding.ProviderId) if err != nil { continue } snapshot.OAuthBindings = append(snapshot.OAuthBindings, user_migration.RemoteOAuthBinding{ ProviderSlug: provider.Slug, ProviderUserId: binding.ProviderUserId, }) } return snapshot }