Redirect on changed user and org name (#11649)
* Add redirect for user * Add redirect for orgs * Add user redirect test * Appease linter * Add comment to DeleteUserRedirect function * Fix locale changes * Fix GetUserByParams * Fix orgAssignment * Remove debug logging * Add redirect prompt * Dont Export DeleteUserRedirect & only use it within a session * Unexport newUserRedirect * cleanup * Fix & Dedub API code * Format Template * Add Migration & rm dublicat * Refactor: unexport newRepoRedirect() & rm dedub del exec * if this fails we'll need to re-rename the user directory Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: zeripath <art27@cantab.net> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							parent
							
								
									4f608ad31f
								
							
						
					
					
						commit
						bc05ddc0eb
					
				
					 25 changed files with 325 additions and 64 deletions
				
			
		|  | @ -146,6 +146,21 @@ func (err ErrUserNotExist) Error() string { | ||||||
| 	return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) | 	return fmt.Sprintf("user does not exist [uid: %d, name: %s, keyid: %d]", err.UID, err.Name, err.KeyID) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ErrUserRedirectNotExist represents a "UserRedirectNotExist" kind of error.
 | ||||||
|  | type ErrUserRedirectNotExist struct { | ||||||
|  | 	Name string | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // IsErrUserRedirectNotExist check if an error is an ErrUserRedirectNotExist.
 | ||||||
|  | func IsErrUserRedirectNotExist(err error) bool { | ||||||
|  | 	_, ok := err.(ErrUserRedirectNotExist) | ||||||
|  | 	return ok | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (err ErrUserRedirectNotExist) Error() string { | ||||||
|  | 	return fmt.Sprintf("user redirect does not exist [name: %s]", err.Name) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error.
 | // ErrUserProhibitLogin represents a "ErrUserProhibitLogin" kind of error.
 | ||||||
| type ErrUserProhibitLogin struct { | type ErrUserProhibitLogin struct { | ||||||
| 	UID  int64 | 	UID  int64 | ||||||
|  |  | ||||||
							
								
								
									
										4
									
								
								models/fixtures/user_redirect.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								models/fixtures/user_redirect.yml
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | - | ||||||
|  |   id: 1 | ||||||
|  |   lower_name: olduser1 | ||||||
|  |   redirect_user_id: 1 | ||||||
|  | @ -279,6 +279,8 @@ var migrations = []Migration{ | ||||||
| 	NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim), | 	NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim), | ||||||
| 	// v166 -> v167
 | 	// v166 -> v167
 | ||||||
| 	NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD), | 	NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD), | ||||||
|  | 	// v167 -> v168
 | ||||||
|  | 	NewMigration("Add user redirect", addUserRedirect), | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetCurrentDBVersion returns the current db version
 | // GetCurrentDBVersion returns the current db version
 | ||||||
|  |  | ||||||
							
								
								
									
										24
									
								
								models/migrations/v167.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								models/migrations/v167.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | // Copyright 2021 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package migrations | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 
 | ||||||
|  | 	"xorm.io/xorm" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func addUserRedirect(x *xorm.Engine) (err error) { | ||||||
|  | 	type UserRedirect struct { | ||||||
|  | 		ID             int64  `xorm:"pk autoincr"` | ||||||
|  | 		LowerName      string `xorm:"UNIQUE(s) INDEX NOT NULL"` | ||||||
|  | 		RedirectUserID int64 | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := x.Sync2(new(UserRedirect)); err != nil { | ||||||
|  | 		return fmt.Errorf("Sync2: %v", err) | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -128,6 +128,7 @@ func init() { | ||||||
| 		new(Task), | 		new(Task), | ||||||
| 		new(LanguageStat), | 		new(LanguageStat), | ||||||
| 		new(EmailHash), | 		new(EmailHash), | ||||||
|  | 		new(UserRedirect), | ||||||
| 		new(Project), | 		new(Project), | ||||||
| 		new(ProjectBoard), | 		new(ProjectBoard), | ||||||
| 		new(ProjectIssue), | 		new(ProjectIssue), | ||||||
|  |  | ||||||
|  | @ -171,6 +171,10 @@ func CreateOrganization(org, owner *User) (err error) { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if err = deleteUserRedirect(sess, org.Name); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	if _, err = sess.Insert(org); err != nil { | 	if _, err = sess.Insert(org); err != nil { | ||||||
| 		return fmt.Errorf("insert organization: %v", err) | 		return fmt.Errorf("insert organization: %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -1312,8 +1312,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error | ||||||
| 		return fmt.Errorf("delete repo redirect: %v", err) | 		return fmt.Errorf("delete repo redirect: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err := NewRepoRedirect(DBContext{sess}, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil { | 	if err := newRepoRedirect(sess, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil { | ||||||
| 		return fmt.Errorf("NewRepoRedirect: %v", err) | 		return fmt.Errorf("newRepoRedirect: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return sess.Commit() | 	return sess.Commit() | ||||||
|  | @ -1361,12 +1361,7 @@ func ChangeRepositoryName(doer *User, repo *Repository, newRepoName string) (err | ||||||
| 		return fmt.Errorf("sess.Begin: %v", err) | 		return fmt.Errorf("sess.Begin: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If there was previously a redirect at this location, remove it.
 | 	if err := newRepoRedirect(sess, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { | ||||||
| 	if err = deleteRepoRedirect(sess, repo.OwnerID, newRepoName); err != nil { |  | ||||||
| 		return fmt.Errorf("delete repo redirect: %v", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := NewRepoRedirect(DBContext{sess}, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -28,16 +28,16 @@ func LookupRepoRedirect(ownerID int64, repoName string) (int64, error) { | ||||||
| 	return redirect.RedirectRepoID, nil | 	return redirect.RedirectRepoID, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NewRepoRedirect create a new repo redirect
 | // newRepoRedirect create a new repo redirect
 | ||||||
| func NewRepoRedirect(ctx DBContext, ownerID, repoID int64, oldRepoName, newRepoName string) error { | func newRepoRedirect(e Engine, ownerID, repoID int64, oldRepoName, newRepoName string) error { | ||||||
| 	oldRepoName = strings.ToLower(oldRepoName) | 	oldRepoName = strings.ToLower(oldRepoName) | ||||||
| 	newRepoName = strings.ToLower(newRepoName) | 	newRepoName = strings.ToLower(newRepoName) | ||||||
| 
 | 
 | ||||||
| 	if err := deleteRepoRedirect(ctx.e, ownerID, newRepoName); err != nil { | 	if err := deleteRepoRedirect(e, ownerID, newRepoName); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err := ctx.e.Insert(&RepoRedirect{ | 	if _, err := e.Insert(&RepoRedirect{ | ||||||
| 		OwnerID:        ownerID, | 		OwnerID:        ownerID, | ||||||
| 		LowerName:      oldRepoName, | 		LowerName:      oldRepoName, | ||||||
| 		RedirectRepoID: repoID, | 		RedirectRepoID: repoID, | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ func TestNewRepoRedirect(t *testing.T) { | ||||||
| 	assert.NoError(t, PrepareTestDatabase()) | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
| 
 | 
 | ||||||
| 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||||
| 	assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "newreponame")) | 	assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "newreponame")) | ||||||
| 
 | 
 | ||||||
| 	AssertExistsAndLoadBean(t, &RepoRedirect{ | 	AssertExistsAndLoadBean(t, &RepoRedirect{ | ||||||
| 		OwnerID:        repo.OwnerID, | 		OwnerID:        repo.OwnerID, | ||||||
|  | @ -45,7 +45,7 @@ func TestNewRepoRedirect2(t *testing.T) { | ||||||
| 	assert.NoError(t, PrepareTestDatabase()) | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
| 
 | 
 | ||||||
| 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository) | ||||||
| 	assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "oldrepo1")) | 	assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "oldrepo1")) | ||||||
| 
 | 
 | ||||||
| 	AssertExistsAndLoadBean(t, &RepoRedirect{ | 	AssertExistsAndLoadBean(t, &RepoRedirect{ | ||||||
| 		OwnerID:        repo.OwnerID, | 		OwnerID:        repo.OwnerID, | ||||||
|  | @ -64,7 +64,7 @@ func TestNewRepoRedirect3(t *testing.T) { | ||||||
| 	assert.NoError(t, PrepareTestDatabase()) | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
| 
 | 
 | ||||||
| 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) | 	repo := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository) | ||||||
| 	assert.NoError(t, NewRepoRedirect(DefaultDBContext(), repo.OwnerID, repo.ID, repo.Name, "newreponame")) | 	assert.NoError(t, newRepoRedirect(x, repo.OwnerID, repo.ID, repo.Name, "newreponame")) | ||||||
| 
 | 
 | ||||||
| 	AssertExistsAndLoadBean(t, &RepoRedirect{ | 	AssertExistsAndLoadBean(t, &RepoRedirect{ | ||||||
| 		OwnerID:        repo.OwnerID, | 		OwnerID:        repo.OwnerID, | ||||||
|  |  | ||||||
|  | @ -863,6 +863,10 @@ func CreateUser(u *User) (err error) { | ||||||
| 		return ErrUserAlreadyExist{u.Name} | 		return ErrUserAlreadyExist{u.Name} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if err = deleteUserRedirect(sess, u.Name); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	u.Email = strings.ToLower(u.Email) | 	u.Email = strings.ToLower(u.Email) | ||||||
| 	isExist, err = sess. | 	isExist, err = sess. | ||||||
| 		Where("email=?", u.Email). | 		Where("email=?", u.Email). | ||||||
|  | @ -973,6 +977,7 @@ func VerifyActiveEmailCode(code, email string) *EmailAddress { | ||||||
| 
 | 
 | ||||||
| // 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(u *User, newUserName string) (err error) { | ||||||
|  | 	oldUserName := u.Name | ||||||
| 	if err = IsUsableUsername(newUserName); err != nil { | 	if err = IsUsableUsername(newUserName); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  | @ -990,16 +995,28 @@ func ChangeUserName(u *User, newUserName string) (err error) { | ||||||
| 		return ErrUserAlreadyExist{newUserName} | 		return ErrUserAlreadyExist{newUserName} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if _, err = sess.Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, u.Name); err != nil { | 	if _, err = sess.Exec("UPDATE `repository` SET owner_name=? WHERE owner_name=?", newUserName, oldUserName); err != nil { | ||||||
| 		return fmt.Errorf("Change repo owner name: %v", err) | 		return fmt.Errorf("Change repo owner name: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Do not fail if directory does not exist
 | 	// Do not fail if directory does not exist
 | ||||||
| 	if err = os.Rename(UserPath(u.Name), UserPath(newUserName)); err != nil && !os.IsNotExist(err) { | 	if err = os.Rename(UserPath(oldUserName), UserPath(newUserName)); err != nil && !os.IsNotExist(err) { | ||||||
| 		return fmt.Errorf("Rename user directory: %v", err) | 		return fmt.Errorf("Rename user directory: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return sess.Commit() | 	if err = newUserRedirect(sess, u.ID, oldUserName, newUserName); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err = sess.Commit(); err != nil { | ||||||
|  | 		if err2 := os.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
 | ||||||
|  |  | ||||||
							
								
								
									
										52
									
								
								models/user_redirect.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								models/user_redirect.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | ||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package models | ||||||
|  | 
 | ||||||
|  | import "strings" | ||||||
|  | 
 | ||||||
|  | // UserRedirect represents that a user name should be redirected to another
 | ||||||
|  | type UserRedirect struct { | ||||||
|  | 	ID             int64  `xorm:"pk autoincr"` | ||||||
|  | 	LowerName      string `xorm:"UNIQUE(s) INDEX NOT NULL"` | ||||||
|  | 	RedirectUserID int64  // userID to redirect to
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // LookupUserRedirect look up userID if a user has a redirect name
 | ||||||
|  | func LookupUserRedirect(userName string) (int64, error) { | ||||||
|  | 	userName = strings.ToLower(userName) | ||||||
|  | 	redirect := &UserRedirect{LowerName: userName} | ||||||
|  | 	if has, err := x.Get(redirect); err != nil { | ||||||
|  | 		return 0, err | ||||||
|  | 	} else if !has { | ||||||
|  | 		return 0, ErrUserRedirectNotExist{Name: userName} | ||||||
|  | 	} | ||||||
|  | 	return redirect.RedirectUserID, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // newUserRedirect create a new user redirect
 | ||||||
|  | func newUserRedirect(e Engine, ID int64, oldUserName, newUserName string) error { | ||||||
|  | 	oldUserName = strings.ToLower(oldUserName) | ||||||
|  | 	newUserName = strings.ToLower(newUserName) | ||||||
|  | 
 | ||||||
|  | 	if err := deleteUserRedirect(e, newUserName); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if _, err := e.Insert(&UserRedirect{ | ||||||
|  | 		LowerName:      oldUserName, | ||||||
|  | 		RedirectUserID: ID, | ||||||
|  | 	}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // deleteUserRedirect delete any redirect from the specified user name to
 | ||||||
|  | // anything else
 | ||||||
|  | func deleteUserRedirect(e Engine, userName string) error { | ||||||
|  | 	userName = strings.ToLower(userName) | ||||||
|  | 	_, err := e.Delete(&UserRedirect{LowerName: userName}) | ||||||
|  | 	return err | ||||||
|  | } | ||||||
							
								
								
									
										69
									
								
								models/user_redirect_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								models/user_redirect_test.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | ||||||
|  | // Copyright 2020 The Gitea Authors. All rights reserved.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package models | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestLookupUserRedirect(t *testing.T) { | ||||||
|  | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
|  | 
 | ||||||
|  | 	userID, err := LookupUserRedirect("olduser1") | ||||||
|  | 	assert.NoError(t, err) | ||||||
|  | 	assert.EqualValues(t, 1, userID) | ||||||
|  | 
 | ||||||
|  | 	_, err = LookupUserRedirect("doesnotexist") | ||||||
|  | 	assert.True(t, IsErrUserRedirectNotExist(err)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestNewUserRedirect(t *testing.T) { | ||||||
|  | 	// redirect to a completely new name
 | ||||||
|  | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
|  | 
 | ||||||
|  | 	user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) | ||||||
|  | 	assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "newusername")) | ||||||
|  | 
 | ||||||
|  | 	AssertExistsAndLoadBean(t, &UserRedirect{ | ||||||
|  | 		LowerName:      user.LowerName, | ||||||
|  | 		RedirectUserID: user.ID, | ||||||
|  | 	}) | ||||||
|  | 	AssertExistsAndLoadBean(t, &UserRedirect{ | ||||||
|  | 		LowerName:      "olduser1", | ||||||
|  | 		RedirectUserID: user.ID, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestNewUserRedirect2(t *testing.T) { | ||||||
|  | 	// redirect to previously used name
 | ||||||
|  | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
|  | 
 | ||||||
|  | 	user := AssertExistsAndLoadBean(t, &User{ID: 1}).(*User) | ||||||
|  | 	assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "olduser1")) | ||||||
|  | 
 | ||||||
|  | 	AssertExistsAndLoadBean(t, &UserRedirect{ | ||||||
|  | 		LowerName:      user.LowerName, | ||||||
|  | 		RedirectUserID: user.ID, | ||||||
|  | 	}) | ||||||
|  | 	AssertNotExistsBean(t, &UserRedirect{ | ||||||
|  | 		LowerName:      "olduser1", | ||||||
|  | 		RedirectUserID: user.ID, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func TestNewUserRedirect3(t *testing.T) { | ||||||
|  | 	// redirect for a previously-unredirected user
 | ||||||
|  | 	assert.NoError(t, PrepareTestDatabase()) | ||||||
|  | 
 | ||||||
|  | 	user := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User) | ||||||
|  | 	assert.NoError(t, newUserRedirect(x, user.ID, user.Name, "newusername")) | ||||||
|  | 
 | ||||||
|  | 	AssertExistsAndLoadBean(t, &UserRedirect{ | ||||||
|  | 		LowerName:      user.LowerName, | ||||||
|  | 		RedirectUserID: user.ID, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | @ -90,6 +90,26 @@ func (ctx *Context) IsUserRepoReaderAny() bool { | ||||||
| 	return ctx.Repo.HasAccess() | 	return ctx.Repo.HasAccess() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // RedirectToUser redirect to a differently-named user
 | ||||||
|  | func RedirectToUser(ctx *Context, userName string, redirectUserID int64) { | ||||||
|  | 	user, err := models.GetUserByID(redirectUserID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		ctx.ServerError("GetUserByID", err) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	redirectPath := strings.Replace( | ||||||
|  | 		ctx.Req.URL.Path, | ||||||
|  | 		userName, | ||||||
|  | 		user.Name, | ||||||
|  | 		1, | ||||||
|  | 	) | ||||||
|  | 	if ctx.Req.URL.RawQuery != "" { | ||||||
|  | 		redirectPath += "?" + ctx.Req.URL.RawQuery | ||||||
|  | 	} | ||||||
|  | 	ctx.Redirect(path.Join(setting.AppSubURL, redirectPath)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // HasAPIError returns true if error occurs in form validation.
 | // HasAPIError returns true if error occurs in form validation.
 | ||||||
| func (ctx *Context) HasAPIError() bool { | func (ctx *Context) HasAPIError() bool { | ||||||
| 	hasErr, ok := ctx.Data["HasError"] | 	hasErr, ok := ctx.Data["HasError"] | ||||||
|  |  | ||||||
|  | @ -54,7 +54,14 @@ func HandleOrgAssignment(ctx *Context, args ...bool) { | ||||||
| 	ctx.Org.Organization, err = models.GetUserByName(orgName) | 	ctx.Org.Organization, err = models.GetUserByName(orgName) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrUserNotExist(err) { | 		if models.IsErrUserNotExist(err) { | ||||||
| 			ctx.NotFound("GetUserByName", err) | 			redirectUserID, err := models.LookupUserRedirect(orgName) | ||||||
|  | 			if err == nil { | ||||||
|  | 				RedirectToUser(ctx, orgName, redirectUserID) | ||||||
|  | 			} else if models.IsErrUserRedirectNotExist(err) { | ||||||
|  | 				ctx.NotFound("GetUserByName", err) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.ServerError("LookupUserRedirect", err) | ||||||
|  | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetUserByName", err) | 			ctx.ServerError("GetUserByName", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -411,11 +411,18 @@ func RepoAssignment() macaron.Handler { | ||||||
| 			owner, err = models.GetUserByName(userName) | 			owner, err = models.GetUserByName(userName) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if models.IsErrUserNotExist(err) { | 				if models.IsErrUserNotExist(err) { | ||||||
| 					if ctx.Query("go-get") == "1" { | 					redirectUserID, err := models.LookupUserRedirect(userName) | ||||||
| 						EarlyResponseForGoGetMeta(ctx) | 					if err == nil { | ||||||
| 						return | 						RedirectToUser(ctx, userName, redirectUserID) | ||||||
|  | 					} else if models.IsErrUserRedirectNotExist(err) { | ||||||
|  | 						if ctx.Query("go-get") == "1" { | ||||||
|  | 							EarlyResponseForGoGetMeta(ctx) | ||||||
|  | 							return | ||||||
|  | 						} | ||||||
|  | 						ctx.NotFound("GetUserByName", nil) | ||||||
|  | 					} else { | ||||||
|  | 						ctx.ServerError("LookupUserRedirect", err) | ||||||
| 					} | 					} | ||||||
| 					ctx.NotFound("GetUserByName", nil) |  | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.ServerError("GetUserByName", err) | 					ctx.ServerError("GetUserByName", err) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -450,6 +450,7 @@ update_language_not_found = Language '%s' is not available. | ||||||
| update_profile_success = Your profile has been updated. | update_profile_success = Your profile has been updated. | ||||||
| change_username = Your username has been changed. | change_username = Your username has been changed. | ||||||
| change_username_prompt = Note: username changes also change your account URL. | change_username_prompt = Note: username changes also change your account URL. | ||||||
|  | change_username_redirect_prompt = The old username will redirect until it is claimed. | ||||||
| continue = Continue | continue = Continue | ||||||
| cancel = Cancel | cancel = Cancel | ||||||
| language = Language | language = Language | ||||||
|  | @ -1941,6 +1942,7 @@ settings.visibility.private_shortname = Private | ||||||
| settings.update_settings = Update Settings | settings.update_settings = Update Settings | ||||||
| settings.update_setting_success = Organization settings have been updated. | settings.update_setting_success = Organization settings have been updated. | ||||||
| settings.change_orgname_prompt = Note: changing the organization name also changes the organization's URL. | settings.change_orgname_prompt = Note: changing the organization name also changes the organization's URL. | ||||||
|  | settings.change_orgname_redirect_prompt = The old name will redirect until it is claimed. | ||||||
| settings.update_avatar_success = The organization's avatar has been updated. | settings.update_avatar_success = The organization's avatar has been updated. | ||||||
| settings.delete = Delete Organization | settings.delete = Delete Organization | ||||||
| settings.delete_account = Delete This Organization | settings.delete_account = Delete This Organization | ||||||
|  |  | ||||||
|  | @ -134,7 +134,13 @@ func repoAssignment() macaron.Handler { | ||||||
| 			owner, err = models.GetUserByName(userName) | 			owner, err = models.GetUserByName(userName) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if models.IsErrUserNotExist(err) { | 				if models.IsErrUserNotExist(err) { | ||||||
| 					ctx.NotFound() | 					if redirectUserID, err := models.LookupUserRedirect(userName); err == nil { | ||||||
|  | 						context.RedirectToUser(ctx.Context, userName, redirectUserID) | ||||||
|  | 					} else if models.IsErrUserRedirectNotExist(err) { | ||||||
|  | 						ctx.NotFound("GetUserByName", err) | ||||||
|  | 					} else { | ||||||
|  | 						ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) | ||||||
|  | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.Error(http.StatusInternalServerError, "GetUserByName", err) | 					ctx.Error(http.StatusInternalServerError, "GetUserByName", err) | ||||||
| 				} | 				} | ||||||
|  | @ -393,7 +399,14 @@ func orgAssignment(args ...bool) macaron.Handler { | ||||||
| 			ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org")) | 			ctx.Org.Organization, err = models.GetOrgByName(ctx.Params(":org")) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				if models.IsErrOrgNotExist(err) { | 				if models.IsErrOrgNotExist(err) { | ||||||
| 					ctx.NotFound() | 					redirectUserID, err := models.LookupUserRedirect(ctx.Params(":org")) | ||||||
|  | 					if err == nil { | ||||||
|  | 						context.RedirectToUser(ctx.Context, ctx.Params(":org"), redirectUserID) | ||||||
|  | 					} else if models.IsErrUserRedirectNotExist(err) { | ||||||
|  | 						ctx.NotFound("GetOrgByName", err) | ||||||
|  | 					} else { | ||||||
|  | 						ctx.Error(http.StatusInternalServerError, "LookupUserRedirect", err) | ||||||
|  | 					} | ||||||
| 				} else { | 				} else { | ||||||
| 					ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) | 					ctx.Error(http.StatusInternalServerError, "GetOrgByName", err) | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
							
								
								
									
										36
									
								
								routers/api/v1/user/helper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								routers/api/v1/user/helper.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | ||||||
|  | // Copyright 2021 The Gitea Authors.
 | ||||||
|  | // Use of this source code is governed by a MIT-style
 | ||||||
|  | // license that can be found in the LICENSE file.
 | ||||||
|  | 
 | ||||||
|  | package user | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 
 | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // GetUserByParamsName get user by name
 | ||||||
|  | func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { | ||||||
|  | 	username := ctx.Params(name) | ||||||
|  | 	user, err := models.GetUserByName(username) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if models.IsErrUserNotExist(err) { | ||||||
|  | 			if redirectUserID, err := models.LookupUserRedirect(username); err == nil { | ||||||
|  | 				context.RedirectToUser(ctx.Context, username, redirectUserID) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.NotFound("GetUserByName", err) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			ctx.Error(http.StatusInternalServerError, "GetUserByName", err) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return user | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // GetUserByParams returns user whose name is presented in URL (":username").
 | ||||||
|  | func GetUserByParams(ctx *context.APIContext) *models.User { | ||||||
|  | 	return GetUserByParamsName(ctx, ":username") | ||||||
|  | } | ||||||
|  | @ -39,25 +39,6 @@ func appendPrivateInformation(apiKey *api.PublicKey, key *models.PublicKey, defa | ||||||
| 	return apiKey, nil | 	return apiKey, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetUserByParamsName get user by name
 |  | ||||||
| func GetUserByParamsName(ctx *context.APIContext, name string) *models.User { |  | ||||||
| 	user, err := models.GetUserByName(ctx.Params(name)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if models.IsErrUserNotExist(err) { |  | ||||||
| 			ctx.NotFound() |  | ||||||
| 		} else { |  | ||||||
| 			ctx.Error(http.StatusInternalServerError, "GetUserByName", err) |  | ||||||
| 		} |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	return user |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetUserByParams returns user whose name is presented in URL paramenter.
 |  | ||||||
| func GetUserByParams(ctx *context.APIContext) *models.User { |  | ||||||
| 	return GetUserByParamsName(ctx, ":username") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func composePublicKeysAPILink() string { | func composePublicKeysAPILink() string { | ||||||
| 	return setting.AppURL + "api/v1/user/keys/" | 	return setting.AppURL + "api/v1/user/keys/" | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -107,13 +107,8 @@ func GetInfo(ctx *context.APIContext) { | ||||||
| 	//   "404":
 | 	//   "404":
 | ||||||
| 	//     "$ref": "#/responses/notFound"
 | 	//     "$ref": "#/responses/notFound"
 | ||||||
| 
 | 
 | ||||||
| 	u, err := models.GetUserByName(ctx.Params(":username")) | 	u := GetUserByParams(ctx) | ||||||
| 	if err != nil { | 	if ctx.Written() { | ||||||
| 		if models.IsErrUserNotExist(err) { |  | ||||||
| 			ctx.NotFound() |  | ||||||
| 		} else { |  | ||||||
| 			ctx.Error(http.StatusInternalServerError, "GetUserByName", err) |  | ||||||
| 		} |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -153,14 +148,8 @@ func GetUserHeatmapData(ctx *context.APIContext) { | ||||||
| 	//   "404":
 | 	//   "404":
 | ||||||
| 	//     "$ref": "#/responses/notFound"
 | 	//     "$ref": "#/responses/notFound"
 | ||||||
| 
 | 
 | ||||||
| 	// Get the user to throw an error if it does not exist
 | 	user := GetUserByParams(ctx) | ||||||
| 	user, err := models.GetUserByName(ctx.Params(":username")) | 	if ctx.Written() { | ||||||
| 	if err != nil { |  | ||||||
| 		if models.IsErrUserNotExist(err) { |  | ||||||
| 			ctx.Status(http.StatusNotFound) |  | ||||||
| 		} else { |  | ||||||
| 			ctx.Error(http.StatusInternalServerError, "GetUserByName", err) |  | ||||||
| 		} |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -102,8 +102,15 @@ func HTTP(ctx *context.Context) { | ||||||
| 
 | 
 | ||||||
| 	owner, err := models.GetUserByName(username) | 	owner, err := models.GetUserByName(username) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("Attempted access of unknown user from %s", ctx.RemoteAddr()) | 		if models.IsErrUserNotExist(err) { | ||||||
| 		ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err) | 			if redirectUserID, err := models.LookupUserRedirect(username); err == nil { | ||||||
|  | 				context.RedirectToUser(ctx, username, redirectUserID) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.NotFound("GetUserByName", err) | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			ctx.ServerError("GetUserByName", err) | ||||||
|  | 		} | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if !owner.IsOrganization() && !owner.IsActive { | 	if !owner.IsOrganization() && !owner.IsActive { | ||||||
|  |  | ||||||
|  | @ -23,7 +23,11 @@ func GetUserByName(ctx *context.Context, name string) *models.User { | ||||||
| 	user, err := models.GetUserByName(name) | 	user, err := models.GetUserByName(name) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if models.IsErrUserNotExist(err) { | 		if models.IsErrUserNotExist(err) { | ||||||
| 			ctx.NotFound("GetUserByName", nil) | 			if redirectUserID, err := models.LookupUserRedirect(name); err == nil { | ||||||
|  | 				context.RedirectToUser(ctx, name, redirectUserID) | ||||||
|  | 			} else { | ||||||
|  | 				ctx.NotFound("GetUserByName", err) | ||||||
|  | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			ctx.ServerError("GetUserByName", err) | 			ctx.ServerError("GetUserByName", err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | @ -13,7 +13,10 @@ | ||||||
| 					<form class="ui form" action="{{.Link}}" method="post"> | 					<form class="ui form" action="{{.Link}}" method="post"> | ||||||
| 						{{.CsrfTokenHtml}} | 						{{.CsrfTokenHtml}} | ||||||
| 						<div class="required field {{if .Err_Name}}error{{end}}"> | 						<div class="required field {{if .Err_Name}}error{{end}}"> | ||||||
| 							<label for="org_name">{{.i18n.Tr "org.org_name_holder"}}<span class="text red hide" id="org-name-change-prompt"> {{.i18n.Tr "org.settings.change_orgname_prompt"}}</span></label> | 							<label for="org_name">{{.i18n.Tr "org.org_name_holder"}} | ||||||
|  | 								<span class="text red hide" id="org-name-change-prompt"> {{.i18n.Tr "org.settings.change_orgname_prompt"}}</span> | ||||||
|  | 								<span class="text red hide" id="org-name-change-redirect-prompt"> {{.i18n.Tr "org.settings.change_orgname_redirect_prompt"}}</span> | ||||||
|  | 							</label> | ||||||
| 							<input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required> | 							<input id="org_name" name="name" value="{{.Org.Name}}" data-org-name="{{.Org.Name}}" autofocus required> | ||||||
| 						</div> | 						</div> | ||||||
| 						<div class="field {{if .Err_FullName}}error{{end}}"> | 						<div class="field {{if .Err_FullName}}error{{end}}"> | ||||||
|  |  | ||||||
|  | @ -11,7 +11,10 @@ | ||||||
| 			<form class="ui form" action="{{.Link}}" method="post"> | 			<form class="ui form" action="{{.Link}}" method="post"> | ||||||
| 				{{.CsrfTokenHtml}} | 				{{.CsrfTokenHtml}} | ||||||
| 				<div class="required field {{if .Err_Name}}error{{end}}"> | 				<div class="required field {{if .Err_Name}}error{{end}}"> | ||||||
| 					<label for="username">{{.i18n.Tr "username"}}<span class="text red hide" id="name-change-prompt"> {{.i18n.Tr "settings.change_username_prompt"}}</span></label> | 					<label for="username">{{.i18n.Tr "username"}} | ||||||
|  | 						<span class="text red hide" id="name-change-prompt"> {{.i18n.Tr "settings.change_username_prompt"}}</span> | ||||||
|  | 						<span class="text red hide" id="name-change-redirect-prompt"> {{.i18n.Tr "settings.change_username_redirect_prompt"}}</span> | ||||||
|  | 					</label> | ||||||
| 					<input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if not .SignedUser.IsLocal}}disabled{{end}}> | 					<input id="username" name="name" value="{{.SignedUser.Name}}" data-name="{{.SignedUser.Name}}" autofocus required {{if not .SignedUser.IsLocal}}disabled{{end}}> | ||||||
| 					{{if not .SignedUser.IsLocal}} | 					{{if not .SignedUser.IsLocal}} | ||||||
| 					<p class="help text blue">{{$.i18n.Tr "settings.password_username_disabled"}}</p> | 					<p class="help text blue">{{$.i18n.Tr "settings.password_username_disabled"}}</p> | ||||||
|  |  | ||||||
|  | @ -1736,10 +1736,13 @@ function initOrganization() { | ||||||
|   if ($('.organization.settings.options').length > 0) { |   if ($('.organization.settings.options').length > 0) { | ||||||
|     $('#org_name').on('keyup', function () { |     $('#org_name').on('keyup', function () { | ||||||
|       const $prompt = $('#org-name-change-prompt'); |       const $prompt = $('#org-name-change-prompt'); | ||||||
|  |       const $prompt_redirect = $('#org-name-change-redirect-prompt'); | ||||||
|       if ($(this).val().toString().toLowerCase() !== $(this).data('org-name').toString().toLowerCase()) { |       if ($(this).val().toString().toLowerCase() !== $(this).data('org-name').toString().toLowerCase()) { | ||||||
|         $prompt.show(); |         $prompt.show(); | ||||||
|  |         $prompt_redirect.show(); | ||||||
|       } else { |       } else { | ||||||
|         $prompt.hide(); |         $prompt.hide(); | ||||||
|  |         $prompt_redirect.hide(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  | @ -1755,10 +1758,13 @@ function initUserSettings() { | ||||||
|   if ($('.user.settings.profile').length > 0) { |   if ($('.user.settings.profile').length > 0) { | ||||||
|     $('#username').on('keyup', function () { |     $('#username').on('keyup', function () { | ||||||
|       const $prompt = $('#name-change-prompt'); |       const $prompt = $('#name-change-prompt'); | ||||||
|  |       const $prompt_redirect = $('#name-change-redirect-prompt'); | ||||||
|       if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) { |       if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) { | ||||||
|         $prompt.show(); |         $prompt.show(); | ||||||
|  |         $prompt_redirect.show(); | ||||||
|       } else { |       } else { | ||||||
|         $prompt.hide(); |         $prompt.hide(); | ||||||
|  |         $prompt_redirect.hide(); | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue