Co-authored-by: Bwko <bouwko@gmail.com> Co-authored-by: techknowlogick <matti@mdranta.net> Co-authored-by: zeripath <art27@cantab.net>tags/v1.15.0-dev
@@ -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() { | |||||
return | |||||
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")) | |||||
} | } | ||||
// Check if user name has been changed | // Check if user name has been changed | ||||
if ctx.User.LowerName != strings.ToLower(newName) { | |||||
if err := models.ChangeUserName(ctx.User, newName); err != nil { | |||||
if user.LowerName != strings.ToLower(newName) { | |||||
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) | |||||
} | } | ||||
// In case it's just a case change | |||||
ctx.User.Name = newName | |||||
ctx.User.LowerName = strings.ToLower(newName) | |||||
return nil | |||||
} | } | ||||
// 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 ctx.Written() { | |||||
return | |||||
if len(form.Name) != 0 && ctx.User.Name != form.Name { | |||||
if err := HandleUsernameChange(ctx, ctx.User, form.Name); err != nil { | |||||
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(); | ||||