mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Add option to change username to the admin panel (#14229)
Co-authored-by: Bwko <bouwko@gmail.com> Co-authored-by: techknowlogick <matti@mdranta.net> Co-authored-by: zeripath <art27@cantab.net>
This commit is contained in:
		
							
								
								
									
										82
									
								
								integrations/admin_user_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								integrations/admin_user_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | |||||||
|  | // 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 integrations | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"strconv" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/models" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestAdminViewUsers(t *testing.T) { | ||||||
|  | 	prepareTestEnv(t) | ||||||
|  |  | ||||||
|  | 	session := loginUser(t, "user1") | ||||||
|  | 	req := NewRequest(t, "GET", "/admin/users") | ||||||
|  | 	session.MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	session = loginUser(t, "user2") | ||||||
|  | 	req = NewRequest(t, "GET", "/admin/users") | ||||||
|  | 	session.MakeRequest(t, req, http.StatusForbidden) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAdminViewUser(t *testing.T) { | ||||||
|  | 	prepareTestEnv(t) | ||||||
|  |  | ||||||
|  | 	session := loginUser(t, "user1") | ||||||
|  | 	req := NewRequest(t, "GET", "/admin/users/1") | ||||||
|  | 	session.MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	session = loginUser(t, "user2") | ||||||
|  | 	req = NewRequest(t, "GET", "/admin/users/1") | ||||||
|  | 	session.MakeRequest(t, req, http.StatusForbidden) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAdminEditUser(t *testing.T) { | ||||||
|  | 	prepareTestEnv(t) | ||||||
|  |  | ||||||
|  | 	testSuccessfullEdit(t, models.User{ID: 2, Name: "newusername", LoginName: "otherlogin", Email: "new@e-mail.gitea"}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func testSuccessfullEdit(t *testing.T, formData models.User) { | ||||||
|  | 	makeRequest(t, formData, http.StatusFound) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func makeRequest(t *testing.T, formData models.User, headerCode int) { | ||||||
|  | 	session := loginUser(t, "user1") | ||||||
|  | 	csrf := GetCSRF(t, session, "/admin/users/"+strconv.Itoa(int(formData.ID))) | ||||||
|  | 	req := NewRequestWithValues(t, "POST", "/admin/users/"+strconv.Itoa(int(formData.ID)), map[string]string{ | ||||||
|  | 		"_csrf":      csrf, | ||||||
|  | 		"user_name":  formData.Name, | ||||||
|  | 		"login_name": formData.LoginName, | ||||||
|  | 		"login_type": "0-0", | ||||||
|  | 		"email":      formData.Email, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	session.MakeRequest(t, req, headerCode) | ||||||
|  | 	user := models.AssertExistsAndLoadBean(t, &models.User{ID: formData.ID}).(*models.User) | ||||||
|  | 	assert.Equal(t, formData.Name, user.Name) | ||||||
|  | 	assert.Equal(t, formData.LoginName, user.LoginName) | ||||||
|  | 	assert.Equal(t, formData.Email, user.Email) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func TestAdminDeleteUser(t *testing.T) { | ||||||
|  | 	defer prepareTestEnv(t)() | ||||||
|  |  | ||||||
|  | 	session := loginUser(t, "user1") | ||||||
|  |  | ||||||
|  | 	csrf := GetCSRF(t, session, "/admin/users/8") | ||||||
|  | 	req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{ | ||||||
|  | 		"_csrf": csrf, | ||||||
|  | 	}) | ||||||
|  | 	session.MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
|  | 	assertUserDeleted(t, 8) | ||||||
|  | 	models.CheckConsistencyFor(t, &models.User{}) | ||||||
|  | } | ||||||
| @@ -24,21 +24,6 @@ func assertUserDeleted(t *testing.T, userID int64) { | |||||||
| 	models.AssertNotExistsBean(t, &models.Star{UID: userID}) | 	models.AssertNotExistsBean(t, &models.Star{UID: userID}) | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestAdminDeleteUser(t *testing.T) { |  | ||||||
| 	defer prepareTestEnv(t)() |  | ||||||
|  |  | ||||||
| 	session := loginUser(t, "user1") |  | ||||||
|  |  | ||||||
| 	csrf := GetCSRF(t, session, "/admin/users/8") |  | ||||||
| 	req := NewRequestWithValues(t, "POST", "/admin/users/8/delete", map[string]string{ |  | ||||||
| 		"_csrf": csrf, |  | ||||||
| 	}) |  | ||||||
| 	session.MakeRequest(t, req, http.StatusOK) |  | ||||||
|  |  | ||||||
| 	assertUserDeleted(t, 8) |  | ||||||
| 	models.CheckConsistencyFor(t, &models.User{}) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| func TestUserDeleteAccount(t *testing.T) { | func TestUserDeleteAccount(t *testing.T) { | ||||||
| 	defer prepareTestEnv(t)() | 	defer prepareTestEnv(t)() | ||||||
|  |  | ||||||
|   | |||||||
| @@ -913,19 +913,19 @@ func ChangeUserName(u *User, newUserName string) (err error) { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	isExist, err := IsUserExist(0, newUserName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} else if isExist { |  | ||||||
| 		return ErrUserAlreadyExist{newUserName} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	sess := x.NewSession() | 	sess := x.NewSession() | ||||||
| 	defer sess.Close() | 	defer sess.Close() | ||||||
| 	if err = sess.Begin(); err != nil { | 	if err = sess.Begin(); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	isExist, err := isUserExist(sess, 0, newUserName) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} else if isExist { | ||||||
|  | 		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, u.Name); err != nil { | ||||||
| 		return fmt.Errorf("Change repo owner name: %v", err) | 		return fmt.Errorf("Change repo owner name: %v", err) | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ func (f *AdminCreateUserForm) Validate(ctx *macaron.Context, errs binding.Errors | |||||||
| // AdminEditUserForm form for admin to create user | // AdminEditUserForm form for admin to create user | ||||||
| type AdminEditUserForm struct { | type AdminEditUserForm struct { | ||||||
| 	LoginType               string `binding:"Required"` | 	LoginType               string `binding:"Required"` | ||||||
|  | 	UserName                string `binding:"AlphaDashDot;MaxSize(40)"` | ||||||
| 	LoginName               string | 	LoginName               string | ||||||
| 	FullName                string `binding:"MaxSize(100)"` | 	FullName                string `binding:"MaxSize(100)"` | ||||||
| 	Email                   string `binding:"Required;Email;MaxSize(254)"` | 	Email                   string `binding:"Required;Email;MaxSize(254)"` | ||||||
|   | |||||||
| @@ -359,6 +359,7 @@ password_not_match = The passwords do not match. | |||||||
| lang_select_error = Select a language from the list. | 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. | ||||||
| repo_name_been_taken = The repository name is already used. | repo_name_been_taken = The repository name is already used. | ||||||
| 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. | ||||||
| repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted. | repository_files_already_exist.adopt = Files already exist for this repository and can only be Adopted. | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/password" | 	"code.gitea.io/gitea/modules/password" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/routers" | 	"code.gitea.io/gitea/routers" | ||||||
|  | 	router_user_setting "code.gitea.io/gitea/routers/user/setting" | ||||||
| 	"code.gitea.io/gitea/services/mailer" | 	"code.gitea.io/gitea/services/mailer" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -269,6 +270,15 @@ func EditUserPost(ctx *context.Context, form auth.AdminEditUserForm) { | |||||||
| 		u.HashPassword(form.Password) | 		u.HashPassword(form.Password) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if len(form.UserName) != 0 && u.Name != form.UserName { | ||||||
|  | 		if err := router_user_setting.HandleUsernameChange(ctx, u, form.UserName); err != nil { | ||||||
|  | 			ctx.Redirect(setting.AppSubURL + "/admin/users") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		u.Name = form.UserName | ||||||
|  | 		u.LowerName = strings.ToLower(form.UserName) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if form.Reset2FA { | 	if form.Reset2FA { | ||||||
| 		tf, err := models.GetTwoFactorByUID(u.ID) | 		tf, err := models.GetTwoFactorByUID(u.ID) | ||||||
| 		if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { | 		if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { | ||||||
|   | |||||||
| @@ -38,42 +38,36 @@ func Profile(ctx *context.Context) { | |||||||
| 	ctx.HTML(200, tplSettingsProfile) | 	ctx.HTML(200, tplSettingsProfile) | ||||||
| } | } | ||||||
|  |  | ||||||
| func handleUsernameChange(ctx *context.Context, newName string) { | // HandleUsernameChange handle username changes from user settings and admin interface | ||||||
|  | func HandleUsernameChange(ctx *context.Context, user *models.User, newName string) error { | ||||||
| 	// Non-local users are not allowed to change their username. | 	// Non-local users are not allowed to change their username. | ||||||
| 	if len(newName) == 0 || !ctx.User.IsLocal() { | 	if !user.IsLocal() { | ||||||
| 		return | 		ctx.Flash.Error(ctx.Tr("form.username_change_not_local_user")) | ||||||
|  | 		return fmt.Errorf(ctx.Tr("form.username_change_not_local_user")) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Check if user name has been changed | 	// Check if user name has been changed | ||||||
| 	if ctx.User.LowerName != strings.ToLower(newName) { | 	if user.LowerName != strings.ToLower(newName) { | ||||||
| 		if err := models.ChangeUserName(ctx.User, newName); err != nil { | 		if err := models.ChangeUserName(user, newName); err != nil { | ||||||
| 			switch { | 			switch { | ||||||
| 			case models.IsErrUserAlreadyExist(err): | 			case models.IsErrUserAlreadyExist(err): | ||||||
| 				ctx.Flash.Error(ctx.Tr("form.username_been_taken")) | 				ctx.Flash.Error(ctx.Tr("form.username_been_taken")) | ||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings") |  | ||||||
| 			case models.IsErrEmailAlreadyUsed(err): | 			case models.IsErrEmailAlreadyUsed(err): | ||||||
| 				ctx.Flash.Error(ctx.Tr("form.email_been_used")) | 				ctx.Flash.Error(ctx.Tr("form.email_been_used")) | ||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings") |  | ||||||
| 			case models.IsErrNameReserved(err): | 			case models.IsErrNameReserved(err): | ||||||
| 				ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) | 				ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) | ||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings") |  | ||||||
| 			case models.IsErrNamePatternNotAllowed(err): | 			case models.IsErrNamePatternNotAllowed(err): | ||||||
| 				ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) | 				ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) | ||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings") |  | ||||||
| 			case models.IsErrNameCharsNotAllowed(err): | 			case models.IsErrNameCharsNotAllowed(err): | ||||||
| 				ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName)) | 				ctx.Flash.Error(ctx.Tr("user.form.name_chars_not_allowed", newName)) | ||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings") |  | ||||||
| 			default: | 			default: | ||||||
| 				ctx.ServerError("ChangeUserName", err) | 				ctx.ServerError("ChangeUserName", err) | ||||||
| 			} | 			} | ||||||
| 			return | 			return err | ||||||
| 		} | 		} | ||||||
| 		log.Trace("User name changed: %s -> %s", ctx.User.Name, newName) | 		log.Trace("User name changed: %s -> %s", user.Name, newName) | ||||||
| 	} | 	} | ||||||
|  | 	return nil | ||||||
| 	// In case it's just a case change |  | ||||||
| 	ctx.User.Name = newName |  | ||||||
| 	ctx.User.LowerName = strings.ToLower(newName) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // ProfilePost response for change user's profile | // ProfilePost response for change user's profile | ||||||
| @@ -86,9 +80,13 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	handleUsernameChange(ctx, form.Name) | 	if len(form.Name) != 0 && ctx.User.Name != form.Name { | ||||||
| 	if ctx.Written() { | 		if err := HandleUsernameChange(ctx, ctx.User, form.Name); err != nil { | ||||||
| 		return | 			ctx.Redirect(setting.AppSubURL + "/user/settings") | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		ctx.User.Name = form.Name | ||||||
|  | 		ctx.User.LowerName = strings.ToLower(form.Name) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.User.FullName = form.FullName | 	ctx.User.FullName = form.FullName | ||||||
|   | |||||||
| @@ -9,9 +9,9 @@ | |||||||
| 		<div class="ui attached segment"> | 		<div class="ui attached segment"> | ||||||
| 			<form class="ui form" action="{{.Link}}" method="post"> | 			<form class="ui form" action="{{.Link}}" method="post"> | ||||||
| 				{{.CsrfTokenHtml}} | 				{{.CsrfTokenHtml}} | ||||||
| 				<div class="inline field {{if .Err_UserName}}error{{end}}"> | 				<div class="field {{if .Err_UserName}}error{{end}}"> | ||||||
| 					<label for="user_name">{{.i18n.Tr "username"}}</label> | 					<label for="user_name">{{.i18n.Tr "username"}}</label> | ||||||
| 					<span>{{.User.Name}}</span> | 					<input id="user_name" name="user_name" value="{{.User.Name}}" autofocus {{if not .User.IsLocal }}disabled{{end}}> | ||||||
| 				</div> | 				</div> | ||||||
| 				<!-- Types and name --> | 				<!-- Types and name --> | ||||||
| 				<div class="inline required field {{if .Err_LoginType}}error{{end}}"> | 				<div class="inline required field {{if .Err_LoginType}}error{{end}}"> | ||||||
|   | |||||||
| @@ -1796,6 +1796,7 @@ function initAdmin() { | |||||||
|   if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) { |   if ($('.admin.new.user').length > 0 || $('.admin.edit.user').length > 0) { | ||||||
|     $('#login_type').on('change', function () { |     $('#login_type').on('change', function () { | ||||||
|       if ($(this).val().substring(0, 1) === '0') { |       if ($(this).val().substring(0, 1) === '0') { | ||||||
|  |         $('#user_name').removeAttr('disabled'); | ||||||
|         $('#login_name').removeAttr('required'); |         $('#login_name').removeAttr('required'); | ||||||
|         $('.non-local').hide(); |         $('.non-local').hide(); | ||||||
|         $('.local').show(); |         $('.local').show(); | ||||||
| @@ -1805,6 +1806,7 @@ function initAdmin() { | |||||||
|           $('#password').attr('required', 'required'); |           $('#password').attr('required', 'required'); | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|  |         $('#user_name').attr('disabled', 'disabled'); | ||||||
|         $('#login_name').attr('required', 'required'); |         $('#login_name').attr('required', 'required'); | ||||||
|         $('.non-local').show(); |         $('.non-local').show(); | ||||||
|         $('.local').hide(); |         $('.local').hide(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user