diff options
Diffstat (limited to 'routers/user/setting')
-rw-r--r-- | routers/user/setting/account.go | 174 | ||||
-rw-r--r-- | routers/user/setting/account_test.go | 68 | ||||
-rw-r--r-- | routers/user/setting/applications.go | 77 | ||||
-rw-r--r-- | routers/user/setting/keys.go | 149 | ||||
-rw-r--r-- | routers/user/setting/main_test.go | 16 | ||||
-rw-r--r-- | routers/user/setting/profile.go | 220 | ||||
-rw-r--r-- | routers/user/setting/security.go | 92 | ||||
-rw-r--r-- | routers/user/setting/security_openid.go | 130 | ||||
-rw-r--r-- | routers/user/setting/security_twofa.go | 187 |
9 files changed, 1113 insertions, 0 deletions
diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go new file mode 100644 index 0000000000..966d96aeda --- /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/account_test.go b/routers/user/setting/account_test.go new file mode 100644 index 0000000000..59fbda1569 --- /dev/null +++ b/routers/user/setting/account_test.go @@ -0,0 +1,68 @@ +// 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 ( + "net/http" + "testing" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/auth" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestChangePassword(t *testing.T) { + oldPassword := "password" + setting.MinPasswordLength = 6 + + for _, req := range []struct { + OldPassword string + NewPassword string + Retype string + Message string + }{ + { + OldPassword: oldPassword, + NewPassword: "123456", + Retype: "123456", + Message: "", + }, + { + OldPassword: oldPassword, + NewPassword: "12345", + Retype: "12345", + Message: "auth.password_too_short", + }, + { + OldPassword: "12334", + NewPassword: "123456", + Retype: "123456", + Message: "settings.password_incorrect", + }, + { + OldPassword: oldPassword, + NewPassword: "123456", + Retype: "12345", + Message: "form.password_not_match", + }, + } { + models.PrepareTestEnv(t) + ctx := test.MockContext(t, "user/settings/security") + test.LoadUser(t, ctx, 2) + test.LoadRepo(t, ctx, 1) + + AccountPost(ctx, auth.ChangePasswordForm{ + OldPassword: req.OldPassword, + Password: req.NewPassword, + Retype: req.Retype, + }) + + assert.EqualValues(t, req.Message, ctx.Flash.ErrorMsg) + assert.EqualValues(t, http.StatusFound, ctx.Resp.Status()) + } +} diff --git a/routers/user/setting/applications.go b/routers/user/setting/applications.go new file mode 100644 index 0000000000..f292b65d70 --- /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 0000000000..5c28fa6e6d --- /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 0000000000..d343c02f48 --- /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 0000000000..2ca64ad2e5 --- /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 0000000000..5346f349ff --- /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/security_openid.go b/routers/user/setting/security_openid.go new file mode 100644 index 0000000000..c98dc2cda9 --- /dev/null +++ b/routers/user/setting/security_openid.go @@ -0,0 +1,130 @@ +// 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/auth/openid" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/setting" +) + +// 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 + + if ctx.HasError() { + openid, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.ServerError("GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = openid + ctx.HTML(200, tplSettingsSecurity) + return + } + + // WARNING: specifying a wrong OpenID here could lock + // a user out of her account, would be better to + // verify/confirm the new OpenID before storing it + + // Also, consider allowing for multiple OpenID URIs + + id, err := openid.Normalize(form.Openid) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + return + } + form.Openid = id + log.Trace("Normalized id: " + id) + + oids, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.ServerError("GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = oids + + // Check that the OpenID is not already used + for _, obj := range oids { + if obj.URI == id { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &form) + return + } + } + + redirectTo := setting.AppURL + "user/settings/security" + url, err := openid.RedirectURL(id, redirectTo, setting.AppURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &form) + return + } + ctx.Redirect(url) +} + +func settingsOpenIDVerify(ctx *context.Context) { + log.Trace("Incoming call to: " + ctx.Req.Request.URL.String()) + + fullURL := setting.AppURL + ctx.Req.Request.URL.String()[1:] + log.Trace("Full URL: " + fullURL) + + oids, err := models.GetUserOpenIDs(ctx.User.ID) + if err != nil { + ctx.ServerError("GetUserOpenIDs", err) + return + } + ctx.Data["OpenIDs"] = oids + + id, err := openid.Verify(fullURL) + if err != nil { + ctx.RenderWithErr(err.Error(), tplSettingsSecurity, &auth.AddOpenIDForm{ + Openid: id, + }) + return + } + + log.Trace("Verified ID: " + id) + + oid := &models.UserOpenID{UID: ctx.User.ID, URI: id} + if err = models.AddUserOpenID(oid); err != nil { + if models.IsErrOpenIDAlreadyUsed(err) { + ctx.RenderWithErr(ctx.Tr("form.openid_been_used", id), tplSettingsSecurity, &auth.AddOpenIDForm{Openid: id}) + return + } + ctx.ServerError("AddUserOpenID", err) + return + } + log.Trace("Associated OpenID %s to user %s", id, ctx.User.Name) + ctx.Flash.Success(ctx.Tr("settings.add_openid_success")) + + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} + +// DeleteOpenID response for delete user's openid +func DeleteOpenID(ctx *context.Context) { + if err := models.DeleteUserOpenID(&models.UserOpenID{ID: ctx.QueryInt64("id"), UID: ctx.User.ID}); err != nil { + ctx.ServerError("DeleteUserOpenID", err) + return + } + log.Trace("OpenID address deleted: %s", ctx.User.Name) + + ctx.Flash.Success(ctx.Tr("settings.openid_deletion_success")) + ctx.JSON(200, map[string]interface{}{ + "redirect": setting.AppSubURL + "/user/settings/security", + }) +} + +// ToggleOpenIDVisibility response for toggle visibility of user's openid +func ToggleOpenIDVisibility(ctx *context.Context) { + if err := models.ToggleUserOpenIDVisibility(ctx.QueryInt64("id")); err != nil { + ctx.ServerError("ToggleUserOpenIDVisibility", err) + return + } + + ctx.Redirect(setting.AppSubURL + "/user/settings/security") +} diff --git a/routers/user/setting/security_twofa.go b/routers/user/setting/security_twofa.go new file mode 100644 index 0000000000..55101ed1a4 --- /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") +} |