From 2aabfc1afa8b808374c76bdb20b936847ba50c86 Mon Sep 17 00:00:00 2001 From: David Schneiderbauer Date: Thu, 17 May 2018 06:05:00 +0200 Subject: [PATCH] Splitted the user settings code into several files to be more maintainable (#3968) * refactor setting router code splitted up one huge router settings file into the smaller files representing the actual page structure * move code to subfolder * rename functions * renamed files * add copyright information --- routers/org/setting.go | 4 +- routers/routes/routes.go | 51 +- routers/user/setting.go | 808 ------------------ routers/user/setting/account.go | 174 ++++ .../account_test.go} | 6 +- routers/user/setting/applications.go | 77 ++ routers/user/setting/keys.go | 149 ++++ routers/user/setting/main_test.go | 16 + routers/user/setting/profile.go | 220 +++++ routers/user/setting/security.go | 92 ++ .../security_openid.go} | 8 +- routers/user/setting/security_twofa.go | 187 ++++ 12 files changed, 950 insertions(+), 842 deletions(-) delete mode 100644 routers/user/setting.go create mode 100644 routers/user/setting/account.go rename routers/user/{setting_test.go => setting/account_test.go} (91%) create mode 100644 routers/user/setting/applications.go create mode 100644 routers/user/setting/keys.go create mode 100644 routers/user/setting/main_test.go create mode 100644 routers/user/setting/profile.go create mode 100644 routers/user/setting/security.go rename routers/user/{setting_openid.go => setting/security_openid.go} (94%) create mode 100644 routers/user/setting/security_twofa.go diff --git a/routers/org/setting.go b/routers/org/setting.go index 937697d07..7f652c11d 100644 --- a/routers/org/setting.go +++ b/routers/org/setting.go @@ -13,7 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/routers/user" + userSetting "code.gitea.io/gitea/routers/user/setting" ) const ( @@ -91,7 +91,7 @@ func SettingsPost(ctx *context.Context, form auth.UpdateOrgSettingForm) { // SettingsAvatar response for change avatar on settings page func SettingsAvatar(ctx *context.Context, form auth.AvatarForm) { form.Source = auth.AvatarLocal - if err := user.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { + if err := userSetting.UpdateAvatarSetting(ctx, form, ctx.Org.Organization); err != nil { ctx.Flash.Error(err.Error()) } else { ctx.Flash.Success(ctx.Tr("org.settings.update_avatar_success")) diff --git a/routers/routes/routes.go b/routers/routes/routes.go index 40b5f4bfb..07be6653a 100644 --- a/routers/routes/routes.go +++ b/routers/routes/routes.go @@ -27,6 +27,7 @@ import ( "code.gitea.io/gitea/routers/private" "code.gitea.io/gitea/routers/repo" "code.gitea.io/gitea/routers/user" + userSetting "code.gitea.io/gitea/routers/user/setting" "github.com/go-macaron/binding" "github.com/go-macaron/cache" @@ -216,39 +217,39 @@ func RegisterRoutes(m *macaron.Macaron) { }, reqSignOut) m.Group("/user/settings", func() { - m.Get("", user.Settings) - m.Post("", bindIgnErr(auth.UpdateProfileForm{}), user.SettingsPost) - m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), user.SettingsAvatarPost) - m.Post("/avatar/delete", user.SettingsDeleteAvatar) + m.Get("", userSetting.Profile) + m.Post("", bindIgnErr(auth.UpdateProfileForm{}), userSetting.ProfilePost) + m.Post("/avatar", binding.MultipartForm(auth.AvatarForm{}), userSetting.AvatarPost) + m.Post("/avatar/delete", userSetting.DeleteAvatar) m.Group("/account", func() { - m.Combo("").Get(user.SettingsAccount).Post(bindIgnErr(auth.ChangePasswordForm{}), user.SettingsAccountPost) - m.Post("/email", bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost) - m.Post("/email/delete", user.DeleteEmail) - m.Post("/delete", user.SettingsDelete) + m.Combo("").Get(userSetting.Account).Post(bindIgnErr(auth.ChangePasswordForm{}), userSetting.AccountPost) + m.Post("/email", bindIgnErr(auth.AddEmailForm{}), userSetting.EmailPost) + m.Post("/email/delete", userSetting.DeleteEmail) + m.Post("/delete", userSetting.DeleteAccount) }) m.Group("/security", func() { - m.Get("", user.SettingsSecurity) + m.Get("", userSetting.Security) m.Group("/two_factor", func() { - m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch) - m.Post("/disable", user.SettingsTwoFactorDisable) - m.Get("/enroll", user.SettingsTwoFactorEnroll) - m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), user.SettingsTwoFactorEnrollPost) + m.Post("/regenerate_scratch", userSetting.RegenerateScratchTwoFactor) + m.Post("/disable", userSetting.DisableTwoFactor) + m.Get("/enroll", userSetting.EnrollTwoFactor) + m.Post("/enroll", bindIgnErr(auth.TwoFactorAuthForm{}), userSetting.EnrollTwoFactorPost) }) m.Group("/openid", func() { - m.Post("", bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost) - m.Post("/delete", user.DeleteOpenID) - m.Post("/toggle_visibility", user.ToggleOpenIDVisibility) + m.Post("", bindIgnErr(auth.AddOpenIDForm{}), userSetting.OpenIDPost) + m.Post("/delete", userSetting.DeleteOpenID) + m.Post("/toggle_visibility", userSetting.ToggleOpenIDVisibility) }, openIDSignInEnabled) - m.Post("/account_link", user.SettingsDeleteAccountLink) + m.Post("/account_link", userSetting.DeleteAccountLink) }) - m.Combo("/applications").Get(user.SettingsApplications). - Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost) - m.Post("/applications/delete", user.SettingsDeleteApplication) - m.Combo("/keys").Get(user.SettingsKeys). - Post(bindIgnErr(auth.AddKeyForm{}), user.SettingsKeysPost) - m.Post("/keys/delete", user.DeleteKey) - m.Get("/organization", user.SettingsOrganization) - m.Get("/repos", user.SettingsRepos) + m.Combo("/applications").Get(userSetting.Applications). + Post(bindIgnErr(auth.NewAccessTokenForm{}), userSetting.ApplicationsPost) + m.Post("/applications/delete", userSetting.DeleteApplication) + m.Combo("/keys").Get(userSetting.Keys). + Post(bindIgnErr(auth.AddKeyForm{}), userSetting.KeysPost) + m.Post("/keys/delete", userSetting.DeleteKey) + m.Get("/organization", userSetting.Organization) + m.Get("/repos", userSetting.Repos) // redirects from old settings urls to new ones // TODO: can be removed on next major version diff --git a/routers/user/setting.go b/routers/user/setting.go deleted file mode 100644 index 1c760e210..000000000 --- a/routers/user/setting.go +++ /dev/null @@ -1,808 +0,0 @@ -// Copyright 2014 The Gogs 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 user - -import ( - "bytes" - "errors" - "fmt" - "io/ioutil" - "strings" - - "github.com/Unknwon/com" - "github.com/Unknwon/i18n" - "github.com/pquerna/otp" - "github.com/pquerna/otp/totp" - - "encoding/base64" - "html/template" - "image/png" - - "code.gitea.io/gitea/models" - "code.gitea.io/gitea/modules/auth" - "code.gitea.io/gitea/modules/base" - "code.gitea.io/gitea/modules/context" - "code.gitea.io/gitea/modules/log" - "code.gitea.io/gitea/modules/setting" -) - -const ( - tplSettingsProfile base.TplName = "user/settings/profile" - tplSettingsAccount base.TplName = "user/settings/account" - tplSettingsSecurity base.TplName = "user/settings/security" - tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" - tplSettingsApplications base.TplName = "user/settings/applications" - tplSettingsKeys base.TplName = "user/settings/keys" - tplSettingsOrganization base.TplName = "user/settings/organization" - tplSettingsRepositories base.TplName = "user/settings/repos" -) - -// Settings render user's profile page -func Settings(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsProfile"] = true - ctx.HTML(200, tplSettingsProfile) -} - -func handleUsernameChange(ctx *context.Context, newName string) { - // Non-local users are not allowed to change their username. - if len(newName) == 0 || !ctx.User.IsLocal() { - return - } - - // Check if user name has been changed - if ctx.User.LowerName != strings.ToLower(newName) { - if err := models.ChangeUserName(ctx.User, newName); err != nil { - switch { - case models.IsErrUserAlreadyExist(err): - ctx.Flash.Error(ctx.Tr("form.username_been_taken")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrEmailAlreadyUsed(err): - ctx.Flash.Error(ctx.Tr("form.email_been_used")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrNameReserved(err): - ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) - ctx.Redirect(setting.AppSubURL + "/user/settings") - case models.IsErrNamePatternNotAllowed(err): - ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) - ctx.Redirect(setting.AppSubURL + "/user/settings") - default: - ctx.ServerError("ChangeUserName", err) - } - return - } - log.Trace("User name changed: %s -> %s", ctx.User.Name, newName) - } - - // In case it's just a case change - ctx.User.Name = newName - ctx.User.LowerName = strings.ToLower(newName) -} - -// SettingsPost response for change user's profile -func SettingsPost(ctx *context.Context, form auth.UpdateProfileForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsProfile"] = true - - if ctx.HasError() { - ctx.HTML(200, tplSettingsProfile) - return - } - - handleUsernameChange(ctx, form.Name) - if ctx.Written() { - return - } - - ctx.User.FullName = form.FullName - ctx.User.Email = form.Email - ctx.User.KeepEmailPrivate = form.KeepEmailPrivate - ctx.User.Website = form.Website - ctx.User.Location = form.Location - ctx.User.Language = form.Language - if err := models.UpdateUserSetting(ctx.User); err != nil { - if _, ok := err.(models.ErrEmailAlreadyUsed); ok { - ctx.Flash.Error(ctx.Tr("form.email_been_used")) - ctx.Redirect(setting.AppSubURL + "/user/settings") - return - } - ctx.ServerError("UpdateUser", err) - return - } - - // Update the language to the one we just set - ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL) - - log.Trace("User settings updated: %s", ctx.User.Name) - ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success")) - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// UpdateAvatarSetting update user's avatar -// FIXME: limit size. -func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error { - ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal - if len(form.Gravatar) > 0 { - ctxUser.Avatar = base.EncodeMD5(form.Gravatar) - ctxUser.AvatarEmail = form.Gravatar - } - - if form.Avatar != nil { - fr, err := form.Avatar.Open() - if err != nil { - return fmt.Errorf("Avatar.Open: %v", err) - } - defer fr.Close() - - data, err := ioutil.ReadAll(fr) - if err != nil { - return fmt.Errorf("ioutil.ReadAll: %v", err) - } - if !base.IsImageFile(data) { - return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) - } - if err = ctxUser.UploadAvatar(data); err != nil { - return fmt.Errorf("UploadAvatar: %v", err) - } - } else { - // No avatar is uploaded but setting has been changed to enable, - // generate a random one when needed. - if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { - if err := ctxUser.GenerateRandomAvatar(); err != nil { - log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) - } - } - } - - if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { - return fmt.Errorf("UpdateUser: %v", err) - } - - return nil -} - -// SettingsAvatarPost response for change user's avatar request -func SettingsAvatarPost(ctx *context.Context, form auth.AvatarForm) { - if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { - ctx.Flash.Error(err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// SettingsDeleteAvatar render delete avatar page -func SettingsDeleteAvatar(ctx *context.Context) { - if err := ctx.User.DeleteAvatar(); err != nil { - ctx.Flash.Error(err.Error()) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings") -} - -// SettingsAccount renders change user's password, user's email and user suicide page -func SettingsAccount(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - ctx.Data["Email"] = ctx.User.Email - - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails - - ctx.HTML(200, tplSettingsAccount) -} - -// SettingsAccountPost response for change user's password -func SettingsAccountPost(ctx *context.Context, form auth.ChangePasswordForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - if ctx.HasError() { - ctx.HTML(200, tplSettingsAccount) - return - } - - if len(form.Password) < setting.MinPasswordLength { - ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength)) - } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) { - ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) - } else if form.Password != form.Retype { - ctx.Flash.Error(ctx.Tr("form.password_not_match")) - } else { - var err error - if ctx.User.Salt, err = models.GetUserSalt(); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - ctx.User.HashPassword(form.Password) - if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil { - ctx.ServerError("UpdateUser", err) - return - } - log.Trace("User password updated: %s", ctx.User.Name) - ctx.Flash.Success(ctx.Tr("settings.change_password_success")) - } - - ctx.Redirect(setting.AppSubURL + "/user/settings/account") -} - -// SettingsEmailPost response for change user's email -func SettingsEmailPost(ctx *context.Context, form auth.AddEmailForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - // Make emailaddress primary. - if ctx.Query("_method") == "PRIMARY" { - if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil { - ctx.ServerError("MakeEmailPrimary", err) - return - } - - log.Trace("Email made primary: %s", ctx.User.Name) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - return - } - - // Add Email address. - emails, err := models.GetEmailAddresses(ctx.User.ID) - if err != nil { - ctx.ServerError("GetEmailAddresses", err) - return - } - ctx.Data["Emails"] = emails - - if ctx.HasError() { - ctx.HTML(200, tplSettingsAccount) - return - } - - email := &models.EmailAddress{ - UID: ctx.User.ID, - Email: form.Email, - IsActivated: !setting.Service.RegisterEmailConfirm, - } - if err := models.AddEmailAddress(email); err != nil { - if models.IsErrEmailAlreadyUsed(err) { - ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) - return - } - ctx.ServerError("AddEmailAddress", err) - return - } - - // Send confirmation email - if setting.Service.RegisterEmailConfirm { - models.SendActivateEmailMail(ctx.Context, ctx.User, email) - - if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { - log.Error(4, "Set cache(MailResendLimit) fail: %v", err) - } - ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language()))) - } else { - ctx.Flash.Success(ctx.Tr("settings.add_email_success")) - } - - log.Trace("Email address added: %s", email.Email) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") -} - -// DeleteEmail response for delete user's email -func DeleteEmail(ctx *context.Context) { - if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { - ctx.ServerError("DeleteEmail", err) - return - } - log.Trace("Email address deleted: %s", ctx.User.Name) - - ctx.Flash.Success(ctx.Tr("settings.email_deletion_success")) - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/account", - }) -} - -// SettingsDelete render user suicide page and response for delete user himself -func SettingsDelete(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsAccount"] = true - - if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { - if models.IsErrUserNotExist(err) { - ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) - } else { - ctx.ServerError("UserSignIn", err) - } - return - } - - if err := models.DeleteUser(ctx.User); err != nil { - switch { - case models.IsErrUserOwnRepos(err): - ctx.Flash.Error(ctx.Tr("form.still_own_repo")) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - case models.IsErrUserHasOrgs(err): - ctx.Flash.Error(ctx.Tr("form.still_has_org")) - ctx.Redirect(setting.AppSubURL + "/user/settings/account") - default: - ctx.ServerError("DeleteUser", err) - } - } else { - log.Trace("Account deleted: %s", ctx.User.Name) - ctx.Redirect(setting.AppSubURL + "/") - } -} - -// SettingsSecurity render change user's password page and 2FA -func SettingsSecurity(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - enrolled := true - _, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - if models.IsErrTwoFactorNotEnrolled(err) { - enrolled = false - } else { - ctx.ServerError("SettingsTwoFactor", err) - return - } - } - ctx.Data["TwofaEnrolled"] = enrolled - - accountLinks, err := models.ListAccountLinks(ctx.User) - if err != nil { - ctx.ServerError("ListAccountLinks", err) - return - } - - // map the provider display name with the LoginSource - sources := make(map[*models.LoginSource]string) - for _, externalAccount := range accountLinks { - if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { - var providerDisplayName string - if loginSource.IsOAuth2() { - providerTechnicalName := loginSource.OAuth2().Provider - providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName - } else { - providerDisplayName = loginSource.Name - } - sources[loginSource] = providerDisplayName - } - } - ctx.Data["AccountLinks"] = sources - - if ctx.Query("openid.return_to") != "" { - settingsOpenIDVerify(ctx) - return - } - - openid, err := models.GetUserOpenIDs(ctx.User.ID) - if err != nil { - ctx.ServerError("GetUserOpenIDs", err) - return - } - ctx.Data["OpenIDs"] = openid - - ctx.HTML(200, tplSettingsSecurity) -} - -// SettingsDeleteAccountLink delete a single account link -func SettingsDeleteAccountLink(ctx *context.Context) { - if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { - ctx.Flash.Error("RemoveAccountLink: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) - } - - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/security", - }) -} - -// SettingsApplications render manage access token page -func SettingsApplications(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsApplications"] = true - - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens - - ctx.HTML(200, tplSettingsApplications) -} - -// SettingsApplicationsPost response for add user's access token -func SettingsApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsApplications"] = true - - if ctx.HasError() { - tokens, err := models.ListAccessTokens(ctx.User.ID) - if err != nil { - ctx.ServerError("ListAccessTokens", err) - return - } - ctx.Data["Tokens"] = tokens - ctx.HTML(200, tplSettingsApplications) - return - } - - t := &models.AccessToken{ - UID: ctx.User.ID, - Name: form.Name, - } - if err := models.NewAccessToken(t); err != nil { - ctx.ServerError("NewAccessToken", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) - ctx.Flash.Info(t.Sha1) - - ctx.Redirect(setting.AppSubURL + "/user/settings/applications") -} - -// SettingsDeleteApplication response for delete user access token -func SettingsDeleteApplication(ctx *context.Context) { - if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil { - ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) - } - - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/applications", - }) -} - -// SettingsKeys render user's SSH/GPG public keys page -func SettingsKeys(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsKeys"] = true - ctx.Data["DisableSSH"] = setting.SSH.Disabled - - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys - - ctx.HTML(200, tplSettingsKeys) -} - -// SettingsKeysPost response for change user's SSH/GPG keys -func SettingsKeysPost(ctx *context.Context, form auth.AddKeyForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsKeys"] = true - - keys, err := models.ListPublicKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListPublicKeys", err) - return - } - ctx.Data["Keys"] = keys - - gpgkeys, err := models.ListGPGKeys(ctx.User.ID) - if err != nil { - ctx.ServerError("ListGPGKeys", err) - return - } - ctx.Data["GPGKeys"] = gpgkeys - - if ctx.HasError() { - ctx.HTML(200, tplSettingsKeys) - return - } - switch form.Type { - case "gpg": - key, err := models.AddGPGKey(ctx.User.ID, form.Content) - if err != nil { - ctx.Data["HasGPGError"] = true - switch { - case models.IsErrGPGKeyParsing(err): - ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error())) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - case models.IsErrGPGKeyIDAlreadyUsed(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) - case models.IsErrGPGNoEmailFound(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) - default: - ctx.ServerError("AddPublicKey", err) - } - return - } - ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID)) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - case "ssh": - content, err := models.CheckPublicKeyString(form.Content) - if err != nil { - if models.IsErrSSHDisabled(err) { - ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) - } else if models.IsErrKeyUnableVerify(err) { - ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) - } else { - ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) - } - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - return - } - - if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil { - ctx.Data["HasSSHError"] = true - switch { - case models.IsErrKeyAlreadyExist(err): - ctx.Data["Err_Content"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) - case models.IsErrKeyNameAlreadyUsed(err): - ctx.Data["Err_Title"] = true - ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) - default: - ctx.ServerError("AddPublicKey", err) - } - return - } - ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - - default: - ctx.Flash.Warning("Function not implemented") - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - } - -} - -// DeleteKey response for delete user's SSH/GPG key -func DeleteKey(ctx *context.Context) { - - switch ctx.Query("type") { - case "gpg": - if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil { - ctx.Flash.Error("DeleteGPGKey: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) - } - case "ssh": - if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil { - ctx.Flash.Error("DeletePublicKey: " + err.Error()) - } else { - ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success")) - } - default: - ctx.Flash.Warning("Function not implemented") - ctx.Redirect(setting.AppSubURL + "/user/settings/keys") - } - ctx.JSON(200, map[string]interface{}{ - "redirect": setting.AppSubURL + "/user/settings/keys", - }) -} - -// SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code. -func SettingsTwoFactorRegenerateScratch(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = t.GenerateScratchToken(); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.UpdateTwoFactor(t); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -// SettingsTwoFactorDisable deletes the user's 2FA settings. -func SettingsTwoFactorDisable(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -func twofaGenerateSecretAndQr(ctx *context.Context) bool { - var otpKey *otp.Key - var err error - uri := ctx.Session.Get("twofaUri") - if uri != nil { - otpKey, err = otp.NewKeyFromURL(uri.(string)) - } - if otpKey == nil { - err = nil // clear the error, in case the URL was invalid - otpKey, err = totp.Generate(totp.GenerateOpts{ - Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", - AccountName: ctx.User.Name, - }) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - } - - ctx.Data["TwofaSecret"] = otpKey.Secret() - img, err := otpKey.Image(320, 240) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - - var imgBytes bytes.Buffer - if err = png.Encode(&imgBytes, img); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return false - } - - ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes())) - ctx.Session.Set("twofaSecret", otpKey.Secret()) - ctx.Session.Set("twofaUri", otpKey.String()) - return true -} - -// SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA. -func SettingsTwoFactorEnroll(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if t != nil { - // already enrolled - ctx.ServerError("SettingsTwoFactor", err) - return - } - if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if !twofaGenerateSecretAndQr(ctx) { - return - } - - ctx.HTML(200, tplSettingsTwofaEnroll) -} - -// SettingsTwoFactorEnrollPost handles enrolling the user into 2FA. -func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsSecurity"] = true - - t, err := models.GetTwoFactorByUID(ctx.User.ID) - if t != nil { - // already enrolled - ctx.ServerError("SettingsTwoFactor", err) - return - } - if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if ctx.HasError() { - if !twofaGenerateSecretAndQr(ctx) { - return - } - ctx.HTML(200, tplSettingsTwofaEnroll) - return - } - - secret := ctx.Session.Get("twofaSecret").(string) - if !totp.Validate(form.Passcode, secret) { - if !twofaGenerateSecretAndQr(ctx) { - return - } - ctx.Flash.Error(ctx.Tr("settings.passcode_invalid")) - ctx.HTML(200, tplSettingsTwofaEnroll) - return - } - - t = &models.TwoFactor{ - UID: ctx.User.ID, - } - err = t.SetSecret(secret) - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - err = t.GenerateScratchToken() - if err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - if err = models.NewTwoFactor(t); err != nil { - ctx.ServerError("SettingsTwoFactor", err) - return - } - - ctx.Session.Delete("twofaSecret") - ctx.Session.Delete("twofaUri") - ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) - ctx.Redirect(setting.AppSubURL + "/user/settings/security") -} - -// SettingsOrganization render all the organization of the user -func SettingsOrganization(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsOrganization"] = true - orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned) - if err != nil { - ctx.ServerError("GetOrgsByUserID", err) - return - } - ctx.Data["Orgs"] = orgs - ctx.HTML(200, tplSettingsOrganization) -} - -// SettingsRepos display a list of all repositories of the user -func SettingsRepos(ctx *context.Context) { - ctx.Data["Title"] = ctx.Tr("settings") - ctx.Data["PageIsSettingsRepos"] = true - ctxUser := ctx.User - - var err error - if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil { - ctx.ServerError("GetRepositories", err) - return - } - repos := ctxUser.Repos - - for i := range repos { - if repos[i].IsFork { - err := repos[i].GetBaseRepo() - if err != nil { - ctx.ServerError("GetBaseRepo", err) - return - } - err = repos[i].BaseRepo.GetOwner() - if err != nil { - ctx.ServerError("GetOwner", err) - return - } - } - } - - ctx.Data["Owner"] = ctxUser - ctx.Data["Repos"] = repos - - ctx.HTML(200, tplSettingsRepositories) -} diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go new file mode 100644 index 000000000..966d96aed --- /dev/null +++ b/routers/user/setting/account.go @@ -0,0 +1,174 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsAccount base.TplName = "user/settings/account" +) + +// Account renders change user's password, user's email and user suicide page +func Account(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + ctx.Data["Email"] = ctx.User.Email + + emails, err := models.GetEmailAddresses(ctx.User.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails + + ctx.HTML(200, tplSettingsAccount) +} + +// AccountPost response for change user's password +func AccountPost(ctx *context.Context, form auth.ChangePasswordForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + if ctx.HasError() { + ctx.HTML(200, tplSettingsAccount) + return + } + + if len(form.Password) < setting.MinPasswordLength { + ctx.Flash.Error(ctx.Tr("auth.password_too_short", setting.MinPasswordLength)) + } else if ctx.User.IsPasswordSet() && !ctx.User.ValidatePassword(form.OldPassword) { + ctx.Flash.Error(ctx.Tr("settings.password_incorrect")) + } else if form.Password != form.Retype { + ctx.Flash.Error(ctx.Tr("form.password_not_match")) + } else { + var err error + if ctx.User.Salt, err = models.GetUserSalt(); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + ctx.User.HashPassword(form.Password) + if err := models.UpdateUserCols(ctx.User, "salt", "passwd"); err != nil { + ctx.ServerError("UpdateUser", err) + return + } + log.Trace("User password updated: %s", ctx.User.Name) + ctx.Flash.Success(ctx.Tr("settings.change_password_success")) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/account") +} + +// EmailPost response for change user's email +func EmailPost(ctx *context.Context, form auth.AddEmailForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + // Make emailaddress primary. + if ctx.Query("_method") == "PRIMARY" { + if err := models.MakeEmailPrimary(&models.EmailAddress{ID: ctx.QueryInt64("id")}); err != nil { + ctx.ServerError("MakeEmailPrimary", err) + return + } + + log.Trace("Email made primary: %s", ctx.User.Name) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + return + } + + // Add Email address. + emails, err := models.GetEmailAddresses(ctx.User.ID) + if err != nil { + ctx.ServerError("GetEmailAddresses", err) + return + } + ctx.Data["Emails"] = emails + + if ctx.HasError() { + ctx.HTML(200, tplSettingsAccount) + return + } + + email := &models.EmailAddress{ + UID: ctx.User.ID, + Email: form.Email, + IsActivated: !setting.Service.RegisterEmailConfirm, + } + if err := models.AddEmailAddress(email); err != nil { + if models.IsErrEmailAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.email_been_used"), tplSettingsAccount, &form) + return + } + ctx.ServerError("AddEmailAddress", err) + return + } + + // Send confirmation email + if setting.Service.RegisterEmailConfirm { + models.SendActivateEmailMail(ctx.Context, ctx.User, email) + + if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { + log.Error(4, "Set cache(MailResendLimit) fail: %v", err) + } + ctx.Flash.Info(ctx.Tr("settings.add_email_confirmation_sent", email.Email, base.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale.Language()))) + } else { + ctx.Flash.Success(ctx.Tr("settings.add_email_success")) + } + + log.Trace("Email address added: %s", email.Email) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") +} + +// DeleteEmail response for delete user's email +func DeleteEmail(ctx *context.Context) { + if err := models.DeleteEmailAddress(&models.EmailAddress{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { + ctx.ServerError("DeleteEmail", err) + return + } + log.Trace("Email address deleted: %s", ctx.User.Name) + + ctx.Flash.Success(ctx.Tr("settings.email_deletion_success")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/account", + }) +} + +// DeleteAccount render user suicide page and response for delete user himself +func DeleteAccount(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsAccount"] = true + + if _, err := models.UserSignIn(ctx.User.Name, ctx.Query("password")); err != nil { + if models.IsErrUserNotExist(err) { + ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_password"), tplSettingsAccount, nil) + } else { + ctx.ServerError("UserSignIn", err) + } + return + } + + if err := models.DeleteUser(ctx.User); err != nil { + switch { + case models.IsErrUserOwnRepos(err): + ctx.Flash.Error(ctx.Tr("form.still_own_repo")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + case models.IsErrUserHasOrgs(err): + ctx.Flash.Error(ctx.Tr("form.still_has_org")) + ctx.Redirect(setting.AppSubURL + "/user/settings/account") + default: + ctx.ServerError("DeleteUser", err) + } + } else { + log.Trace("Account deleted: %s", ctx.User.Name) + ctx.Redirect(setting.AppSubURL + "/") + } +} diff --git a/routers/user/setting_test.go b/routers/user/setting/account_test.go similarity index 91% rename from routers/user/setting_test.go rename to routers/user/setting/account_test.go index 6aa9a0743..59fbda156 100644 --- a/routers/user/setting_test.go +++ b/routers/user/setting/account_test.go @@ -1,8 +1,8 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// 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 user +package setting import ( "net/http" @@ -56,7 +56,7 @@ func TestChangePassword(t *testing.T) { test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) - SettingsAccountPost(ctx, auth.ChangePasswordForm{ + AccountPost(ctx, auth.ChangePasswordForm{ OldPassword: req.OldPassword, Password: req.NewPassword, Retype: req.Retype, diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go new file mode 100644 index 000000000..f292b65d7 --- /dev/null +++ b/routers/user/setting/applications.go @@ -0,0 +1,77 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsApplications base.TplName = "user/settings/applications" +) + +// Applications render manage access token page +func Applications(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsApplications"] = true + + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + + ctx.HTML(200, tplSettingsApplications) +} + +// ApplicationsPost response for add user's access token +func ApplicationsPost(ctx *context.Context, form auth.NewAccessTokenForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsApplications"] = true + + if ctx.HasError() { + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + ctx.HTML(200, tplSettingsApplications) + return + } + + t := &models.AccessToken{ + UID: ctx.User.ID, + Name: form.Name, + } + if err := models.NewAccessToken(t); err != nil { + ctx.ServerError("NewAccessToken", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.generate_token_success")) + ctx.Flash.Info(t.Sha1) + + ctx.Redirect(setting.AppSubURL + "/user/settings/applications") +} + +// DeleteApplication response for delete user access token +func DeleteApplication(ctx *context.Context) { + if err := models.DeleteAccessTokenByID(ctx.QueryInt64("id"), ctx.User.ID); err != nil { + ctx.Flash.Error("DeleteAccessTokenByID: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.delete_token_success")) + } + + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/applications", + }) +} diff --git a/routers/user/setting/keys.go b/routers/user/setting/keys.go new file mode 100644 index 000000000..5c28fa6e6 --- /dev/null +++ b/routers/user/setting/keys.go @@ -0,0 +1,149 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsKeys base.TplName = "user/settings/keys" +) + +// Keys render user's SSH/GPG public keys page +func Keys(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsKeys"] = true + ctx.Data["DisableSSH"] = setting.SSH.Disabled + + keys, err := models.ListPublicKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListPublicKeys", err) + return + } + ctx.Data["Keys"] = keys + + gpgkeys, err := models.ListGPGKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListGPGKeys", err) + return + } + ctx.Data["GPGKeys"] = gpgkeys + + ctx.HTML(200, tplSettingsKeys) +} + +// KeysPost response for change user's SSH/GPG keys +func KeysPost(ctx *context.Context, form auth.AddKeyForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsKeys"] = true + + keys, err := models.ListPublicKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListPublicKeys", err) + return + } + ctx.Data["Keys"] = keys + + gpgkeys, err := models.ListGPGKeys(ctx.User.ID) + if err != nil { + ctx.ServerError("ListGPGKeys", err) + return + } + ctx.Data["GPGKeys"] = gpgkeys + + if ctx.HasError() { + ctx.HTML(200, tplSettingsKeys) + return + } + switch form.Type { + case "gpg": + key, err := models.AddGPGKey(ctx.User.ID, form.Content) + if err != nil { + ctx.Data["HasGPGError"] = true + switch { + case models.IsErrGPGKeyParsing(err): + ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error())) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + case models.IsErrGPGKeyIDAlreadyUsed(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form) + case models.IsErrGPGNoEmailFound(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form) + default: + ctx.ServerError("AddPublicKey", err) + } + return + } + ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", key.KeyID)) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + case "ssh": + content, err := models.CheckPublicKeyString(form.Content) + if err != nil { + if models.IsErrSSHDisabled(err) { + ctx.Flash.Info(ctx.Tr("settings.ssh_disabled")) + } else if models.IsErrKeyUnableVerify(err) { + ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key")) + } else { + ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error())) + } + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + return + } + + if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content); err != nil { + ctx.Data["HasSSHError"] = true + switch { + case models.IsErrKeyAlreadyExist(err): + ctx.Data["Err_Content"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form) + case models.IsErrKeyNameAlreadyUsed(err): + ctx.Data["Err_Title"] = true + ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form) + default: + ctx.ServerError("AddPublicKey", err) + } + return + } + ctx.Flash.Success(ctx.Tr("settings.add_key_success", form.Title)) + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + + default: + ctx.Flash.Warning("Function not implemented") + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + } + +} + +// DeleteKey response for delete user's SSH/GPG key +func DeleteKey(ctx *context.Context) { + + switch ctx.Query("type") { + case "gpg": + if err := models.DeleteGPGKey(ctx.User, ctx.QueryInt64("id")); err != nil { + ctx.Flash.Error("DeleteGPGKey: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success")) + } + case "ssh": + if err := models.DeletePublicKey(ctx.User, ctx.QueryInt64("id")); err != nil { + ctx.Flash.Error("DeletePublicKey: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success")) + } + default: + ctx.Flash.Warning("Function not implemented") + ctx.Redirect(setting.AppSubURL + "/user/settings/keys") + } + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/keys", + }) +} diff --git a/routers/user/setting/main_test.go b/routers/user/setting/main_test.go new file mode 100644 index 000000000..d343c02f4 --- /dev/null +++ b/routers/user/setting/main_test.go @@ -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 setting + +import ( + "path/filepath" + "testing" + + "code.gitea.io/gitea/models" +) + +func TestMain(m *testing.M) { + models.MainTest(m, filepath.Join("..", "..", "..")) +} diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go new file mode 100644 index 000000000..2ca64ad2e --- /dev/null +++ b/routers/user/setting/profile.go @@ -0,0 +1,220 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" + + "github.com/Unknwon/com" + "github.com/Unknwon/i18n" +) + +const ( + tplSettingsProfile base.TplName = "user/settings/profile" + tplSettingsOrganization base.TplName = "user/settings/organization" + tplSettingsRepositories base.TplName = "user/settings/repos" +) + +// Profile render user's profile page +func Profile(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsProfile"] = true + ctx.HTML(200, tplSettingsProfile) +} + +func handleUsernameChange(ctx *context.Context, newName string) { + // Non-local users are not allowed to change their username. + if len(newName) == 0 || !ctx.User.IsLocal() { + return + } + + // Check if user name has been changed + if ctx.User.LowerName != strings.ToLower(newName) { + if err := models.ChangeUserName(ctx.User, newName); err != nil { + switch { + case models.IsErrUserAlreadyExist(err): + ctx.Flash.Error(ctx.Tr("form.username_been_taken")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrEmailAlreadyUsed(err): + ctx.Flash.Error(ctx.Tr("form.email_been_used")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrNameReserved(err): + ctx.Flash.Error(ctx.Tr("user.form.name_reserved", newName)) + ctx.Redirect(setting.AppSubURL + "/user/settings") + case models.IsErrNamePatternNotAllowed(err): + ctx.Flash.Error(ctx.Tr("user.form.name_pattern_not_allowed", newName)) + ctx.Redirect(setting.AppSubURL + "/user/settings") + default: + ctx.ServerError("ChangeUserName", err) + } + return + } + log.Trace("User name changed: %s -> %s", ctx.User.Name, newName) + } + + // 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 +func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsProfile"] = true + + if ctx.HasError() { + ctx.HTML(200, tplSettingsProfile) + return + } + + handleUsernameChange(ctx, form.Name) + if ctx.Written() { + return + } + + ctx.User.FullName = form.FullName + ctx.User.Email = form.Email + ctx.User.KeepEmailPrivate = form.KeepEmailPrivate + ctx.User.Website = form.Website + ctx.User.Location = form.Location + ctx.User.Language = form.Language + if err := models.UpdateUserSetting(ctx.User); err != nil { + if _, ok := err.(models.ErrEmailAlreadyUsed); ok { + ctx.Flash.Error(ctx.Tr("form.email_been_used")) + ctx.Redirect(setting.AppSubURL + "/user/settings") + return + } + ctx.ServerError("UpdateUser", err) + return + } + + // Update the language to the one we just set + ctx.SetCookie("lang", ctx.User.Language, nil, setting.AppSubURL) + + log.Trace("User settings updated: %s", ctx.User.Name) + ctx.Flash.Success(i18n.Tr(ctx.User.Language, "settings.update_profile_success")) + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// UpdateAvatarSetting update user's avatar +// FIXME: limit size. +func UpdateAvatarSetting(ctx *context.Context, form auth.AvatarForm, ctxUser *models.User) error { + ctxUser.UseCustomAvatar = form.Source == auth.AvatarLocal + if len(form.Gravatar) > 0 { + ctxUser.Avatar = base.EncodeMD5(form.Gravatar) + ctxUser.AvatarEmail = form.Gravatar + } + + if form.Avatar != nil { + fr, err := form.Avatar.Open() + if err != nil { + return fmt.Errorf("Avatar.Open: %v", err) + } + defer fr.Close() + + data, err := ioutil.ReadAll(fr) + if err != nil { + return fmt.Errorf("ioutil.ReadAll: %v", err) + } + if !base.IsImageFile(data) { + return errors.New(ctx.Tr("settings.uploaded_avatar_not_a_image")) + } + if err = ctxUser.UploadAvatar(data); err != nil { + return fmt.Errorf("UploadAvatar: %v", err) + } + } else { + // No avatar is uploaded but setting has been changed to enable, + // generate a random one when needed. + if ctxUser.UseCustomAvatar && !com.IsFile(ctxUser.CustomAvatarPath()) { + if err := ctxUser.GenerateRandomAvatar(); err != nil { + log.Error(4, "GenerateRandomAvatar[%d]: %v", ctxUser.ID, err) + } + } + } + + if err := models.UpdateUserCols(ctxUser, "avatar", "avatar_email", "use_custom_avatar"); err != nil { + return fmt.Errorf("UpdateUser: %v", err) + } + + return nil +} + +// AvatarPost response for change user's avatar request +func AvatarPost(ctx *context.Context, form auth.AvatarForm) { + if err := UpdateAvatarSetting(ctx, form, ctx.User); err != nil { + ctx.Flash.Error(err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.update_avatar_success")) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// DeleteAvatar render delete avatar page +func DeleteAvatar(ctx *context.Context) { + if err := ctx.User.DeleteAvatar(); err != nil { + ctx.Flash.Error(err.Error()) + } + + ctx.Redirect(setting.AppSubURL + "/user/settings") +} + +// Organization render all the organization of the user +func Organization(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsOrganization"] = true + orgs, err := models.GetOrgsByUserID(ctx.User.ID, ctx.IsSigned) + if err != nil { + ctx.ServerError("GetOrgsByUserID", err) + return + } + ctx.Data["Orgs"] = orgs + ctx.HTML(200, tplSettingsOrganization) +} + +// Repos display a list of all repositories of the user +func Repos(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsRepos"] = true + ctxUser := ctx.User + + var err error + if err = ctxUser.GetRepositories(1, setting.UI.User.RepoPagingNum); err != nil { + ctx.ServerError("GetRepositories", err) + return + } + repos := ctxUser.Repos + + for i := range repos { + if repos[i].IsFork { + err := repos[i].GetBaseRepo() + if err != nil { + ctx.ServerError("GetBaseRepo", err) + return + } + err = repos[i].BaseRepo.GetOwner() + if err != nil { + ctx.ServerError("GetOwner", err) + return + } + } + } + + ctx.Data["Owner"] = ctxUser + ctx.Data["Repos"] = repos + + ctx.HTML(200, tplSettingsRepositories) +} diff --git a/routers/user/setting/security.go b/routers/user/setting/security.go new file mode 100644 index 000000000..5346f349f --- /dev/null +++ b/routers/user/setting/security.go @@ -0,0 +1,92 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" +) + +const ( + tplSettingsSecurity base.TplName = "user/settings/security" + tplSettingsTwofaEnroll base.TplName = "user/settings/twofa_enroll" +) + +// Security render change user's password page and 2FA +func Security(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + enrolled := true + _, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + if models.IsErrTwoFactorNotEnrolled(err) { + enrolled = false + } else { + ctx.ServerError("SettingsTwoFactor", err) + return + } + } + ctx.Data["TwofaEnrolled"] = enrolled + + tokens, err := models.ListAccessTokens(ctx.User.ID) + if err != nil { + ctx.ServerError("ListAccessTokens", err) + return + } + ctx.Data["Tokens"] = tokens + + accountLinks, err := models.ListAccountLinks(ctx.User) + if err != nil { + ctx.ServerError("ListAccountLinks", err) + return + } + + // map the provider display name with the LoginSource + sources := make(map[*models.LoginSource]string) + for _, externalAccount := range accountLinks { + if loginSource, err := models.GetLoginSourceByID(externalAccount.LoginSourceID); err == nil { + var providerDisplayName string + if loginSource.IsOAuth2() { + providerTechnicalName := loginSource.OAuth2().Provider + providerDisplayName = models.OAuth2Providers[providerTechnicalName].DisplayName + } else { + providerDisplayName = loginSource.Name + } + sources[loginSource] = providerDisplayName + } + } + ctx.Data["AccountLinks"] = sources + + if ctx.Query("openid.return_to") != "" { + settingsOpenIDVerify(ctx) + return + } + + openid, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.ServerError("GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = openid + + ctx.HTML(200, tplSettingsSecurity) +} + +// DeleteAccountLink delete a single account link +func DeleteAccountLink(ctx *context.Context) { + if _, err := models.RemoveAccountLink(ctx.User, ctx.QueryInt64("loginSourceID")); err != nil { + ctx.Flash.Error("RemoveAccountLink: " + err.Error()) + } else { + ctx.Flash.Success(ctx.Tr("settings.remove_account_link_success")) + } + + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/security", + }) +} diff --git a/routers/user/setting_openid.go b/routers/user/setting/security_openid.go similarity index 94% rename from routers/user/setting_openid.go rename to routers/user/setting/security_openid.go index 771646612..c98dc2cda 100644 --- a/routers/user/setting_openid.go +++ b/routers/user/setting/security_openid.go @@ -1,8 +1,8 @@ -// Copyright 2017 The Gitea Authors. All rights reserved. +// 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 user +package setting import ( "code.gitea.io/gitea/models" @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/setting" ) -// SettingsOpenIDPost response for change user's openid -func SettingsOpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { +// OpenIDPost response for change user's openid +func OpenIDPost(ctx *context.Context, form auth.AddOpenIDForm) { ctx.Data["Title"] = ctx.Tr("settings") ctx.Data["PageIsSettingsSecurity"] = true diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go new file mode 100644 index 000000000..55101ed1a --- /dev/null +++ b/routers/user/setting/security_twofa.go @@ -0,0 +1,187 @@ +// Copyright 2014 The Gogs Authors. All rights reserved. +// 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 setting + +import ( + "bytes" + "encoding/base64" + "html/template" + "image/png" + "strings" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + + "github.com/pquerna/otp" + "github.com/pquerna/otp/totp" +) + +// RegenerateScratchTwoFactor regenerates the user's 2FA scratch code. +func RegenerateScratchTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = t.GenerateScratchToken(); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.UpdateTwoFactor(t); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} + +// DisableTwoFactor deletes the user's 2FA settings. +func DisableTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.DeleteTwoFactorByID(t.ID, ctx.User.ID); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Flash.Success(ctx.Tr("settings.twofa_disabled")) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} + +func twofaGenerateSecretAndQr(ctx *context.Context) bool { + var otpKey *otp.Key + var err error + uri := ctx.Session.Get("twofaUri") + if uri != nil { + otpKey, err = otp.NewKeyFromURL(uri.(string)) + } + if otpKey == nil { + err = nil // clear the error, in case the URL was invalid + otpKey, err = totp.Generate(totp.GenerateOpts{ + Issuer: setting.AppName + " (" + strings.TrimRight(setting.AppURL, "/") + ")", + AccountName: ctx.User.Name, + }) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + } + + ctx.Data["TwofaSecret"] = otpKey.Secret() + img, err := otpKey.Image(320, 240) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + + var imgBytes bytes.Buffer + if err = png.Encode(&imgBytes, img); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return false + } + + ctx.Data["QrUri"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(imgBytes.Bytes())) + ctx.Session.Set("twofaSecret", otpKey.Secret()) + ctx.Session.Set("twofaUri", otpKey.String()) + return true +} + +// EnrollTwoFactor shows the page where the user can enroll into 2FA. +func EnrollTwoFactor(ctx *context.Context) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.ServerError("SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if !twofaGenerateSecretAndQr(ctx) { + return + } + + ctx.HTML(200, tplSettingsTwofaEnroll) +} + +// EnrollTwoFactorPost handles enrolling the user into 2FA. +func EnrollTwoFactorPost(ctx *context.Context, form auth.TwoFactorAuthForm) { + ctx.Data["Title"] = ctx.Tr("settings") + ctx.Data["PageIsSettingsSecurity"] = true + + t, err := models.GetTwoFactorByUID(ctx.User.ID) + if t != nil { + // already enrolled + ctx.ServerError("SettingsTwoFactor", err) + return + } + if err != nil && !models.IsErrTwoFactorNotEnrolled(err) { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if ctx.HasError() { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + secret := ctx.Session.Get("twofaSecret").(string) + if !totp.Validate(form.Passcode, secret) { + if !twofaGenerateSecretAndQr(ctx) { + return + } + ctx.Flash.Error(ctx.Tr("settings.passcode_invalid")) + ctx.HTML(200, tplSettingsTwofaEnroll) + return + } + + t = &models.TwoFactor{ + UID: ctx.User.ID, + } + err = t.SetSecret(secret) + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + err = t.GenerateScratchToken() + if err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + if err = models.NewTwoFactor(t); err != nil { + ctx.ServerError("SettingsTwoFactor", err) + return + } + + ctx.Session.Delete("twofaSecret") + ctx.Session.Delete("twofaUri") + ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken)) + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +}