]> source.dussan.org Git - gitea.git/commitdiff
Redirect on changed user and org name (#11649)
authorAndrew Bezold <andrew.bezold@gmail.com>
Sun, 24 Jan 2021 15:23:05 +0000 (10:23 -0500)
committerGitHub <noreply@github.com>
Sun, 24 Jan 2021 15:23:05 +0000 (16:23 +0100)
* 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>
25 files changed:
models/error.go
models/fixtures/user_redirect.yml [new file with mode: 0644]
models/migrations/migrations.go
models/migrations/v167.go [new file with mode: 0644]
models/models.go
models/org.go
models/repo.go
models/repo_redirect.go
models/repo_redirect_test.go
models/user.go
models/user_redirect.go [new file with mode: 0644]
models/user_redirect_test.go [new file with mode: 0644]
modules/context/context.go
modules/context/org.go
modules/context/repo.go
options/locale/locale_en-US.ini
routers/api/v1/api.go
routers/api/v1/user/helper.go [new file with mode: 0644]
routers/api/v1/user/key.go
routers/api/v1/user/user.go
routers/repo/http.go
routers/user/profile.go
templates/org/settings/options.tmpl
templates/user/settings/profile.tmpl
web_src/js/index.js

index 7f1eda1b14eb5966b6c7daa109b6284841249e14..fc161ed806f338b03c858fd855c23677de215735 100644 (file)
@@ -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)
 }
 
+// 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.
 type ErrUserProhibitLogin struct {
        UID  int64
diff --git a/models/fixtures/user_redirect.yml b/models/fixtures/user_redirect.yml
new file mode 100644 (file)
index 0000000..8ff7993
--- /dev/null
@@ -0,0 +1,4 @@
+-
+  id: 1
+  lower_name: olduser1
+  redirect_user_id: 1
index f62bba2a71906db9a51e1b511dc957670356c7d7..3227f6f75451b59b1b166366d326dbee01339b08 100644 (file)
@@ -279,6 +279,8 @@ var migrations = []Migration{
        NewMigration("Convert hook task type from char(16) to varchar(16) and trim the column", convertHookTaskTypeToVarcharAndTrim),
        // v166 -> v167
        NewMigration("Where Password is Valid with Empty String delete it", recalculateUserEmptyPWD),
+       // v167 -> v168
+       NewMigration("Add user redirect", addUserRedirect),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v167.go b/models/migrations/v167.go
new file mode 100644 (file)
index 0000000..fd91f22
--- /dev/null
@@ -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
+}
index 2ace1ea6dd6a951d07ac102eaac152631f5c8092..1bfe75b7fc561f18f622485f2c9cc2ed3135a7a6 100644 (file)
@@ -128,6 +128,7 @@ func init() {
                new(Task),
                new(LanguageStat),
                new(EmailHash),
+               new(UserRedirect),
                new(Project),
                new(ProjectBoard),
                new(ProjectIssue),
index f45c9af7a7338b381c21dd866b337fa09c4ad6e0..ee867eec887f44c8d2fa32aa019925d0a263484c 100644 (file)
@@ -171,6 +171,10 @@ func CreateOrganization(org, owner *User) (err error) {
                return err
        }
 
+       if err = deleteUserRedirect(sess, org.Name); err != nil {
+               return err
+       }
+
        if _, err = sess.Insert(org); err != nil {
                return fmt.Errorf("insert organization: %v", err)
        }
index b11671e1fc09b665aea5d863b50397baec6fb77c..62d64fbee9183ae0239498d78e04c6c0819f7ddb 100644 (file)
@@ -1312,8 +1312,8 @@ func TransferOwnership(doer *User, newOwnerName string, repo *Repository) error
                return fmt.Errorf("delete repo redirect: %v", err)
        }
 
-       if err := NewRepoRedirect(DBContext{sess}, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
-               return fmt.Errorf("NewRepoRedirect: %v", err)
+       if err := newRepoRedirect(sess, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil {
+               return fmt.Errorf("newRepoRedirect: %v", err)
        }
 
        return sess.Commit()
@@ -1361,12 +1361,7 @@ func ChangeRepositoryName(doer *User, repo *Repository, newRepoName string) (err
                return fmt.Errorf("sess.Begin: %v", err)
        }
 
-       // If there was previously a redirect at this location, remove it.
-       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 {
+       if err := newRepoRedirect(sess, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil {
                return err
        }
 
index 182b6b41a2cf7f38d34198de4d53303eac195638..afbfeb499e11a081c7f9fa1457238f6a967b8d2d 100644 (file)
@@ -28,16 +28,16 @@ func LookupRepoRedirect(ownerID int64, repoName string) (int64, error) {
        return redirect.RedirectRepoID, nil
 }
 
-// NewRepoRedirect create a new repo redirect
-func NewRepoRedirect(ctx DBContext, ownerID, repoID int64, oldRepoName, newRepoName string) error {
+// newRepoRedirect create a new repo redirect
+func newRepoRedirect(e Engine, ownerID, repoID int64, oldRepoName, newRepoName string) error {
        oldRepoName = strings.ToLower(oldRepoName)
        newRepoName = strings.ToLower(newRepoName)
 
-       if err := deleteRepoRedirect(ctx.e, ownerID, newRepoName); err != nil {
+       if err := deleteRepoRedirect(e, ownerID, newRepoName); err != nil {
                return err
        }
 
-       if _, err := ctx.e.Insert(&RepoRedirect{
+       if _, err := e.Insert(&RepoRedirect{
                OwnerID:        ownerID,
                LowerName:      oldRepoName,
                RedirectRepoID: repoID,
index 44ec2b4e942b624f014d5da77b7e4d627b4e29f4..4c3184a0fd4799be41943f613908ad35c17a7d16 100644 (file)
@@ -26,7 +26,7 @@ func TestNewRepoRedirect(t *testing.T) {
        assert.NoError(t, PrepareTestDatabase())
 
        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{
                OwnerID:        repo.OwnerID,
@@ -45,7 +45,7 @@ func TestNewRepoRedirect2(t *testing.T) {
        assert.NoError(t, PrepareTestDatabase())
 
        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{
                OwnerID:        repo.OwnerID,
@@ -64,7 +64,7 @@ func TestNewRepoRedirect3(t *testing.T) {
        assert.NoError(t, PrepareTestDatabase())
 
        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{
                OwnerID:        repo.OwnerID,
index 746608aaa47cfbff9efcc5df1cb65faff211ade1..8147c9f62610bb36be43317976b8c6575afe8f87 100644 (file)
@@ -863,6 +863,10 @@ func CreateUser(u *User) (err error) {
                return ErrUserAlreadyExist{u.Name}
        }
 
+       if err = deleteUserRedirect(sess, u.Name); err != nil {
+               return err
+       }
+
        u.Email = strings.ToLower(u.Email)
        isExist, err = sess.
                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.
 func ChangeUserName(u *User, newUserName string) (err error) {
+       oldUserName := u.Name
        if err = IsUsableUsername(newUserName); err != nil {
                return err
        }
@@ -990,16 +995,28 @@ func ChangeUserName(u *User, newUserName string) (err error) {
                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)
        }
 
        // 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 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
diff --git a/models/user_redirect.go b/models/user_redirect.go
new file mode 100644 (file)
index 0000000..1da8b44
--- /dev/null
@@ -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
+}
diff --git a/models/user_redirect_test.go b/models/user_redirect_test.go
new file mode 100644 (file)
index 0000000..791c920
--- /dev/null
@@ -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,
+       })
+}
index 1ee31e0ebbac312e2db75759ac9c8ceaad3fde93..e4121649ae6747962f7a9812baf8a8ec16f4d73d 100644 (file)
@@ -90,6 +90,26 @@ func (ctx *Context) IsUserRepoReaderAny() bool {
        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.
 func (ctx *Context) HasAPIError() bool {
        hasErr, ok := ctx.Data["HasError"]
index 9b87fba9fd0bc0e1e48ce7780af4ec9f3ddf5dcf..f61a39c666b95857e09972b0476bcd83dd9909fc 100644 (file)
@@ -54,7 +54,14 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
        ctx.Org.Organization, err = models.GetUserByName(orgName)
        if err != nil {
                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 {
                        ctx.ServerError("GetUserByName", err)
                }
index 2aee6caca4b2cf525769c5f8ef44473a1e5d0ceb..63cb02dc067d745b15dd94f2c00d2760da8e8908 100644 (file)
@@ -411,11 +411,18 @@ func RepoAssignment() macaron.Handler {
                        owner, err = models.GetUserByName(userName)
                        if err != nil {
                                if models.IsErrUserNotExist(err) {
-                                       if ctx.Query("go-get") == "1" {
-                                               EarlyResponseForGoGetMeta(ctx)
-                                               return
+                                       redirectUserID, err := models.LookupUserRedirect(userName)
+                                       if err == nil {
+                                               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 {
                                        ctx.ServerError("GetUserByName", err)
                                }
index 770670ec1f872e187ffd63e28c988b4e4043d4e7..6c9604bf8babbb5a45562ae312c1cb0f8ba43e51 100644 (file)
@@ -450,6 +450,7 @@ update_language_not_found = Language '%s' is not available.
 update_profile_success = Your profile has been updated.
 change_username = Your username has been changed.
 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
 cancel = Cancel
 language = Language
@@ -1941,6 +1942,7 @@ settings.visibility.private_shortname = Private
 settings.update_settings = Update Settings
 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_redirect_prompt = The old name will redirect until it is claimed.
 settings.update_avatar_success = The organization's avatar has been updated.
 settings.delete = Delete Organization
 settings.delete_account = Delete This Organization
index 02ad8ab3606d6ccba974a8832d6b59a885307bc4..876f48ca5cca33c2978ddeeb0f842465e3bd82d9 100644 (file)
@@ -134,7 +134,13 @@ func repoAssignment() macaron.Handler {
                        owner, err = models.GetUserByName(userName)
                        if err != nil {
                                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 {
                                        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"))
                        if err != nil {
                                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 {
                                        ctx.Error(http.StatusInternalServerError, "GetOrgByName", err)
                                }
diff --git a/routers/api/v1/user/helper.go b/routers/api/v1/user/helper.go
new file mode 100644 (file)
index 0000000..fcdac25
--- /dev/null
@@ -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")
+}
index 8069660653652c98e3673201e8e870f98cbadad7..fa16df1836350fc334518faa86b51fdc5e059ab5 100644 (file)
@@ -39,25 +39,6 @@ func appendPrivateInformation(apiKey *api.PublicKey, key *models.PublicKey, defa
        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 {
        return setting.AppURL + "api/v1/user/keys/"
 }
index b860219e6290c1bc4aa6cfb35dbfa15a7dd4b640..ecc149fe52d324f82cd933c291136309d3aa71be 100644 (file)
@@ -107,13 +107,8 @@ func GetInfo(ctx *context.APIContext) {
        //   "404":
        //     "$ref": "#/responses/notFound"
 
-       u, err := models.GetUserByName(ctx.Params(":username"))
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.NotFound()
-               } else {
-                       ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
-               }
+       u := GetUserByParams(ctx)
+       if ctx.Written() {
                return
        }
 
@@ -153,14 +148,8 @@ func GetUserHeatmapData(ctx *context.APIContext) {
        //   "404":
        //     "$ref": "#/responses/notFound"
 
-       // Get the user to throw an error if it does not exist
-       user, err := models.GetUserByName(ctx.Params(":username"))
-       if err != nil {
-               if models.IsErrUserNotExist(err) {
-                       ctx.Status(http.StatusNotFound)
-               } else {
-                       ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
-               }
+       user := GetUserByParams(ctx)
+       if ctx.Written() {
                return
        }
 
index d4464ec62e4e9ee7e18463e86781a9e9a9140d50..3de45698e8dbbddd4393b26bfdf7bae226de1f1d 100644 (file)
@@ -102,8 +102,15 @@ func HTTP(ctx *context.Context) {
 
        owner, err := models.GetUserByName(username)
        if err != nil {
-               log.Error("Attempted access of unknown user from %s", ctx.RemoteAddr())
-               ctx.NotFoundOrServerError("GetUserByName", models.IsErrUserNotExist, err)
+               if 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
        }
        if !owner.IsOrganization() && !owner.IsActive {
index bd5b3592721ebb666548e766aaafb166ac475f51..e19407baa78cf4486e6c937d5858343183442cbd 100644 (file)
@@ -23,7 +23,11 @@ func GetUserByName(ctx *context.Context, name string) *models.User {
        user, err := models.GetUserByName(name)
        if err != nil {
                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 {
                        ctx.ServerError("GetUserByName", err)
                }
index 348f8cb5331f317bcb1d63004dac9e8c3f8a4262..09bf5e0caa16eecfa0a71015deed32b898b97466 100644 (file)
                                        <form class="ui form" action="{{.Link}}" method="post">
                                                {{.CsrfTokenHtml}}
                                                <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>
                                                </div>
                                                <div class="field {{if .Err_FullName}}error{{end}}">
index 7ec8d2a15fc1727240c41bd0dd19cc41364811b7..ee3cc589041a81b22b9f357130ac7aaa72f052af 100644 (file)
                        <form class="ui form" action="{{.Link}}" method="post">
                                {{.CsrfTokenHtml}}
                                <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}}>
                                        {{if not .SignedUser.IsLocal}}
                                        <p class="help text blue">{{$.i18n.Tr "settings.password_username_disabled"}}</p>
index 16bb412f0962b294621905426d5d22667bb12f7f..f5f4841410b4116cd6e3fe575bcf41cf306ba9fa 100644 (file)
@@ -1736,10 +1736,13 @@ function initOrganization() {
   if ($('.organization.settings.options').length > 0) {
     $('#org_name').on('keyup', function () {
       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()) {
         $prompt.show();
+        $prompt_redirect.show();
       } else {
         $prompt.hide();
+        $prompt_redirect.hide();
       }
     });
   }
@@ -1755,10 +1758,13 @@ function initUserSettings() {
   if ($('.user.settings.profile').length > 0) {
     $('#username').on('keyup', function () {
       const $prompt = $('#name-change-prompt');
+      const $prompt_redirect = $('#name-change-redirect-prompt');
       if ($(this).val().toString().toLowerCase() !== $(this).data('name').toString().toLowerCase()) {
         $prompt.show();
+        $prompt_redirect.show();
       } else {
         $prompt.hide();
+        $prompt_redirect.hide();
       }
     });
   }