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:
techknowlogick 2023-03-14 03:45:21 -04:00 committed by GitHub
parent aac07d010f
commit 03591f0f95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 206 additions and 44 deletions

View File

@ -660,10 +660,10 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
// By poster id. // By poster id.
func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) { func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
pulls := make([]*PullRequest, 0, 10) 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=?", Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
false, PullRequestFlowAGit, false, uid). false, PullRequestFlowAGit, false, uid).
Join("INNER", "issue", "issue.id=pull_request.issue_id"). Join("INNER", "issue", "issue.id=pull_request.issue_id").

View File

@ -742,13 +742,13 @@ func VerifyUserActiveCode(code string) (user *User) {
} }
// ChangeUserName changes all corresponding setting from old user name to new one. // 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 oldUserName := u.Name
if err = IsUsableUsername(newUserName); err != nil { if err = IsUsableUsername(newUserName); err != nil {
return err return err
} }
ctx, committer, err := db.TxContext(db.DefaultContext) ctx, committer, err := db.TxContext(ctx)
if err != nil { if err != nil {
return err return err
} }

View File

@ -93,3 +93,12 @@ type UserSettingsOptions struct {
HideEmail *bool `json:"hide_email"` HideEmail *bool `json:"hide_email"`
HideActivity *bool `json:"hide_activity"` 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"`
}

View File

@ -461,3 +461,61 @@ func GetAllUsers(ctx *context.APIContext) {
ctx.SetTotalCountHeader(maxResults) ctx.SetTotalCountHeader(maxResults)
ctx.JSON(http.StatusOK, &results) 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)
}

View File

@ -1257,6 +1257,7 @@ func Routes(ctx gocontext.Context) *web.Route {
m.Get("/orgs", org.ListUserOrgs) m.Get("/orgs", org.ListUserOrgs)
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg) m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo) m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
}, context_service.UserAssignmentAPI()) }, context_service.UserAssignmentAPI())
}) })
m.Group("/unadopted", func() { m.Group("/unadopted", func() {

View File

@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
// in:body // in:body
CreateKeyOption api.CreateKeyOption CreateKeyOption api.CreateKeyOption
// in:body
RenameUserOption api.RenameUserOption
// in:body // in:body
CreateLabelOption api.CreateLabelOption CreateLabelOption api.CreateLabelOption
// in:body // in:body

View File

@ -79,7 +79,7 @@ func SettingsPost(ctx *context.Context) {
ctx.Data["OrgName"] = true ctx.Data["OrgName"] = true
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form) ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
return 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 { switch {
case db.IsErrNameReserved(err): case db.IsErrNameReserved(err):
ctx.Data["OrgName"] = true ctx.Data["OrgName"] = true

View File

@ -27,9 +27,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/services/agit"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
container_service "code.gitea.io/gitea/services/packages/container"
user_service "code.gitea.io/gitea/services/user" 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")) return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
} }
// Check if user name has been changed // rename user
if user.LowerName != strings.ToLower(newName) { if err := user_service.RenameUser(ctx, user, newName); err != nil {
if err := user_model.ChangeUserName(user, newName); err != nil { switch {
switch { case user_model.IsErrUserAlreadyExist(err):
case user_model.IsErrUserAlreadyExist(err): ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
ctx.Flash.Error(ctx.Tr("form.username_been_taken")) case user_model.IsErrEmailAlreadyUsed(err):
case user_model.IsErrEmailAlreadyUsed(err): ctx.Flash.Error(ctx.Tr("form.email_been_used"))
ctx.Flash.Error(ctx.Tr("form.email_been_used")) case db.IsErrNameReserved(err):
case db.IsErrNameReserved(err): ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) case db.IsErrNamePatternNotAllowed(err):
case db.IsErrNamePatternNotAllowed(err): ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) case db.IsErrNameCharsNotAllowed(err):
case db.IsErrNameCharsNotAllowed(err): ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName)) default:
default: ctx.ServerError("ChangeUserName", err)
ctx.ServerError("ChangeUserName", err)
}
return 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 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 return nil
} }

View File

@ -226,8 +226,8 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
} }
// UserNameChanged handle user name change for agit flow pull // UserNameChanged handle user name change for agit flow pull
func UserNameChanged(user *user_model.User, newName string) error { func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID) pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
if err != nil { if err != nil {
return err return err
} }

41
services/user/rename.go Normal file
View File

@ -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
}

View File

@ -27,6 +27,22 @@ import (
"code.gitea.io/gitea/services/packages" "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, // DeleteUser completely and permanently deletes everything of a user,
// but issues/comments/pulls will be kept and shown as someone has been deleted, // 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. // unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.

View File

@ -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": { "/admin/users/{username}/repos": {
"post": { "post": {
"consumes": [ "consumes": [
@ -19105,6 +19145,22 @@
}, },
"x-go-package": "code.gitea.io/gitea/modules/structs" "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": { "RepoCollaboratorPermission": {
"description": "RepoCollaboratorPermission to get repository permission for a collaborator", "description": "RepoCollaboratorPermission to get repository permission for a collaborator",
"type": "object", "type": "object",