* redirect to login page after successfully activating account * force users to change password if account was created by an admin * force users to change password if account was created by an admin * fixed build * fixed build * fix pending issues with translation and wrong routes * make sure path check is safe * remove unneccessary newline * make sure users that don't have to view the form get redirected * move route to use /settings prefix so as to make sure unauthenticated users can't view the page * update as per @lafriks review * add necessary comment * remove unrelated changes * support redirecting to location the user actually want to go to before being forced to change his/her password * run make fmt * added tests * improve assertions * add assertion * fix copyright year Signed-off-by: Lanre Adelowo <yo@lanre.wtf>tags/v1.21.12.1
| @@ -198,6 +198,8 @@ var migrations = []Migration{ | |||||
| NewMigration("protect each scratch token", addScratchHash), | NewMigration("protect each scratch token", addScratchHash), | ||||
| // v72 -> v73 | // v72 -> v73 | ||||
| NewMigration("add review", addReview), | NewMigration("add review", addReview), | ||||
| // v73 -> v74 | |||||
| NewMigration("add must_change_password column for users table", addMustChangePassword), | |||||
| } | } | ||||
| // Migrate database to current version | // Migrate database to current version | ||||
| @@ -0,0 +1,19 @@ | |||||
| // Copyright 2018 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 ( | |||||
| "github.com/go-xorm/xorm" | |||||
| ) | |||||
| func addMustChangePassword(x *xorm.Engine) error { | |||||
| // User see models/user.go | |||||
| type User struct { | |||||
| ID int64 `xorm:"pk autoincr"` | |||||
| MustChangePassword bool `xorm:"NOT NULL DEFAULT false"` | |||||
| } | |||||
| return x.Sync2(new(User)) | |||||
| } | |||||
| @@ -83,18 +83,23 @@ type User struct { | |||||
| Email string `xorm:"NOT NULL"` | Email string `xorm:"NOT NULL"` | ||||
| KeepEmailPrivate bool | KeepEmailPrivate bool | ||||
| Passwd string `xorm:"NOT NULL"` | Passwd string `xorm:"NOT NULL"` | ||||
| LoginType LoginType | |||||
| LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` | |||||
| LoginName string | |||||
| Type UserType | |||||
| OwnedOrgs []*User `xorm:"-"` | |||||
| Orgs []*User `xorm:"-"` | |||||
| Repos []*Repository `xorm:"-"` | |||||
| Location string | |||||
| Website string | |||||
| Rands string `xorm:"VARCHAR(10)"` | |||||
| Salt string `xorm:"VARCHAR(10)"` | |||||
| Language string `xorm:"VARCHAR(5)"` | |||||
| // MustChangePassword is an attribute that determines if a user | |||||
| // is to change his/her password after registration. | |||||
| MustChangePassword bool `xorm:"NOT NULL DEFAULT false"` | |||||
| LoginType LoginType | |||||
| LoginSource int64 `xorm:"NOT NULL DEFAULT 0"` | |||||
| LoginName string | |||||
| Type UserType | |||||
| OwnedOrgs []*User `xorm:"-"` | |||||
| Orgs []*User `xorm:"-"` | |||||
| Repos []*Repository `xorm:"-"` | |||||
| Location string | |||||
| Website string | |||||
| Rands string `xorm:"VARCHAR(10)"` | |||||
| Salt string `xorm:"VARCHAR(10)"` | |||||
| Language string `xorm:"VARCHAR(5)"` | |||||
| CreatedUnix util.TimeStamp `xorm:"INDEX created"` | CreatedUnix util.TimeStamp `xorm:"INDEX created"` | ||||
| UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | UpdatedUnix util.TimeStamp `xorm:"INDEX updated"` | ||||
| @@ -84,6 +84,18 @@ func (f *RegisterForm) Validate(ctx *macaron.Context, errs binding.Errors) bindi | |||||
| return validate(errs, ctx.Data, f, ctx.Locale) | return validate(errs, ctx.Data, f, ctx.Locale) | ||||
| } | } | ||||
| // MustChangePasswordForm form for updating your password after account creation | |||||
| // by an admin | |||||
| type MustChangePasswordForm struct { | |||||
| Password string `binding:"Required;MaxSize(255)"` | |||||
| Retype string | |||||
| } | |||||
| // Validate valideates the fields | |||||
| func (f *MustChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||||
| } | |||||
| // SignInForm form for signing in with user/password | // SignInForm form for signing in with user/password | ||||
| type SignInForm struct { | type SignInForm struct { | ||||
| UserName string `binding:"Required;MaxSize(254)"` | UserName string `binding:"Required;MaxSize(254)"` | ||||
| @@ -31,10 +31,31 @@ func Toggle(options *ToggleOptions) macaron.Handler { | |||||
| } | } | ||||
| // Check prohibit login users. | // Check prohibit login users. | ||||
| if ctx.IsSigned && ctx.User.ProhibitLogin { | |||||
| ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") | |||||
| ctx.HTML(200, "user/auth/prohibit_login") | |||||
| return | |||||
| if ctx.IsSigned { | |||||
| if ctx.User.ProhibitLogin { | |||||
| ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") | |||||
| ctx.HTML(200, "user/auth/prohibit_login") | |||||
| return | |||||
| } | |||||
| // prevent infinite redirection | |||||
| // also make sure that the form cannot be accessed by | |||||
| // users who don't need this | |||||
| if ctx.Req.URL.Path == setting.AppSubURL+"/user/settings/change_password" { | |||||
| if !ctx.User.MustChangePassword { | |||||
| ctx.Redirect(setting.AppSubURL + "/") | |||||
| } | |||||
| return | |||||
| } | |||||
| if ctx.User.MustChangePassword { | |||||
| ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | |||||
| ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/change_password" | |||||
| ctx.SetCookie("redirect_to", url.QueryEscape(setting.AppSubURL+ctx.Req.RequestURI), 0, setting.AppSubURL) | |||||
| ctx.Redirect(setting.AppSubURL + "/user/settings/change_password") | |||||
| return | |||||
| } | |||||
| } | } | ||||
| // Redirect to dashboard if user tries to visit any non-login page. | // Redirect to dashboard if user tries to visit any non-login page. | ||||
| @@ -205,6 +205,7 @@ forgot_password = Forgot password? | |||||
| sign_up_now = Need an account? Register now. | sign_up_now = Need an account? Register now. | ||||
| sign_up_successful = Account was successfully created. | sign_up_successful = Account was successfully created. | ||||
| confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. | confirmation_mail_sent_prompt = A new confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the registration process. | ||||
| must_change_password = Update your password | |||||
| reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the password reset process. | reset_password_mail_sent_prompt = A confirmation email has been sent to <b>%s</b>. Please check your inbox within the next %s to complete the password reset process. | ||||
| active_your_account = Activate Your Account | active_your_account = Activate Your Account | ||||
| account_activated = Account has been activated | account_activated = Account has been activated | ||||
| @@ -0,0 +1,16 @@ | |||||
| // Copyright 2018 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 admin | |||||
| import ( | |||||
| "path/filepath" | |||||
| "testing" | |||||
| "code.gitea.io/gitea/models" | |||||
| ) | |||||
| func TestMain(m *testing.M) { | |||||
| models.MainTest(m, filepath.Join("..", "..")) | |||||
| } | |||||
| @@ -77,11 +77,12 @@ func NewUserPost(ctx *context.Context, form auth.AdminCreateUserForm) { | |||||
| } | } | ||||
| u := &models.User{ | u := &models.User{ | ||||
| Name: form.UserName, | |||||
| Email: form.Email, | |||||
| Passwd: form.Password, | |||||
| IsActive: true, | |||||
| LoginType: models.LoginPlain, | |||||
| Name: form.UserName, | |||||
| Email: form.Email, | |||||
| Passwd: form.Password, | |||||
| IsActive: true, | |||||
| LoginType: models.LoginPlain, | |||||
| MustChangePassword: true, | |||||
| } | } | ||||
| if len(form.LoginType) > 0 { | if len(form.LoginType) > 0 { | ||||
| @@ -0,0 +1,50 @@ | |||||
| // Copyright 2017 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 admin | |||||
| import ( | |||||
| "testing" | |||||
| "code.gitea.io/gitea/models" | |||||
| "code.gitea.io/gitea/modules/auth" | |||||
| "code.gitea.io/gitea/modules/test" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func TestNewUserPost_MustChangePassword(t *testing.T) { | |||||
| models.PrepareTestEnv(t) | |||||
| ctx := test.MockContext(t, "admin/users/new") | |||||
| u := models.AssertExistsAndLoadBean(t, &models.User{ | |||||
| IsAdmin: true, | |||||
| ID: 2, | |||||
| }).(*models.User) | |||||
| ctx.User = u | |||||
| username := "gitea" | |||||
| email := "gitea@gitea.io" | |||||
| form := auth.AdminCreateUserForm{ | |||||
| LoginType: "local", | |||||
| LoginName: "local", | |||||
| UserName: username, | |||||
| Email: email, | |||||
| Password: "xxxxxxxx", | |||||
| SendNotify: false, | |||||
| } | |||||
| NewUserPost(ctx, form) | |||||
| assert.NotEmpty(t, ctx.Flash.SuccessMsg) | |||||
| u, err := models.GetUserByName(username) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, username, u.Name) | |||||
| assert.Equal(t, email, u.Email) | |||||
| assert.True(t, u.MustChangePassword) | |||||
| } | |||||
| @@ -230,6 +230,8 @@ func RegisterRoutes(m *macaron.Macaron) { | |||||
| m.Group("/user/settings", func() { | m.Group("/user/settings", func() { | ||||
| m.Get("", userSetting.Profile) | m.Get("", userSetting.Profile) | ||||
| m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) | ||||
| m.Get("/change_password", user.MustChangePassword) | |||||
| m.Post("/change_password", bindIgnErr(auth.MustChangePasswordForm{}), user.MustChangePasswordPost) | |||||
| m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) | m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) | ||||
| m.Post("/avatar/delete", userSetting.DeleteAvatar) | m.Post("/avatar/delete", userSetting.DeleteAvatar) | ||||
| m.Group("/account", func() { | m.Group("/account", func() { | ||||
| @@ -28,6 +28,8 @@ import ( | |||||
| ) | ) | ||||
| const ( | const ( | ||||
| // tplMustChangePassword template for updating a user's password | |||||
| tplMustChangePassword = "user/auth/change_passwd" | |||||
| // tplSignIn template for sign in page | // tplSignIn template for sign in page | ||||
| tplSignIn base.TplName = "user/auth/signin" | tplSignIn base.TplName = "user/auth/signin" | ||||
| // tplSignUp template path for sign up page | // tplSignUp template path for sign up page | ||||
| @@ -1178,7 +1180,8 @@ func ResetPasswdPost(ctx *context.Context) { | |||||
| return | return | ||||
| } | } | ||||
| u.HashPassword(passwd) | u.HashPassword(passwd) | ||||
| if err := models.UpdateUserCols(u, "passwd", "rands", "salt"); err != nil { | |||||
| u.MustChangePassword = false | |||||
| if err := models.UpdateUserCols(u, "must_change_password", "passwd", "rands", "salt"); err != nil { | |||||
| ctx.ServerError("UpdateUser", err) | ctx.ServerError("UpdateUser", err) | ||||
| return | return | ||||
| } | } | ||||
| @@ -1191,3 +1194,71 @@ func ResetPasswdPost(ctx *context.Context) { | |||||
| ctx.Data["IsResetFailed"] = true | ctx.Data["IsResetFailed"] = true | ||||
| ctx.HTML(200, tplResetPassword) | ctx.HTML(200, tplResetPassword) | ||||
| } | } | ||||
| // MustChangePassword renders the page to change a user's password | |||||
| func MustChangePassword(ctx *context.Context) { | |||||
| ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | |||||
| ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" | |||||
| ctx.HTML(200, tplMustChangePassword) | |||||
| } | |||||
| // MustChangePasswordPost response for updating a user's password after his/her | |||||
| // account was created by an admin | |||||
| func MustChangePasswordPost(ctx *context.Context, cpt *captcha.Captcha, form auth.MustChangePasswordForm) { | |||||
| ctx.Data["Title"] = ctx.Tr("auth.must_change_password") | |||||
| ctx.Data["ChangePasscodeLink"] = setting.AppSubURL + "/user/settings/change_password" | |||||
| if ctx.HasError() { | |||||
| ctx.HTML(200, tplMustChangePassword) | |||||
| return | |||||
| } | |||||
| u := ctx.User | |||||
| // Make sure only requests for users who are eligible to change their password via | |||||
| // this method passes through | |||||
| if !u.MustChangePassword { | |||||
| ctx.ServerError("MustUpdatePassword", errors.New("cannot update password.. Please visit the settings page")) | |||||
| return | |||||
| } | |||||
| if form.Password != form.Retype { | |||||
| ctx.Data["Err_Password"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("form.password_not_match"), tplMustChangePassword, &form) | |||||
| return | |||||
| } | |||||
| if len(form.Password) < setting.MinPasswordLength { | |||||
| ctx.Data["Err_Password"] = true | |||||
| ctx.RenderWithErr(ctx.Tr("auth.password_too_short", setting.MinPasswordLength), tplMustChangePassword, &form) | |||||
| return | |||||
| } | |||||
| var err error | |||||
| if u.Salt, err = models.GetUserSalt(); err != nil { | |||||
| ctx.ServerError("UpdateUser", err) | |||||
| return | |||||
| } | |||||
| u.HashPassword(form.Password) | |||||
| u.MustChangePassword = false | |||||
| if err := models.UpdateUserCols(u, "must_change_password", "passwd", "salt"); err != nil { | |||||
| ctx.ServerError("UpdateUser", err) | |||||
| return | |||||
| } | |||||
| ctx.Flash.Success(ctx.Tr("settings.change_password_success")) | |||||
| log.Trace("User updated password: %s", u.Name) | |||||
| if redirectTo, _ := url.QueryUnescape(ctx.GetCookie("redirect_to")); len(redirectTo) > 0 && !util.IsExternalURL(redirectTo) { | |||||
| ctx.SetCookie("redirect_to", "", -1, setting.AppSubURL) | |||||
| ctx.RedirectToFirst(redirectTo) | |||||
| return | |||||
| } | |||||
| ctx.Redirect(setting.AppSubURL + "/") | |||||
| } | |||||
| @@ -0,0 +1,7 @@ | |||||
| {{template "base/head" .}} | |||||
| <div class="user signin{{if .LinkAccountMode}} icon{{end}}"> | |||||
| <div class="ui container"> | |||||
| {{template "user/auth/change_passwd_inner" .}} | |||||
| </div> | |||||
| </div> | |||||
| {{template "base/footer" .}} | |||||
| @@ -0,0 +1,26 @@ | |||||
| {{if or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn)}} | |||||
| {{template "base/alert" .}} | |||||
| {{end}} | |||||
| <h4 class="ui top attached header center"> | |||||
| {{.i18n.Tr "settings.change_password"}} | |||||
| </h4> | |||||
| <div class="ui attached segment"> | |||||
| <form class="ui form" action="{{.ChangePasscodeLink}}" method="post"> | |||||
| {{.CsrfTokenHtml}} | |||||
| <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeSignIn))}}error{{end}}"> | |||||
| <label for="password">{{.i18n.Tr "password"}}</label> | |||||
| <input id="password" name="password" type="password" value="{{.password}}" autocomplete="off" required> | |||||
| </div> | |||||
| <div class="required inline field {{if and (.Err_Password) (or (not .LinkAccountMode) (and .LinkAccountMode .LinkAccountModeRegister))}}error{{end}}"> | |||||
| <label for="retype">{{.i18n.Tr "re_type"}}</label> | |||||
| <input id="retype" name="retype" type="password" autocomplete="off" required> | |||||
| </div> | |||||
| <div class="inline field"> | |||||
| <label></label> | |||||
| <button class="ui green button">{{.i18n.Tr "settings.change_password" }}</button> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||