diff --git a/models/issues/pull.go b/models/issues/pull.go index 6a1dc3155..a15ebec0b 100644 --- a/models/issues/pull.go +++ b/models/issues/pull.go @@ -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"). diff --git a/models/user/user.go b/models/user/user.go index 454779b9e..82c2d3b6c 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -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 } diff --git a/modules/structs/user.go b/modules/structs/user.go index c5e96f335..f68b92ac0 100644 --- a/modules/structs/user.go +++ b/modules/structs/user.go @@ -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"` +} diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index 4192d8654..369d13943 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -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) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 735939a55..7001dc72a 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -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() { diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 979b18407..0c8d3d353 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -48,6 +48,9 @@ type swaggerParameterBodies struct { // in:body CreateKeyOption api.CreateKeyOption + // in:body + RenameUserOption api.RenameUserOption + // in:body CreateLabelOption api.CreateLabelOption // in:body diff --git a/routers/web/org/setting.go b/routers/web/org/setting.go index b57ebfbcd..654e9000f 100644 --- a/routers/web/org/setting.go +++ b/routers/web/org/setting.go @@ -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 diff --git a/routers/web/user/setting/profile.go b/routers/web/user/setting/profile.go index f0f053a51..f500be763 100644 --- a/routers/web/user/setting/profile.go +++ b/routers/web/user/setting/profile.go @@ -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 } diff --git a/services/agit/agit.go b/services/agit/agit.go index b61cb6f3f..32fc3cba4 100644 --- a/services/agit/agit.go +++ b/services/agit/agit.go @@ -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 } diff --git a/services/user/rename.go b/services/user/rename.go new file mode 100644 index 000000000..af195d7d7 --- /dev/null +++ b/services/user/rename.go @@ -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 +} diff --git a/services/user/user.go b/services/user/user.go index f0b8fe1c3..d52a2f404 100644 --- a/services/user/user.go +++ b/services/user/user.go @@ -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. diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c304a7a49..7dc7f563c 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -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",