mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Refactor rename user and rename organization (#24052)
This PR is a refactor at the beginning. And now it did 4 things. - [x] Move renaming organizaiton and user logics into services layer and merged as one function - [x] Support rename a user capitalization only. For example, rename the user from `Lunny` to `lunny`. We just need to change one table `user` and others should not be touched. - [x] Before this PR, some renaming were missed like `agit` - [x] Fix bug the API reutrned from `http.StatusNoContent` to `http.StatusOK`
This commit is contained in:
		| @@ -832,3 +832,11 @@ func FixNullArchivedRepository(ctx context.Context) (int64, error) { | |||||||
| 		IsArchived: false, | 		IsArchived: false, | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // UpdateRepositoryOwnerName updates the owner name of all repositories owned by the user | ||||||
|  | func UpdateRepositoryOwnerName(ctx context.Context, oldUserName, newUserName string) error { | ||||||
|  | 	if _, err := db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil { | ||||||
|  | 		return fmt.Errorf("change repo owner name: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,13 +9,6 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| //  ____ ___ |  | ||||||
| // |    |   \______ ___________ |  | ||||||
| // |    |   /  ___// __ \_  __ \ |  | ||||||
| // |    |  /\___ \\  ___/|  | \/ |  | ||||||
| // |______//____  >\___  >__| |  | ||||||
| //              \/     \/ |  | ||||||
|  |  | ||||||
| // ErrUserAlreadyExist represents a "user already exists" error. | // ErrUserAlreadyExist represents a "user already exists" error. | ||||||
| type ErrUserAlreadyExist struct { | type ErrUserAlreadyExist struct { | ||||||
| 	Name string | 	Name string | ||||||
| @@ -99,3 +92,34 @@ func (err ErrUserInactive) Error() string { | |||||||
| func (err ErrUserInactive) Unwrap() error { | func (err ErrUserInactive) Unwrap() error { | ||||||
| 	return util.ErrPermissionDenied | 	return util.ErrPermissionDenied | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ErrUserIsNotLocal represents a "ErrUserIsNotLocal" kind of error. | ||||||
|  | type ErrUserIsNotLocal struct { | ||||||
|  | 	UID  int64 | ||||||
|  | 	Name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (err ErrUserIsNotLocal) Error() string { | ||||||
|  | 	return fmt.Sprintf("user is not local type [uid: %d, name: %s]", err.UID, err.Name) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsErrUserIsNotLocal | ||||||
|  | func IsErrUserIsNotLocal(err error) bool { | ||||||
|  | 	_, ok := err.(ErrUserIsNotLocal) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type ErrUsernameNotChanged struct { | ||||||
|  | 	UID  int64 | ||||||
|  | 	Name string | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (err ErrUsernameNotChanged) Error() string { | ||||||
|  | 	return fmt.Sprintf("username hasn't been changed[uid: %d, name: %s]", err.UID, err.Name) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // IsErrUsernameNotChanged | ||||||
|  | func IsErrUsernameNotChanged(err error) bool { | ||||||
|  | 	_, ok := err.(ErrUsernameNotChanged) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import ( | |||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"os" |  | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -756,50 +755,6 @@ func VerifyUserActiveCode(code string) (user *User) { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // ChangeUserName changes all corresponding setting from old user name to new one. |  | ||||||
| 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(ctx) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer committer.Close() |  | ||||||
|  |  | ||||||
| 	isExist, err := IsUserExist(ctx, 0, newUserName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} else if isExist { |  | ||||||
| 		return ErrUserAlreadyExist{newUserName} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if _, err = db.GetEngine(ctx).Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil { |  | ||||||
| 		return fmt.Errorf("Change repo owner name: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Do not fail if directory does not exist |  | ||||||
| 	if err = util.Rename(UserPath(oldUserName), UserPath(newUserName)); err != nil && !os.IsNotExist(err) { |  | ||||||
| 		return fmt.Errorf("Rename user directory: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err = NewUserRedirect(ctx, u.ID, oldUserName, newUserName); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err = committer.Commit(); err != nil { |  | ||||||
| 		if err2 := util.Rename(UserPath(newUserName), UserPath(oldUserName)); err2 != nil && !os.IsNotExist(err2) { |  | ||||||
| 			log.Critical("Unable to rollback directory change during failed username change from: %s to: %s. DB Error: %v. Filesystem Error: %v", oldUserName, newUserName, err, err2) |  | ||||||
| 			return fmt.Errorf("failed to rollback directory change during failed username change from: %s to: %s. DB Error: %w. Filesystem Error: %v", oldUserName, newUserName, err, err2) |  | ||||||
| 		} |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // checkDupEmail checks whether there are the same email with the user | // checkDupEmail checks whether there are the same email with the user | ||||||
| func checkDupEmail(ctx context.Context, u *User) error { | func checkDupEmail(ctx context.Context, u *User) error { | ||||||
| 	u.Email = strings.ToLower(u.Email) | 	u.Email = strings.ToLower(u.Email) | ||||||
|   | |||||||
| @@ -520,6 +520,7 @@ lang_select_error = Select a language from the list. | |||||||
|  |  | ||||||
| username_been_taken = The username is already taken. | username_been_taken = The username is already taken. | ||||||
| username_change_not_local_user = Non-local users are not allowed to change their username. | username_change_not_local_user = Non-local users are not allowed to change their username. | ||||||
|  | username_has_not_been_changed = Username has not been changed | ||||||
| repo_name_been_taken = The repository name is already used. | repo_name_been_taken = The repository name is already used. | ||||||
| repository_force_private = Force Private is enabled: private repositories cannot be made public. | repository_force_private = Force Private is enabled: private repositories cannot be made public. | ||||||
| repository_files_already_exist = Files already exist for this repository. Contact the system administrator. | repository_files_already_exist = Files already exist for this repository. Contact the system administrator. | ||||||
|   | |||||||
| @@ -502,17 +502,15 @@ func RenameUser(ctx *context.APIContext) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	oldName := ctx.ContextUser.Name | ||||||
| 	newName := web.GetForm(ctx).(*api.RenameUserOption).NewName | 	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 | 	// Check if user name has been changed | ||||||
| 	if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil { | 	if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil { | ||||||
| 		switch { | 		switch { | ||||||
|  | 		case user_model.IsErrUsernameNotChanged(err): | ||||||
|  | 			// Noop as username is not changed | ||||||
|  | 			ctx.Status(http.StatusNoContent) | ||||||
| 		case user_model.IsErrUserAlreadyExist(err): | 		case user_model.IsErrUserAlreadyExist(err): | ||||||
| 			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken")) | 			ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken")) | ||||||
| 		case db.IsErrNameReserved(err): | 		case db.IsErrNameReserved(err): | ||||||
| @@ -526,5 +524,7 @@ func RenameUser(ctx *context.APIContext) { | |||||||
| 		} | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	ctx.Status(http.StatusNoContent) |  | ||||||
|  | 	log.Trace("User name changed: %s -> %s", oldName, newName) | ||||||
|  | 	ctx.Status(http.StatusOK) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -22,8 +22,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	user_setting "code.gitea.io/gitea/routers/web/user/setting" | 	user_setting "code.gitea.io/gitea/routers/web/user/setting" | ||||||
| 	"code.gitea.io/gitea/services/forms" | 	"code.gitea.io/gitea/services/forms" | ||||||
| 	"code.gitea.io/gitea/services/org" | 	org_service "code.gitea.io/gitea/services/org" | ||||||
| 	container_service "code.gitea.io/gitea/services/packages/container" |  | ||||||
| 	repo_service "code.gitea.io/gitea/services/repository" | 	repo_service "code.gitea.io/gitea/services/repository" | ||||||
| 	user_service "code.gitea.io/gitea/services/user" | 	user_service "code.gitea.io/gitea/services/user" | ||||||
| ) | ) | ||||||
| @@ -67,31 +66,23 @@ func SettingsPost(ctx *context.Context) { | |||||||
| 	nameChanged := org.Name != form.Name | 	nameChanged := org.Name != form.Name | ||||||
|  |  | ||||||
| 	// Check if organization name has been changed. | 	// Check if organization name has been changed. | ||||||
| 	if org.LowerName != strings.ToLower(form.Name) { | 	if nameChanged { | ||||||
| 		isExist, err := user_model.IsUserExist(ctx, org.ID, form.Name) | 		err := org_service.RenameOrganization(ctx, org, form.Name) | ||||||
| 		if err != nil { | 		switch { | ||||||
| 			ctx.ServerError("IsUserExist", err) | 		case user_model.IsErrUserAlreadyExist(err): | ||||||
| 			return |  | ||||||
| 		} else if isExist { |  | ||||||
| 			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(ctx, org.AsUser(), form.Name); err != nil { | 		case db.IsErrNameReserved(err): | ||||||
| 			switch { | 			ctx.Data["OrgName"] = true | ||||||
| 			case db.IsErrNameReserved(err): | 			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) | ||||||
| 				ctx.Data["OrgName"] = true |  | ||||||
| 				ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplSettingsOptions, &form) |  | ||||||
| 			case db.IsErrNamePatternNotAllowed(err): |  | ||||||
| 				ctx.Data["OrgName"] = true |  | ||||||
| 				ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) |  | ||||||
| 			default: |  | ||||||
| 				ctx.ServerError("ChangeUserName", err) |  | ||||||
| 			} |  | ||||||
| 			return | 			return | ||||||
| 		} | 		case db.IsErrNamePatternNotAllowed(err): | ||||||
|  | 			ctx.Data["OrgName"] = true | ||||||
| 		if err := container_service.UpdateRepositoryNames(ctx, org.AsUser(), form.Name); err != nil { | 			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplSettingsOptions, &form) | ||||||
| 			ctx.ServerError("UpdateRepositoryNames", err) | 			return | ||||||
|  | 		case err != nil: | ||||||
|  | 			ctx.ServerError("org_service.RenameOrganization", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -186,7 +177,7 @@ func SettingsDelete(ctx *context.Context) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := org.DeleteOrganization(ctx.Org.Organization); err != nil { | 		if err := org_service.DeleteOrganization(ctx.Org.Organization); err != nil { | ||||||
| 			if models.IsErrUserOwnRepos(err) { | 			if models.IsErrUserOwnRepos(err) { | ||||||
| 				ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) | 				ctx.Flash.Error(ctx.Tr("form.org_still_own_repo")) | ||||||
| 				ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") | 				ctx.Redirect(ctx.Org.OrgLink + "/settings/delete") | ||||||
|   | |||||||
| @@ -49,15 +49,16 @@ func Profile(ctx *context.Context) { | |||||||
|  |  | ||||||
| // HandleUsernameChange handle username changes from user settings and admin interface | // HandleUsernameChange handle username changes from user settings and admin interface | ||||||
| func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName string) error { | func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName string) error { | ||||||
| 	// Non-local users are not allowed to change their username. | 	oldName := user.Name | ||||||
| 	if !user.IsLocal() { |  | ||||||
| 		ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user")) |  | ||||||
| 		return fmt.Errorf(ctx.Tr("form.username_change_not_local_user")) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// rename user | 	// rename user | ||||||
| 	if err := user_service.RenameUser(ctx, user, newName); err != nil { | 	if err := user_service.RenameUser(ctx, user, newName); err != nil { | ||||||
| 		switch { | 		switch { | ||||||
|  | 		// Noop as username is not changed | ||||||
|  | 		case user_model.IsErrUsernameNotChanged(err): | ||||||
|  | 			ctx.Flash.Error(ctx.Tr("form.username_has_not_been_changed")) | ||||||
|  | 		// Non-local users are not allowed to change their username. | ||||||
|  | 		case user_model.IsErrUserIsNotLocal(err): | ||||||
|  | 			ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user")) | ||||||
| 		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): | ||||||
| @@ -73,7 +74,7 @@ func HandleUsernameChange(ctx *context.Context, user *user_model.User, newName s | |||||||
| 		} | 		} | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | 	log.Trace("User name changed: %s -> %s", oldName, newName) | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,20 +4,22 @@ | |||||||
| package org | package org | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/organization" | 	org_model "code.gitea.io/gitea/models/organization" | ||||||
| 	packages_model "code.gitea.io/gitea/models/packages" | 	packages_model "code.gitea.io/gitea/models/packages" | ||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"code.gitea.io/gitea/modules/storage" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	user_service "code.gitea.io/gitea/services/user" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // DeleteOrganization completely and permanently deletes everything of organization. | // DeleteOrganization completely and permanently deletes everything of organization. | ||||||
| func DeleteOrganization(org *organization.Organization) error { | func DeleteOrganization(org *org_model.Organization) error { | ||||||
| 	ctx, commiter, err := db.TxContext(db.DefaultContext) | 	ctx, commiter, err := db.TxContext(db.DefaultContext) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| @@ -39,7 +41,7 @@ func DeleteOrganization(org *organization.Organization) error { | |||||||
| 		return models.ErrUserOwnPackages{UID: org.ID} | 		return models.ErrUserOwnPackages{UID: org.ID} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := organization.DeleteOrganization(ctx, org); err != nil { | 	if err := org_model.DeleteOrganization(ctx, org); err != nil { | ||||||
| 		return fmt.Errorf("DeleteOrganization: %w", err) | 		return fmt.Errorf("DeleteOrganization: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -53,15 +55,20 @@ func DeleteOrganization(org *organization.Organization) error { | |||||||
| 	path := user_model.UserPath(org.Name) | 	path := user_model.UserPath(org.Name) | ||||||
|  |  | ||||||
| 	if err := util.RemoveAll(path); err != nil { | 	if err := util.RemoveAll(path); err != nil { | ||||||
| 		return fmt.Errorf("Failed to RemoveAll %s: %w", path, err) | 		return fmt.Errorf("failed to RemoveAll %s: %w", path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if len(org.Avatar) > 0 { | 	if len(org.Avatar) > 0 { | ||||||
| 		avatarPath := org.CustomAvatarRelativePath() | 		avatarPath := org.CustomAvatarRelativePath() | ||||||
| 		if err := storage.Avatars.Delete(avatarPath); err != nil { | 		if err := storage.Avatars.Delete(avatarPath); err != nil { | ||||||
| 			return fmt.Errorf("Failed to remove %s: %w", avatarPath, err) | 			return fmt.Errorf("failed to remove %s: %w", avatarPath, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // RenameOrganization renames an organization. | ||||||
|  | func RenameOrganization(ctx context.Context, org *org_model.Organization, newName string) error { | ||||||
|  | 	return user_service.RenameUser(ctx, org.AsUser(), newName) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								services/user/avatar.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								services/user/avatar.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | // Copyright 2023 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package user | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models/db" | ||||||
|  | 	user_model "code.gitea.io/gitea/models/user" | ||||||
|  | 	"code.gitea.io/gitea/modules/avatar" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 	"code.gitea.io/gitea/modules/storage" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // UploadAvatar saves custom avatar for user. | ||||||
|  | func UploadAvatar(u *user_model.User, data []byte) error { | ||||||
|  | 	avatarData, err := avatar.ProcessAvatarImage(data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ctx, committer, err := db.TxContext(db.DefaultContext) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	defer committer.Close() | ||||||
|  |  | ||||||
|  | 	u.UseCustomAvatar = true | ||||||
|  | 	u.Avatar = avatar.HashAvatar(u.ID, data) | ||||||
|  | 	if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil { | ||||||
|  | 		return fmt.Errorf("updateUser: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { | ||||||
|  | 		_, err := w.Write(avatarData) | ||||||
|  | 		return err | ||||||
|  | 	}); err != nil { | ||||||
|  | 		return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return committer.Commit() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // DeleteAvatar deletes the user's custom avatar. | ||||||
|  | func DeleteAvatar(u *user_model.User) error { | ||||||
|  | 	aPath := u.CustomAvatarRelativePath() | ||||||
|  | 	log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath) | ||||||
|  | 	if len(u.Avatar) > 0 { | ||||||
|  | 		if err := storage.Avatars.Delete(aPath); err != nil { | ||||||
|  | 			return fmt.Errorf("Failed to remove %s: %w", aPath, err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	u.UseCustomAvatar = false | ||||||
|  | 	u.Avatar = "" | ||||||
|  | 	if _, err := db.GetEngine(db.DefaultContext).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil { | ||||||
|  | 		return fmt.Errorf("UpdateUser: %w", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
| @@ -1,41 +0,0 @@ | |||||||
| // 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 |  | ||||||
| } |  | ||||||
| @@ -6,7 +6,8 @@ package user | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"os" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| @@ -17,29 +18,105 @@ import ( | |||||||
| 	repo_model "code.gitea.io/gitea/models/repo" | 	repo_model "code.gitea.io/gitea/models/repo" | ||||||
| 	system_model "code.gitea.io/gitea/models/system" | 	system_model "code.gitea.io/gitea/models/system" | ||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/avatar" |  | ||||||
| 	"code.gitea.io/gitea/modules/eventsource" | 	"code.gitea.io/gitea/modules/eventsource" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/storage" | 	"code.gitea.io/gitea/modules/storage" | ||||||
| 	"code.gitea.io/gitea/modules/util" | 	"code.gitea.io/gitea/modules/util" | ||||||
|  | 	"code.gitea.io/gitea/services/agit" | ||||||
| 	"code.gitea.io/gitea/services/packages" | 	"code.gitea.io/gitea/services/packages" | ||||||
|  | 	container_service "code.gitea.io/gitea/services/packages/container" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // RenameUser renames a user | // RenameUser renames a user | ||||||
| func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error { | func RenameUser(ctx context.Context, u *user_model.User, newUserName string) error { | ||||||
|  | 	// Non-local users are not allowed to change their username. | ||||||
|  | 	if !u.IsOrganization() && !u.IsLocal() { | ||||||
|  | 		return user_model.ErrUserIsNotLocal{ | ||||||
|  | 			UID:  u.ID, | ||||||
|  | 			Name: u.Name, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if newUserName == u.Name { | ||||||
|  | 		return user_model.ErrUsernameNotChanged{ | ||||||
|  | 			UID:  u.ID, | ||||||
|  | 			Name: u.Name, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := user_model.IsUsableUsername(newUserName); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	onlyCapitalization := strings.EqualFold(newUserName, u.Name) | ||||||
|  | 	oldUserName := u.Name | ||||||
|  |  | ||||||
|  | 	if onlyCapitalization { | ||||||
|  | 		u.Name = newUserName | ||||||
|  | 		if err := user_model.UpdateUserCols(ctx, u, "name"); err != nil { | ||||||
|  | 			u.Name = oldUserName | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	ctx, committer, err := db.TxContext(ctx) | 	ctx, committer, err := db.TxContext(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	defer committer.Close() | 	defer committer.Close() | ||||||
| 	if err := renameUser(ctx, u, newUserName); err != nil { |  | ||||||
|  | 	isExist, err := user_model.IsUserExist(ctx, u.ID, newUserName) | ||||||
|  | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if err := committer.Commit(); err != nil { | 	if isExist { | ||||||
|  | 		return user_model.ErrUserAlreadyExist{ | ||||||
|  | 			Name: newUserName, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = repo_model.UpdateRepositoryOwnerName(ctx, oldUserName, newUserName); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return err |  | ||||||
|  | 	if err = user_model.NewUserRedirect(ctx, u.ID, oldUserName, 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.UpdateUserCols(ctx, u, "name", "lower_name"); err != nil { | ||||||
|  | 		u.Name = oldUserName | ||||||
|  | 		u.LowerName = strings.ToLower(oldUserName) | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Do not fail if directory does not exist | ||||||
|  | 	if err = util.Rename(user_model.UserPath(oldUserName), user_model.UserPath(newUserName)); err != nil && !os.IsNotExist(err) { | ||||||
|  | 		u.Name = oldUserName | ||||||
|  | 		u.LowerName = strings.ToLower(oldUserName) | ||||||
|  | 		return fmt.Errorf("rename user directory: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err = committer.Commit(); err != nil { | ||||||
|  | 		u.Name = oldUserName | ||||||
|  | 		u.LowerName = strings.ToLower(oldUserName) | ||||||
|  | 		if err2 := util.Rename(user_model.UserPath(newUserName), user_model.UserPath(oldUserName)); err2 != nil && !os.IsNotExist(err2) { | ||||||
|  | 			log.Critical("Unable to rollback directory change during failed username change from: %s to: %s. DB Error: %v. Filesystem Error: %v", oldUserName, newUserName, err, err2) | ||||||
|  | 			return fmt.Errorf("failed to rollback directory change during failed username change from: %s to: %s. DB Error: %w. Filesystem Error: %v", oldUserName, newUserName, err, err2) | ||||||
|  | 		} | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // DeleteUser completely and permanently deletes everything of a user, | // DeleteUser completely and permanently deletes everything of a user, | ||||||
| @@ -240,50 +317,3 @@ func DeleteInactiveUsers(ctx context.Context, olderThan time.Duration) error { | |||||||
|  |  | ||||||
| 	return user_model.DeleteInactiveEmailAddresses(ctx) | 	return user_model.DeleteInactiveEmailAddresses(ctx) | ||||||
| } | } | ||||||
|  |  | ||||||
| // UploadAvatar saves custom avatar for user. |  | ||||||
| func UploadAvatar(u *user_model.User, data []byte) error { |  | ||||||
| 	avatarData, err := avatar.ProcessAvatarImage(data) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	ctx, committer, err := db.TxContext(db.DefaultContext) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	defer committer.Close() |  | ||||||
|  |  | ||||||
| 	u.UseCustomAvatar = true |  | ||||||
| 	u.Avatar = avatar.HashAvatar(u.ID, data) |  | ||||||
| 	if err = user_model.UpdateUserCols(ctx, u, "use_custom_avatar", "avatar"); err != nil { |  | ||||||
| 		return fmt.Errorf("updateUser: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := storage.SaveFrom(storage.Avatars, u.CustomAvatarRelativePath(), func(w io.Writer) error { |  | ||||||
| 		_, err := w.Write(avatarData) |  | ||||||
| 		return err |  | ||||||
| 	}); err != nil { |  | ||||||
| 		return fmt.Errorf("Failed to create dir %s: %w", u.CustomAvatarRelativePath(), err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return committer.Commit() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // DeleteAvatar deletes the user's custom avatar. |  | ||||||
| func DeleteAvatar(u *user_model.User) error { |  | ||||||
| 	aPath := u.CustomAvatarRelativePath() |  | ||||||
| 	log.Trace("DeleteAvatar[%d]: %s", u.ID, aPath) |  | ||||||
| 	if len(u.Avatar) > 0 { |  | ||||||
| 		if err := storage.Avatars.Delete(aPath); err != nil { |  | ||||||
| 			return fmt.Errorf("Failed to remove %s: %w", aPath, err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	u.UseCustomAvatar = false |  | ||||||
| 	u.Avatar = "" |  | ||||||
| 	if _, err := db.GetEngine(db.DefaultContext).ID(u.ID).Cols("avatar, use_custom_avatar").Update(u); err != nil { |  | ||||||
| 		return fmt.Errorf("UpdateUser: %w", err) |  | ||||||
| 	} |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -241,3 +241,44 @@ func TestAPICreateRepoForUser(t *testing.T) { | |||||||
| 	) | 	) | ||||||
| 	MakeRequest(t, req, http.StatusCreated) | 	MakeRequest(t, req, http.StatusCreated) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestAPIRenameUser(t *testing.T) { | ||||||
|  | 	defer tests.PrepareTestEnv(t)() | ||||||
|  | 	adminUsername := "user1" | ||||||
|  | 	token := getUserToken(t, adminUsername, auth_model.AccessTokenScopeSudo) | ||||||
|  | 	urlStr := fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "user2", token) | ||||||
|  | 	req := NewRequestWithValues(t, "POST", urlStr, map[string]string{ | ||||||
|  | 		// required | ||||||
|  | 		"new_name": "User2", | ||||||
|  | 	}) | ||||||
|  | 	MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "User2", token) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ | ||||||
|  | 		// required | ||||||
|  | 		"new_name": "User2-2-2", | ||||||
|  | 	}) | ||||||
|  | 	MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "User2", token) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ | ||||||
|  | 		// required | ||||||
|  | 		"new_name": "user1", | ||||||
|  | 	}) | ||||||
|  | 	// the old user name still be used by with a redirect | ||||||
|  | 	MakeRequest(t, req, http.StatusTemporaryRedirect) | ||||||
|  |  | ||||||
|  | 	urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "User2-2-2", token) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ | ||||||
|  | 		// required | ||||||
|  | 		"new_name": "user1", | ||||||
|  | 	}) | ||||||
|  | 	MakeRequest(t, req, http.StatusUnprocessableEntity) | ||||||
|  |  | ||||||
|  | 	urlStr = fmt.Sprintf("/api/v1/admin/users/%s/rename?token=%s", "User2-2-2", token) | ||||||
|  | 	req = NewRequestWithValues(t, "POST", urlStr, map[string]string{ | ||||||
|  | 		// required | ||||||
|  | 		"new_name": "user2", | ||||||
|  | 	}) | ||||||
|  | 	MakeRequest(t, req, http.StatusOK) | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user