mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-01 05:36:19 +01:00
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
12 changed files with 206 additions and 44 deletions
|
@ -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").
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"`
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
41
services/user/rename.go
Normal 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
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue