add user rename endpoint to admin api (#22789)
this is a simple endpoint that adds the ability to rename users to the
admin API.
Note: this is not in a mergeable state. It would be better if this was
handled by a PATCH/POST to the /api/v1/admin/users/{username} endpoint
and the username is modified.
---------
Co-authored-by: Jason Song <i@wolfogre.com>
			
			
This commit is contained in:
		
							parent
							
								
									aac07d010f
								
							
						
					
					
						commit
						03591f0f95
					
				| 
						 | 
				
			
			@ -660,10 +660,10 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
 | 
			
		|||
 | 
			
		||||
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
 | 
			
		||||
// By poster id.
 | 
			
		||||
func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) {
 | 
			
		||||
func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
 | 
			
		||||
	pulls := make([]*PullRequest, 0, 10)
 | 
			
		||||
 | 
			
		||||
	err := db.GetEngine(db.DefaultContext).
 | 
			
		||||
	err := db.GetEngine(ctx).
 | 
			
		||||
		Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
 | 
			
		||||
			false, PullRequestFlowAGit, false, uid).
 | 
			
		||||
		Join("INNER", "issue", "issue.id=pull_request.issue_id").
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -742,13 +742,13 @@ func VerifyUserActiveCode(code string) (user *User) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// ChangeUserName changes all corresponding setting from old user name to new one.
 | 
			
		||||
func ChangeUserName(u *User, newUserName string) (err error) {
 | 
			
		||||
func ChangeUserName(ctx context.Context, u *User, newUserName string) (err error) {
 | 
			
		||||
	oldUserName := u.Name
 | 
			
		||||
	if err = IsUsableUsername(newUserName); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, committer, err := db.TxContext(db.DefaultContext)
 | 
			
		||||
	ctx, committer, err := db.TxContext(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,3 +93,12 @@ type UserSettingsOptions struct {
 | 
			
		|||
	HideEmail    *bool `json:"hide_email"`
 | 
			
		||||
	HideActivity *bool `json:"hide_activity"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenameUserOption options when renaming a user
 | 
			
		||||
type RenameUserOption struct {
 | 
			
		||||
	// New username for this user. This name cannot be in use yet by any other user.
 | 
			
		||||
	//
 | 
			
		||||
	// required: true
 | 
			
		||||
	// unique: true
 | 
			
		||||
	NewName string `json:"new_username" binding:"Required"`
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -461,3 +461,61 @@ func GetAllUsers(ctx *context.APIContext) {
 | 
			
		|||
	ctx.SetTotalCountHeader(maxResults)
 | 
			
		||||
	ctx.JSON(http.StatusOK, &results)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// RenameUser api for renaming a user
 | 
			
		||||
func RenameUser(ctx *context.APIContext) {
 | 
			
		||||
	// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Rename a user
 | 
			
		||||
	// produces:
 | 
			
		||||
	// - application/json
 | 
			
		||||
	// parameters:
 | 
			
		||||
	// - name: username
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: existing username of user
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: body
 | 
			
		||||
	//   in: body
 | 
			
		||||
	//   required: true
 | 
			
		||||
	//   schema:
 | 
			
		||||
	//     "$ref": "#/definitions/RenameUserOption"
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "204":
 | 
			
		||||
	//     "$ref": "#/responses/empty"
 | 
			
		||||
	//   "403":
 | 
			
		||||
	//     "$ref": "#/responses/forbidden"
 | 
			
		||||
	//   "422":
 | 
			
		||||
	//     "$ref": "#/responses/validationError"
 | 
			
		||||
 | 
			
		||||
	if ctx.ContextUser.IsOrganization() {
 | 
			
		||||
		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
 | 
			
		||||
 | 
			
		||||
	if strings.EqualFold(newName, ctx.ContextUser.Name) {
 | 
			
		||||
		// Noop as username is not changed
 | 
			
		||||
		ctx.Status(http.StatusNoContent)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if user name has been changed
 | 
			
		||||
	if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case user_model.IsErrUserAlreadyExist(err):
 | 
			
		||||
			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
 | 
			
		||||
		case db.IsErrNameReserved(err):
 | 
			
		||||
			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
 | 
			
		||||
		case db.IsErrNamePatternNotAllowed(err):
 | 
			
		||||
			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
 | 
			
		||||
		case db.IsErrNameCharsNotAllowed(err):
 | 
			
		||||
			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
 | 
			
		||||
		default:
 | 
			
		||||
			ctx.ServerError("ChangeUserName", err)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	ctx.Status(http.StatusNoContent)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1257,6 +1257,7 @@ func Routes(ctx gocontext.Context) *web.Route {
 | 
			
		|||
					m.Get("/orgs", org.ListUserOrgs)
 | 
			
		||||
					m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
 | 
			
		||||
					m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
 | 
			
		||||
					m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
 | 
			
		||||
				}, context_service.UserAssignmentAPI())
 | 
			
		||||
			})
 | 
			
		||||
			m.Group("/unadopted", func() {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
 | 
			
		|||
	// in:body
 | 
			
		||||
	CreateKeyOption api.CreateKeyOption
 | 
			
		||||
 | 
			
		||||
	// in:body
 | 
			
		||||
	RenameUserOption api.RenameUserOption
 | 
			
		||||
 | 
			
		||||
	// in:body
 | 
			
		||||
	CreateLabelOption api.CreateLabelOption
 | 
			
		||||
	// in:body
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,7 +79,7 @@ func SettingsPost(ctx *context.Context) {
 | 
			
		|||
			ctx.Data["OrgName"] = true
 | 
			
		||||
			ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
 | 
			
		||||
			return
 | 
			
		||||
		} else if err = user_model.ChangeUserName(org.AsUser(), form.Name); err != nil {
 | 
			
		||||
		} else if err = user_model.ChangeUserName(ctx, org.AsUser(), form.Name); err != nil {
 | 
			
		||||
			switch {
 | 
			
		||||
			case db.IsErrNameReserved(err):
 | 
			
		||||
				ctx.Data["OrgName"] = true
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,9 +27,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web"
 | 
			
		||||
	"code.gitea.io/gitea/modules/web/middleware"
 | 
			
		||||
	"code.gitea.io/gitea/services/agit"
 | 
			
		||||
	"code.gitea.io/gitea/services/forms"
 | 
			
		||||
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
			
		||||
	user_service "code.gitea.io/gitea/services/user"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -57,45 +55,25 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
 | 
			
		|||
		return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if user name has been changed
 | 
			
		||||
	if user.LowerName != strings.ToLower(newName) {
 | 
			
		||||
		if err := user_model.ChangeUserName(user, newName); err != nil {
 | 
			
		||||
			switch {
 | 
			
		||||
			case user_model.IsErrUserAlreadyExist(err):
 | 
			
		||||
				ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
 | 
			
		||||
			case user_model.IsErrEmailAlreadyUsed(err):
 | 
			
		||||
				ctx.Flash.Error(ctx.Tr("form.email_been_used"))
 | 
			
		||||
			case db.IsErrNameReserved(err):
 | 
			
		||||
				ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
 | 
			
		||||
			case db.IsErrNamePatternNotAllowed(err):
 | 
			
		||||
				ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
 | 
			
		||||
			case db.IsErrNameCharsNotAllowed(err):
 | 
			
		||||
				ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
 | 
			
		||||
			default:
 | 
			
		||||
				ctx.ServerError("ChangeUserName", err)
 | 
			
		||||
			}
 | 
			
		||||
			return err
 | 
			
		||||
	// rename user
 | 
			
		||||
	if err := user_service.RenameUser(ctx, user, newName); err != nil {
 | 
			
		||||
		switch {
 | 
			
		||||
		case user_model.IsErrUserAlreadyExist(err):
 | 
			
		||||
			ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
 | 
			
		||||
		case user_model.IsErrEmailAlreadyUsed(err):
 | 
			
		||||
			ctx.Flash.Error(ctx.Tr("form.email_been_used"))
 | 
			
		||||
		case db.IsErrNameReserved(err):
 | 
			
		||||
			ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
 | 
			
		||||
		case db.IsErrNamePatternNotAllowed(err):
 | 
			
		||||
			ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
 | 
			
		||||
		case db.IsErrNameCharsNotAllowed(err):
 | 
			
		||||
			ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
 | 
			
		||||
		default:
 | 
			
		||||
			ctx.ServerError("ChangeUserName", err)
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		if err := repo_model.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
 | 
			
		||||
			ctx.ServerError("UpdateRepository", err)
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// update all agit flow pull request header
 | 
			
		||||
	err := agit.UserNameChanged(user, newName)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("agit.UserNameChanged", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
 | 
			
		||||
		ctx.ServerError("UpdateRepositoryNames", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("User name changed: %s -> %s", user.Name, newName)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -226,8 +226,8 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// UserNameChanged handle user name change for agit flow pull
 | 
			
		||||
func UserNameChanged(user *user_model.User, newName string) error {
 | 
			
		||||
	pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID)
 | 
			
		||||
func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
 | 
			
		||||
	pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,41 @@
 | 
			
		|||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
			
		||||
// SPDX-License-Identifier: MIT
 | 
			
		||||
 | 
			
		||||
package user
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	user_model "code.gitea.io/gitea/models/user"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/services/agit"
 | 
			
		||||
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func renameUser(ctx context.Context, u *user_model.User, newUserName string) error {
 | 
			
		||||
	if u.IsOrganization() {
 | 
			
		||||
		return fmt.Errorf("cannot rename organization")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := user_model.ChangeUserName(ctx, u, newUserName); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := agit.UserNameChanged(ctx, u, newUserName); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := container_service.UpdateRepositoryNames(ctx, u, newUserName); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	u.Name = newUserName
 | 
			
		||||
	u.LowerName = strings.ToLower(newUserName)
 | 
			
		||||
	if err := user_model.UpdateUser(ctx, u, false); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Trace("User name changed: %s -> %s", u.Name, newUserName)
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -27,6 +27,22 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/services/packages"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// RenameUser renames a user
 | 
			
		||||
func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
 | 
			
		||||
	ctx, committer, err := db.TxContext(ctx)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer committer.Close()
 | 
			
		||||
	if err := renameUser(ctx, u, newUserName); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	if err := committer.Commit(); err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeleteUser completely and permanently deletes everything of a user,
 | 
			
		||||
// but issues/comments/pulls will be kept and shown as someone has been deleted,
 | 
			
		||||
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -679,6 +679,46 @@
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/admin/users/{username}/rename": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "application/json"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "admin"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Rename a user",
 | 
			
		||||
        "operationId": "adminRenameUser",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "existing username of user",
 | 
			
		||||
            "name": "username",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "name": "body",
 | 
			
		||||
            "in": "body",
 | 
			
		||||
            "required": true,
 | 
			
		||||
            "schema": {
 | 
			
		||||
              "$ref": "#/definitions/RenameUserOption"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "204": {
 | 
			
		||||
            "$ref": "#/responses/empty"
 | 
			
		||||
          },
 | 
			
		||||
          "403": {
 | 
			
		||||
            "$ref": "#/responses/forbidden"
 | 
			
		||||
          },
 | 
			
		||||
          "422": {
 | 
			
		||||
            "$ref": "#/responses/validationError"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/admin/users/{username}/repos": {
 | 
			
		||||
      "post": {
 | 
			
		||||
        "consumes": [
 | 
			
		||||
| 
						 | 
				
			
			@ -19105,6 +19145,22 @@
 | 
			
		|||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "RenameUserOption": {
 | 
			
		||||
      "description": "RenameUserOption options when renaming a user",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
      "required": [
 | 
			
		||||
        "new_username"
 | 
			
		||||
      ],
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "new_username": {
 | 
			
		||||
          "description": "New username for this user. This name cannot be in use yet by any other user.",
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "uniqueItems": true,
 | 
			
		||||
          "x-go-name": "NewName"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "x-go-package": "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
    },
 | 
			
		||||
    "RepoCollaboratorPermission": {
 | 
			
		||||
      "description": "RepoCollaboratorPermission to get repository permission for a collaborator",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue