Skip to content

Commit 03591f0

Browse files
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 <[email protected]>
1 parent aac07d0 commit 03591f0

File tree

12 files changed

+206
-44
lines changed

12 files changed

+206
-44
lines changed

models/issues/pull.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -660,10 +660,10 @@ func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest,
660660

661661
// GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request
662662
// By poster id.
663-
func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) {
663+
func GetAllUnmergedAgitPullRequestByPoster(ctx context.Context, uid int64) ([]*PullRequest, error) {
664664
pulls := make([]*PullRequest, 0, 10)
665665

666-
err := db.GetEngine(db.DefaultContext).
666+
err := db.GetEngine(ctx).
667667
Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?",
668668
false, PullRequestFlowAGit, false, uid).
669669
Join("INNER", "issue", "issue.id=pull_request.issue_id").

models/user/user.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,13 +742,13 @@ func VerifyUserActiveCode(code string) (user *User) {
742742
}
743743

744744
// ChangeUserName changes all corresponding setting from old user name to new one.
745-
func ChangeUserName(u *User, newUserName string) (err error) {
745+
func ChangeUserName(ctx context.Context, u *User, newUserName string) (err error) {
746746
oldUserName := u.Name
747747
if err = IsUsableUsername(newUserName); err != nil {
748748
return err
749749
}
750750

751-
ctx, committer, err := db.TxContext(db.DefaultContext)
751+
ctx, committer, err := db.TxContext(ctx)
752752
if err != nil {
753753
return err
754754
}

modules/structs/user.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,12 @@ type UserSettingsOptions struct {
9393
HideEmail *bool `json:"hide_email"`
9494
HideActivity *bool `json:"hide_activity"`
9595
}
96+
97+
// RenameUserOption options when renaming a user
98+
type RenameUserOption struct {
99+
// New username for this user. This name cannot be in use yet by any other user.
100+
//
101+
// required: true
102+
// unique: true
103+
NewName string `json:"new_username" binding:"Required"`
104+
}

routers/api/v1/admin/user.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,61 @@ func GetAllUsers(ctx *context.APIContext) {
461461
ctx.SetTotalCountHeader(maxResults)
462462
ctx.JSON(http.StatusOK, &results)
463463
}
464+
465+
// RenameUser api for renaming a user
466+
func RenameUser(ctx *context.APIContext) {
467+
// swagger:operation POST /admin/users/{username}/rename admin adminRenameUser
468+
// ---
469+
// summary: Rename a user
470+
// produces:
471+
// - application/json
472+
// parameters:
473+
// - name: username
474+
// in: path
475+
// description: existing username of user
476+
// type: string
477+
// required: true
478+
// - name: body
479+
// in: body
480+
// required: true
481+
// schema:
482+
// "$ref": "#/definitions/RenameUserOption"
483+
// responses:
484+
// "204":
485+
// "$ref": "#/responses/empty"
486+
// "403":
487+
// "$ref": "#/responses/forbidden"
488+
// "422":
489+
// "$ref": "#/responses/validationError"
490+
491+
if ctx.ContextUser.IsOrganization() {
492+
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("%s is an organization not a user", ctx.ContextUser.Name))
493+
return
494+
}
495+
496+
newName := web.GetForm(ctx).(*api.RenameUserOption).NewName
497+
498+
if strings.EqualFold(newName, ctx.ContextUser.Name) {
499+
// Noop as username is not changed
500+
ctx.Status(http.StatusNoContent)
501+
return
502+
}
503+
504+
// Check if user name has been changed
505+
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
506+
switch {
507+
case user_model.IsErrUserAlreadyExist(err):
508+
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
509+
case db.IsErrNameReserved(err):
510+
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_reserved", newName))
511+
case db.IsErrNamePatternNotAllowed(err):
512+
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_pattern_not_allowed", newName))
513+
case db.IsErrNameCharsNotAllowed(err):
514+
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("user.form.name_chars_not_allowed", newName))
515+
default:
516+
ctx.ServerError("ChangeUserName", err)
517+
}
518+
return
519+
}
520+
ctx.Status(http.StatusNoContent)
521+
}

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,7 @@ func Routes(ctx gocontext.Context) *web.Route {
12571257
m.Get("/orgs", org.ListUserOrgs)
12581258
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
12591259
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
1260+
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
12601261
}, context_service.UserAssignmentAPI())
12611262
})
12621263
m.Group("/unadopted", func() {

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ type swaggerParameterBodies struct {
4848
// in:body
4949
CreateKeyOption api.CreateKeyOption
5050

51+
// in:body
52+
RenameUserOption api.RenameUserOption
53+
5154
// in:body
5255
CreateLabelOption api.CreateLabelOption
5356
// in:body

routers/web/org/setting.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func SettingsPost(ctx *context.Context) {
7979
ctx.Data["OrgName"] = true
8080
ctx.RenderWithErr(ctx.Tr("form.username_been_taken"), tplSettingsOptions, &form)
8181
return
82-
} else if err = user_model.ChangeUserName(org.AsUser(), form.Name); err != nil {
82+
} else if err = user_model.ChangeUserName(ctx, org.AsUser(), form.Name); err != nil {
8383
switch {
8484
case db.IsErrNameReserved(err):
8585
ctx.Data["OrgName"] = true

routers/web/user/setting/profile.go

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,7 @@ import (
2727
"code.gitea.io/gitea/modules/util"
2828
"code.gitea.io/gitea/modules/web"
2929
"code.gitea.io/gitea/modules/web/middleware"
30-
"code.gitea.io/gitea/services/agit"
3130
"code.gitea.io/gitea/services/forms"
32-
container_service "code.gitea.io/gitea/services/packages/container"
3331
user_service "code.gitea.io/gitea/services/user"
3432
)
3533

@@ -57,45 +55,25 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s
5755
return fmt.Errorf(ctx.Tr("form.username_change_not_local_user"))
5856
}
5957

60-
// Check if user name has been changed
61-
if user.LowerName != strings.ToLower(newName) {
62-
if err := user_model.ChangeUserName(user, newName); err != nil {
63-
switch {
64-
case user_model.IsErrUserAlreadyExist(err):
65-
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
66-
case user_model.IsErrEmailAlreadyUsed(err):
67-
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
68-
case db.IsErrNameReserved(err):
69-
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
70-
case db.IsErrNamePatternNotAllowed(err):
71-
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
72-
case db.IsErrNameCharsNotAllowed(err):
73-
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
74-
default:
75-
ctx.ServerError("ChangeUserName", err)
76-
}
77-
return err
78-
}
79-
} else {
80-
if err := repo_model.UpdateRepositoryOwnerNames(user.ID, newName); err != nil {
81-
ctx.ServerError("UpdateRepository", err)
82-
return err
58+
// rename user
59+
if err := user_service.RenameUser(ctx, user, newName); err != nil {
60+
switch {
61+
case user_model.IsErrUserAlreadyExist(err):
62+
ctx.Flash.Error(ctx.Tr("form.username_been_taken"))
63+
case user_model.IsErrEmailAlreadyUsed(err):
64+
ctx.Flash.Error(ctx.Tr("form.email_been_used"))
65+
case db.IsErrNameReserved(err):
66+
ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName))
67+
case db.IsErrNamePatternNotAllowed(err):
68+
ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName))
69+
case db.IsErrNameCharsNotAllowed(err):
70+
ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName))
71+
default:
72+
ctx.ServerError("ChangeUserName", err)
8373
}
84-
}
85-
86-
// update all agit flow pull request header
87-
err := agit.UserNameChanged(user, newName)
88-
if err != nil {
89-
ctx.ServerError("agit.UserNameChanged", err)
90-
return err
91-
}
92-
93-
if err := container_service.UpdateRepositoryNames(ctx, user, newName); err != nil {
94-
ctx.ServerError("UpdateRepositoryNames", err)
9574
return err
9675
}
9776

98-
log.Trace("User name changed: %s -> %s", user.Name, newName)
9977
return nil
10078
}
10179

services/agit/agit.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
226226
}
227227

228228
// UserNameChanged handle user name change for agit flow pull
229-
func UserNameChanged(user *user_model.User, newName string) error {
230-
pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(user.ID)
229+
func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
230+
pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
231231
if err != nil {
232232
return err
233233
}

services/user/rename.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2023 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package user
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"strings"
10+
11+
user_model "code.gitea.io/gitea/models/user"
12+
"code.gitea.io/gitea/modules/log"
13+
"code.gitea.io/gitea/services/agit"
14+
container_service "code.gitea.io/gitea/services/packages/container"
15+
)
16+
17+
func renameUser(ctx context.Context, u *user_model.User, newUserName string) error {
18+
if u.IsOrganization() {
19+
return fmt.Errorf("cannot rename organization")
20+
}
21+
22+
if err := user_model.ChangeUserName(ctx, u, newUserName); err != nil {
23+
return err
24+
}
25+
26+
if err := agit.UserNameChanged(ctx, u, newUserName); err != nil {
27+
return err
28+
}
29+
if err := container_service.UpdateRepositoryNames(ctx, u, newUserName); err != nil {
30+
return err
31+
}
32+
33+
u.Name = newUserName
34+
u.LowerName = strings.ToLower(newUserName)
35+
if err := user_model.UpdateUser(ctx, u, false); err != nil {
36+
return err
37+
}
38+
39+
log.Trace("User name changed: %s -> %s", u.Name, newUserName)
40+
return nil
41+
}

services/user/user.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,22 @@ import (
2727
"code.gitea.io/gitea/services/packages"
2828
)
2929

30+
// RenameUser renames a user
31+
func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error {
32+
ctx, committer, err := db.TxContext(ctx)
33+
if err != nil {
34+
return err
35+
}
36+
defer committer.Close()
37+
if err := renameUser(ctx, u, newUserName); err != nil {
38+
return err
39+
}
40+
if err := committer.Commit(); err != nil {
41+
return err
42+
}
43+
return err
44+
}
45+
3046
// DeleteUser completely and permanently deletes everything of a user,
3147
// but issues/comments/pulls will be kept and shown as someone has been deleted,
3248
// unless the user is younger than USER_DELETE_WITH_COMMENTS_MAX_DAYS.

templates/swagger/v1_json.tmpl

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -679,6 +679,46 @@
679679
}
680680
}
681681
},
682+
"/admin/users/{username}/rename": {
683+
"post": {
684+
"produces": [
685+
"application/json"
686+
],
687+
"tags": [
688+
"admin"
689+
],
690+
"summary": "Rename a user",
691+
"operationId": "adminRenameUser",
692+
"parameters": [
693+
{
694+
"type": "string",
695+
"description": "existing username of user",
696+
"name": "username",
697+
"in": "path",
698+
"required": true
699+
},
700+
{
701+
"name": "body",
702+
"in": "body",
703+
"required": true,
704+
"schema": {
705+
"$ref": "#/definitions/RenameUserOption"
706+
}
707+
}
708+
],
709+
"responses": {
710+
"204": {
711+
"$ref": "#/responses/empty"
712+
},
713+
"403": {
714+
"$ref": "#/responses/forbidden"
715+
},
716+
"422": {
717+
"$ref": "#/responses/validationError"
718+
}
719+
}
720+
}
721+
},
682722
"/admin/users/{username}/repos": {
683723
"post": {
684724
"consumes": [
@@ -19105,6 +19145,22 @@
1910519145
},
1910619146
"x-go-package": "code.gitea.io/gitea/modules/structs"
1910719147
},
19148+
"RenameUserOption": {
19149+
"description": "RenameUserOption options when renaming a user",
19150+
"type": "object",
19151+
"required": [
19152+
"new_username"
19153+
],
19154+
"properties": {
19155+
"new_username": {
19156+
"description": "New username for this user. This name cannot be in use yet by any other user.",
19157+
"type": "string",
19158+
"uniqueItems": true,
19159+
"x-go-name": "NewName"
19160+
}
19161+
},
19162+
"x-go-package": "code.gitea.io/gitea/modules/structs"
19163+
},
1910819164
"RepoCollaboratorPermission": {
1910919165
"description": "RepoCollaboratorPermission to get repository permission for a collaborator",
1911019166
"type": "object",

0 commit comments

Comments
 (0)