// Copyright 2015 - Present, The Gogs Authors. All rights reserved. // Copyright 2018 - Present, Gitote. All rights reserved. // // This source code is licensed under the MIT license found in the // LICENSE file in the root directory of this source tree. package user import ( "bytes" "encoding/base64" "fmt" "gitote/gitote/models" "gitote/gitote/models/errors" "gitote/gitote/pkg/context" "gitote/gitote/pkg/form" "gitote/gitote/pkg/mailer" "gitote/gitote/pkg/setting" "gitote/gitote/pkg/tool" "html/template" "image/png" "io/ioutil" "strings" raven "github.com/getsentry/raven-go" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "gitlab.com/gitote/com" log "gopkg.in/clog.v1" ) const ( // SettingsProfileTPL page template SettingsProfileTPL = "user/settings/profile" // SettingsSocialTPL page template SettingsSocialTPL = "user/settings/social" // SettingsAvatarTPL page template SettingsAvatarTPL = "user/settings/avatar" // SettingsPasswordTPL page template SettingsPasswordTPL = "user/settings/password" // SettingsEmailsTPL page template SettingsEmailsTPL = "user/settings/email" // SettingsSSHKeysTPL page template SettingsSSHKeysTPL = "user/settings/sshkeys" // SettingsSecurityTPL page template SettingsSecurityTPL = "user/settings/security" // SettingsTwoFactorEnableTPL page template SettingsTwoFactorEnableTPL = "user/settings/two_factor_enable" // SettingsTwoFactorRecoveryCodesTPL page template SettingsTwoFactorRecoveryCodesTPL = "user/settings/two_factor_recovery_codes" // SettingsRepositoriesTPL page template SettingsRepositoriesTPL = "user/settings/repositories" // SettingsOrganizationsTPL page template SettingsOrganizationsTPL = "user/settings/organizations" // SettingsApplicationsTPL page template SettingsApplicationsTPL = "user/settings/applications" // SettingsEmbedsTPL page template SettingsEmbedsTPL = "user/settings/embeds" // SettingsDeleteTPL page template SettingsDeleteTPL = "user/settings/delete" // NotificationTPL page template NotificationTPL = "user/notification" ) // Settings shows settings page func Settings(c *context.Context) { c.Title("settings.profile") c.PageIs("SettingsProfile") c.Data["origin_name"] = c.User.Name c.Data["name"] = c.User.Name c.Data["company"] = c.User.Company c.Data["description"] = c.User.Description c.Data["full_name"] = c.User.FullName c.Data["email"] = c.User.Email c.Data["website"] = c.User.Website c.Data["location"] = c.User.Location c.Data["status"] = c.User.Status c.Data["themecolor"] = c.User.ThemeColor c.Data["is_beta"] = c.User.IsBeta c.Data["private_email"] = c.User.PrivateEmail c.Data["private_profile"] = c.User.PrivateProfile c.Success(SettingsProfileTPL) } // SettingsPost updates user settings func SettingsPost(c *context.Context, f form.UpdateProfile) { c.Title("settings.profile") c.PageIs("SettingsProfile") c.Data["origin_name"] = c.User.Name if c.HasError() { c.Success(SettingsProfileTPL) return } // Non-local users are not allowed to change their username if c.User.IsLocal() { // Check if username characters have been changed if c.User.LowerName != strings.ToLower(f.Name) { if err := models.ChangeUserName(c.User, f.Name); err != nil { c.FormErr("Name") var msg string switch { case models.IsErrUserAlreadyExist(err): msg = c.Tr("form.username_been_taken") case models.IsErrNameReserved(err): msg = c.Tr("form.name_reserved") case models.IsErrNamePatternNotAllowed(err): msg = c.Tr("form.name_pattern_not_allowed") default: c.ServerError("ChangeUserName", err) return } c.RenderWithErr(msg, SettingsProfileTPL, &f) return } log.Trace("Username changed: %s -> %s", c.User.Name, f.Name) } // In case it's just a case change c.User.Name = f.Name c.User.LowerName = strings.ToLower(f.Name) } var email = strings.ToLower(f.Email) if c.User.Email != email { if used, err := models.IsEmailUsed(email); used { c.FormErr("Email") c.RenderWithErr(c.Tr("form.email_been_used"), SettingsProfileTPL, &f) return } else if err != nil { c.ServerError("ChangeUserName", err) return } c.User.Email = email } c.User.FullName = f.FullName c.User.Company = f.Company c.User.Description = f.Description c.User.Website = f.Website c.User.Location = f.Location c.User.Status = f.Status c.User.ThemeColor = f.ThemeColor c.User.IsBeta = f.IsBeta c.User.PrivateEmail = f.PrivateEmail c.User.PrivateProfile = f.PrivateProfile if err := models.UpdateUser(c.User); err != nil { c.ServerError("UpdateUser", err) return } c.Flash.Success(c.Tr("settings.update_profile_success")) c.SubURLRedirect("/user/settings") } // SettingsSocial shows user social settings page func SettingsSocial(c *context.Context) { c.Title("settings.social") c.PageIs("SettingsSocial") c.Data["twitter"] = c.User.Twitter c.Data["linkedin"] = c.User.Linkedin c.Data["github"] = c.User.Github c.Data["stackoverflow"] = c.User.Stackoverflow c.Data["telegram"] = c.User.Telegram c.Data["codepen"] = c.User.Codepen c.Data["gitlab"] = c.User.Gitlab c.Success(SettingsSocialTPL) } // SettingsSocialPost updates user social settings func SettingsSocialPost(c *context.Context, f form.UpdateSocial) { c.Title("settings.profile") c.PageIs("SettingsProfile") c.Data["origin_name"] = c.User.Name if c.HasError() { c.Success(SettingsSocialTPL) return } c.User.Twitter = f.Twitter c.User.Linkedin = f.Linkedin c.User.Github = f.Github c.User.Stackoverflow = f.Stackoverflow c.User.Telegram = f.Telegram c.User.Codepen = f.Codepen c.User.Gitlab = f.Gitlab if err := models.UpdateUser(c.User); err != nil { c.ServerError("UpdateUser", err) return } c.Flash.Success(c.Tr("settings.update_social_success")) c.SubURLRedirect("/user/settings/social") } // UpdateAvatarSetting FIXME: limit upload size func UpdateAvatarSetting(c *context.Context, f form.Avatar, ctxUser *models.User) error { ctxUser.UseCustomAvatar = f.Source == form.AvatarLocal if len(f.Gravatar) > 0 { ctxUser.Avatar = tool.MD5(f.Gravatar) ctxUser.AvatarEmail = f.Gravatar } if f.Avatar != nil && f.Avatar.Filename != "" { r, err := f.Avatar.Open() if err != nil { return fmt.Errorf("open avatar reader: %v", err) } defer r.Close() data, err := ioutil.ReadAll(r) if err != nil { return fmt.Errorf("read avatar content: %v", err) } if !tool.IsImageFile(data) { return errors.New(c.Tr("settings.uploaded_avatar_not_a_image")) } if err = ctxUser.UploadAvatar(data); err != nil { return fmt.Errorf("upload avatar: %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 { raven.CaptureErrorAndWait(err, nil) log.Error(2, "generate random avatar [%d]: %v", ctxUser.ID, err) } } } if err := models.UpdateUser(ctxUser); err != nil { return fmt.Errorf("update user: %v", err) } return nil } // SettingsAvatar shows user avatar settings page func SettingsAvatar(c *context.Context) { c.Title("settings.avatar") c.PageIs("SettingsAvatar") c.Success(SettingsAvatarTPL) } // SettingsAvatarPost updates user avatar settings func SettingsAvatarPost(c *context.Context, f form.Avatar) { if err := UpdateAvatarSetting(c, f, c.User); err != nil { c.Flash.Error(err.Error()) } else { c.Flash.Success(c.Tr("settings.update_avatar_success")) } c.SubURLRedirect("/user/settings/avatar") } // SettingsDeleteAvatar shows user delete avatar settings page func SettingsDeleteAvatar(c *context.Context) { if err := c.User.DeleteAvatar(); err != nil { c.Flash.Error(fmt.Sprintf("Failed to delete avatar: %v", err)) } c.SubURLRedirect("/user/settings/avatar") } // SettingsPassword shows user password settings page func SettingsPassword(c *context.Context) { c.Title("settings.password") c.PageIs("SettingsPassword") c.Success(SettingsPasswordTPL) } // SettingsPasswordPost updates user password settings func SettingsPasswordPost(c *context.Context, f form.ChangePassword) { c.Title("settings.password") c.PageIs("SettingsPassword") if c.HasError() { c.Success(SettingsPasswordTPL) return } if !c.User.ValidatePassword(f.OldPassword) { c.Flash.Error(c.Tr("settings.password_incorrect")) } else if f.Password != f.Retype { c.Flash.Error(c.Tr("form.password_not_match")) } else { c.User.Passwd = f.Password var err error if c.User.Salt, err = models.GetUserSalt(); err != nil { c.ServerError("GetUserSalt", err) return } c.User.EncodePasswd() if err := models.UpdateUser(c.User); err != nil { c.ServerError("UpdateUser", err) return } c.Flash.Success(c.Tr("settings.change_password_success")) } c.SubURLRedirect("/user/settings/password") } // SettingsEmails shows user email settings page func SettingsEmails(c *context.Context) { c.Title("settings.emails") c.PageIs("SettingsEmails") emails, err := models.GetEmailAddresses(c.User.ID) if err != nil { c.ServerError("GetEmailAddresses", err) return } c.Data["Emails"] = emails c.Success(SettingsEmailsTPL) } // SettingsEmailPost updates user email settings func SettingsEmailPost(c *context.Context, f form.AddEmail) { c.Title("settings.emails") c.PageIs("SettingsEmails") // Make emailaddress primary. if c.Query("_method") == "PRIMARY" { if err := models.MakeEmailPrimary(&models.EmailAddress{ID: c.QueryInt64("id")}); err != nil { c.ServerError("MakeEmailPrimary", err) return } c.SubURLRedirect("/user/settings/email") return } // Add Email address. emails, err := models.GetEmailAddresses(c.User.ID) if err != nil { c.ServerError("GetEmailAddresses", err) return } c.Data["Emails"] = emails if c.HasError() { c.Success(SettingsEmailsTPL) return } email := &models.EmailAddress{ UID: c.User.ID, Email: f.Email, IsActivated: !setting.Service.RegisterEmailConfirm, } if err := models.AddEmailAddress(email); err != nil { if models.IsErrEmailAlreadyUsed(err) { c.RenderWithErr(c.Tr("form.email_been_used"), SettingsEmailsTPL, &f) } else { c.ServerError("AddEmailAddress", err) } return } // Send confirmation email if setting.Service.RegisterEmailConfirm { mailer.SendActivateEmailMail(c.Context, models.NewMailerUser(c.User), email.Email) if err := c.Cache.Put("MailResendLimit_"+c.User.LowerName, c.User.LowerName, 180); err != nil { raven.CaptureErrorAndWait(err, nil) log.Error(2, "Set cache 'MailResendLimit' failed: %v", err) } c.Flash.Info(c.Tr("settings.add_email_confirmation_sent", email.Email, setting.Service.ActiveCodeLives/60)) } else { c.Flash.Success(c.Tr("settings.add_email_success")) } c.SubURLRedirect("/user/settings/email") } // DeleteEmail shows user delete email settings page func DeleteEmail(c *context.Context) { if err := models.DeleteEmailAddress(&models.EmailAddress{ ID: c.QueryInt64("id"), UID: c.User.ID, }); err != nil { c.ServerError("DeleteEmailAddress", err) return } c.Flash.Success(c.Tr("settings.email_deletion_success")) c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/email", }) } // SettingsSSHKeys shows user SSH settings page func SettingsSSHKeys(c *context.Context) { c.Title("settings.ssh_keys") c.PageIs("SettingsSSHKeys") keys, err := models.ListPublicKeys(c.User.ID) if err != nil { c.ServerError("ListPublicKeys", err) return } c.Data["Keys"] = keys c.Success(SettingsSSHKeysTPL) } // SettingsSSHKeysPost updates user SSH settings func SettingsSSHKeysPost(c *context.Context, f form.AddSSHKey) { c.Title("settings.ssh_keys") c.PageIs("SettingsSSHKeys") keys, err := models.ListPublicKeys(c.User.ID) if err != nil { c.ServerError("ListPublicKeys", err) return } c.Data["Keys"] = keys if c.HasError() { c.Success(SettingsSSHKeysTPL) return } content, err := models.CheckPublicKeyString(f.Content) if err != nil { if models.IsErrKeyUnableVerify(err) { c.Flash.Info(c.Tr("form.unable_verify_ssh_key")) } else { c.Flash.Error(c.Tr("form.invalid_ssh_key", err.Error())) c.SubURLRedirect("/user/settings/ssh") return } } if _, err = models.AddPublicKey(c.User.ID, f.Title, content); err != nil { c.Data["HasError"] = true switch { case models.IsErrKeyAlreadyExist(err): c.FormErr("Content") c.RenderWithErr(c.Tr("settings.ssh_key_been_used"), SettingsSSHKeysTPL, &f) case models.IsErrKeyNameAlreadyUsed(err): c.FormErr("Title") c.RenderWithErr(c.Tr("settings.ssh_key_name_used"), SettingsSSHKeysTPL, &f) default: c.ServerError("AddPublicKey", err) } return } c.Flash.Success(c.Tr("settings.add_key_success", f.Title)) c.SubURLRedirect("/user/settings/ssh") } // DeleteSSHKey shows user SSH delete settings page func DeleteSSHKey(c *context.Context) { if err := models.DeletePublicKey(c.User, c.QueryInt64("id")); err != nil { c.Flash.Error("DeletePublicKey: " + err.Error()) } else { c.Flash.Success(c.Tr("settings.ssh_key_deletion_success")) } c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/ssh", }) } // SettingsSecurity shows user security settings page func SettingsSecurity(c *context.Context) { c.Title("settings.security") c.PageIs("SettingsSecurity") t, err := models.GetTwoFactorByUserID(c.UserID()) if err != nil && !errors.IsTwoFactorNotFound(err) { c.ServerError("GetTwoFactorByUserID", err) return } c.Data["TwoFactor"] = t c.Success(SettingsSecurityTPL) } // SettingsTwoFactorEnable shows user 2FA settings page func SettingsTwoFactorEnable(c *context.Context) { if c.User.IsEnabledTwoFactor() { c.NotFound() return } c.Title("settings.two_factor_enable_title") c.PageIs("SettingsSecurity") var key *otp.Key var err error keyURL := c.Session.Get("twoFactorURL") if keyURL != nil { key, _ = otp.NewKeyFromURL(keyURL.(string)) } if key == nil { key, err = totp.Generate(totp.GenerateOpts{ Issuer: "Gitote", AccountName: c.User.Email, }) if err != nil { c.ServerError("Generate", err) return } } c.Data["TwoFactorSecret"] = key.Secret() img, err := key.Image(240, 240) if err != nil { c.ServerError("Image", err) return } var buf bytes.Buffer if err = png.Encode(&buf, img); err != nil { c.ServerError("Encode", err) return } c.Data["QRCode"] = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes())) c.Session.Set("twoFactorSecret", c.Data["TwoFactorSecret"]) c.Session.Set("twoFactorURL", key.String()) c.Success(SettingsTwoFactorEnableTPL) } // SettingsTwoFactorEnablePost updates user 2FA settings func SettingsTwoFactorEnablePost(c *context.Context) { secret, ok := c.Session.Get("twoFactorSecret").(string) if !ok { c.NotFound() return } if !totp.Validate(c.Query("passcode"), secret) { c.Flash.Error(c.Tr("settings.two_factor_invalid_passcode")) c.SubURLRedirect("/user/settings/security/two_factor_enable") return } if err := models.NewTwoFactor(c.UserID(), secret); err != nil { c.Flash.Error(c.Tr("settings.two_factor_enable_error", err)) c.SubURLRedirect("/user/settings/security/two_factor_enable") return } c.Session.Delete("twoFactorSecret") c.Session.Delete("twoFactorURL") c.Flash.Success(c.Tr("settings.two_factor_enable_success")) c.SubURLRedirect("/user/settings/security/two_factor_recovery_codes") } // SettingsTwoFactorRecoveryCodes shows user 2FA recovery settings page func SettingsTwoFactorRecoveryCodes(c *context.Context) { if !c.User.IsEnabledTwoFactor() { c.NotFound() return } c.Title("settings.two_factor_recovery_codes_title") c.PageIs("SettingsSecurity") recoveryCodes, err := models.GetRecoveryCodesByUserID(c.UserID()) if err != nil { c.ServerError("GetRecoveryCodesByUserID", err) return } c.Data["RecoveryCodes"] = recoveryCodes c.Success(SettingsTwoFactorRecoveryCodesTPL) } // SettingsTwoFactorRecoveryCodesPost updates user 2FA recovery settings func SettingsTwoFactorRecoveryCodesPost(c *context.Context) { if !c.User.IsEnabledTwoFactor() { c.NotFound() return } if err := models.RegenerateRecoveryCodes(c.UserID()); err != nil { c.Flash.Error(c.Tr("settings.two_factor_regenerate_recovery_codes_error", err)) } else { c.Flash.Success(c.Tr("settings.two_factor_regenerate_recovery_codes_success")) } c.SubURLRedirect("/user/settings/security/two_factor_recovery_codes") } // SettingsTwoFactorDisable shows user disable 2FA settings page func SettingsTwoFactorDisable(c *context.Context) { if !c.User.IsEnabledTwoFactor() { c.NotFound() return } if err := models.DeleteTwoFactor(c.UserID()); err != nil { c.ServerError("DeleteTwoFactor", err) return } c.Flash.Success(c.Tr("settings.two_factor_disable_success")) c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/security", }) } // SettingsRepos shows user repo settings page func SettingsRepos(c *context.Context) { c.Title("settings.repos") c.PageIs("SettingsRepositories") repos, err := models.GetUserAndCollaborativeRepositories(c.User.ID) if err != nil { c.ServerError("GetUserAndCollaborativeRepositories", err) return } if err = models.RepositoryList(repos).LoadAttributes(); err != nil { c.ServerError("LoadAttributes", err) return } c.Data["Repos"] = repos c.Success(SettingsRepositoriesTPL) } // SettingsLeaveRepo shows user leave repo settings page func SettingsLeaveRepo(c *context.Context) { repo, err := models.GetRepositoryByID(c.QueryInt64("id")) if err != nil { c.NotFoundOrServerError("GetRepositoryByID", errors.IsRepoNotExist, err) return } if err = repo.DeleteCollaboration(c.User.ID); err != nil { c.ServerError("DeleteCollaboration", err) return } c.Flash.Success(c.Tr("settings.repos.leave_success", repo.FullName())) c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/repositories", }) } // SettingsOrganizations shows org settings page func SettingsOrganizations(c *context.Context) { c.Title("settings.orgs") c.PageIs("SettingsOrganizations") orgs, err := models.GetOrgsByUserID(c.User.ID, true) if err != nil { c.ServerError("GetOrgsByUserID", err) return } c.Data["Orgs"] = orgs c.Success(SettingsOrganizationsTPL) } // SettingsLeaveOrganization shows user leave org settings page func SettingsLeaveOrganization(c *context.Context) { if err := models.RemoveOrgUser(c.QueryInt64("id"), c.User.ID); err != nil { if models.IsErrLastOrgOwner(err) { c.Flash.Error(c.Tr("form.last_org_owner")) } else { c.ServerError("RemoveOrgUser", err) return } } c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/organizations", }) } // SettingsApplications shows application settings page func SettingsApplications(c *context.Context) { c.Title("settings.applications") c.PageIs("SettingsApplications") tokens, err := models.ListAccessTokens(c.User.ID) if err != nil { c.ServerError("ListAccessTokens", err) return } c.Data["Tokens"] = tokens c.Success(SettingsApplicationsTPL) } // SettingsApplicationsPost updates application settings func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) { c.Title("settings.applications") c.PageIs("SettingsApplications") if c.HasError() { tokens, err := models.ListAccessTokens(c.User.ID) if err != nil { c.ServerError("ListAccessTokens", err) return } c.Data["Tokens"] = tokens c.Success(SettingsApplicationsTPL) return } t := &models.AccessToken{ UID: c.User.ID, Name: f.Name, } if err := models.NewAccessToken(t); err != nil { c.ServerError("NewAccessToken", err) return } c.Flash.Success(c.Tr("settings.generate_token_succeeds")) c.Flash.Info(t.Sha1) c.SubURLRedirect("/user/settings/applications") } // SettingsDeleteApplication shows delete application settings page func SettingsDeleteApplication(c *context.Context) { if err := models.DeleteAccessTokenOfUserByID(c.User.ID, c.QueryInt64("id")); err != nil { c.Flash.Error("DeleteAccessTokenByID: " + err.Error()) } else { c.Flash.Success(c.Tr("settings.delete_token_success")) } c.JSONSuccess(map[string]interface{}{ "redirect": setting.AppSubURL + "/user/settings/applications", }) } // SettingsEmbeds shows user embed settings page func SettingsEmbeds(c *context.Context) { c.Title("settings.embeds") c.PageIs("SettingsEmbeds") c.Success(SettingsEmbedsTPL) } // SettingsDelete shows user delete settings page func SettingsDelete(c *context.Context) { c.Title("settings.delete") c.PageIs("SettingsDelete") if c.Req.Method == "POST" { if _, err := models.UserLogin(c.User.Name, c.Query("password"), c.User.LoginSource); err != nil { if errors.IsUserNotExist(err) { c.RenderWithErr(c.Tr("form.enterred_invalid_password"), SettingsDeleteTPL, nil) } else { c.ServerError("UserLogin", err) } return } if err := models.DeleteUser(c.User); err != nil { switch { case models.IsErrUserOwnRepos(err): c.Flash.Error(c.Tr("form.still_own_repo")) c.Redirect(setting.AppSubURL + "/user/settings/delete") case models.IsErrUserHasOrgs(err): c.Flash.Error(c.Tr("form.still_has_org")) c.Redirect(setting.AppSubURL + "/user/settings/delete") default: c.ServerError("DeleteUser", err) } } else { log.Trace("Account deleted: %s", c.User.Name) c.Redirect(setting.AppSubURL + "/") } return } c.Success(SettingsDeleteTPL) }