From 9e865cee67728486959a4fcbbf2112a57df7e331 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Mon, 16 Oct 2017 17:14:12 +0800
Subject: [PATCH] Merge password and 2fa page on user settings (#2695)

* merge password and 2fa page on user settings
---
 integrations/links_test.go            |  4 +-
 options/locale/locale_en-US.ini       |  1 +
 routers/routes/routes.go              |  7 +--
 routers/user/setting.go               | 67 ++++++++++-------------
 templates/user/settings/navbar.tmpl   |  7 +--
 templates/user/settings/password.tmpl | 41 --------------
 templates/user/settings/security.tmpl | 79 +++++++++++++++++++++++++++
 7 files changed, 117 insertions(+), 89 deletions(-)
 delete mode 100644 templates/user/settings/password.tmpl
 create mode 100644 templates/user/settings/security.tmpl

diff --git a/integrations/links_test.go b/integrations/links_test.go
index bab147bdd9..d9d647f4b9 100644
--- a/integrations/links_test.go
+++ b/integrations/links_test.go
@@ -71,11 +71,11 @@ func testLinksAsUser(userName string, t *testing.T) {
 		"/user2?tab=activity",
 		"/user/settings",
 		"/user/settings/avatar",
-		"/user/settings/password",
+		"/user/settings/security",
+		"/user/settings/security/two_factor/enroll",
 		"/user/settings/email",
 		"/user/settings/keys",
 		"/user/settings/applications",
-		"/user/settings/two_factor",
 		"/user/settings/account_link",
 		"/user/settings/organization",
 		"/user/settings/delete",
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index fede39484c..f664827821 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -303,6 +303,7 @@ form.name_pattern_not_allowed = The username pattern '%s' is not allowed.
 [settings]
 profile = Profile
 password = Password
+security = Security
 avatar = Avatar
 ssh_gpg_keys = SSH / GPG Keys
 social = Social Accounts
diff --git a/routers/routes/routes.go b/routers/routes/routes.go
index a6f73aaedd..749f8263f8 100644
--- a/routers/routes/routes.go
+++ b/routers/routes/routes.go
@@ -220,8 +220,8 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Combo("/email").Get(user.SettingsEmails).
 			Post(bindIgnErr(auth.AddEmailForm{}), user.SettingsEmailPost)
 		m.Post("/email/delete", user.DeleteEmail)
-		m.Get("/password", user.SettingsPassword)
-		m.Post("/password", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsPasswordPost)
+		m.Get("/security", user.SettingsSecurity)
+		m.Post("/security", bindIgnErr(auth.ChangePasswordForm{}), user.SettingsSecurityPost)
 		m.Group("/openid", func() {
 			m.Combo("").Get(user.SettingsOpenID).
 				Post(bindIgnErr(auth.AddOpenIDForm{}), user.SettingsOpenIDPost)
@@ -238,8 +238,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 		m.Combo("/account_link").Get(user.SettingsAccountLinks).Post(user.SettingsDeleteAccountLink)
 		m.Get("/organization", user.SettingsOrganization)
 		m.Get("/repos", user.SettingsRepos)
-		m.Group("/two_factor", func() {
-			m.Get("", user.SettingsTwoFactor)
+		m.Group("/security/two_factor", func() {
 			m.Post("/regenerate_scratch", user.SettingsTwoFactorRegenerateScratch)
 			m.Post("/disable", user.SettingsTwoFactorDisable)
 			m.Get("/enroll", user.SettingsTwoFactorEnroll)
diff --git a/routers/user/setting.go b/routers/user/setting.go
index b71b29ba21..a00f3f287a 100644
--- a/routers/user/setting.go
+++ b/routers/user/setting.go
@@ -41,7 +41,7 @@ const (
 	tplSettingsOrganization base.TplName = "user/settings/organization"
 	tplSettingsRepositories base.TplName = "user/settings/repos"
 	tplSettingsDelete       base.TplName = "user/settings/delete"
-	tplSecurity             base.TplName = "user/security"
+	tplSettingsSecurity     base.TplName = "user/settings/security"
 )
 
 // Settings render user's profile page
@@ -191,22 +191,35 @@ func SettingsDeleteAvatar(ctx *context.Context) {
 	ctx.Redirect(setting.AppSubURL + "/user/settings/avatar")
 }
 
-// SettingsPassword render change user's password page
-func SettingsPassword(ctx *context.Context) {
+// SettingsSecurity render change user's password page and 2FA
+func SettingsSecurity(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsPassword"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 	ctx.Data["Email"] = ctx.User.Email
-	ctx.HTML(200, tplSettingsPassword)
+
+	enrolled := true
+	_, err := models.GetTwoFactorByUID(ctx.User.ID)
+	if err != nil {
+		if models.IsErrTwoFactorNotEnrolled(err) {
+			enrolled = false
+		} else {
+			ctx.Handle(500, "SettingsTwoFactor", err)
+			return
+		}
+	}
+
+	ctx.Data["TwofaEnrolled"] = enrolled
+	ctx.HTML(200, tplSettingsSecurity)
 }
 
-// SettingsPasswordPost response for change user's password
-func SettingsPasswordPost(ctx *context.Context, form auth.ChangePasswordForm) {
+// SettingsSecurityPost response for change user's password
+func SettingsSecurityPost(ctx *context.Context, form auth.ChangePasswordForm) {
 	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsPassword"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 	ctx.Data["PageIsSettingsDelete"] = true
 
 	if ctx.HasError() {
-		ctx.HTML(200, tplSettingsPassword)
+		ctx.HTML(200, tplSettingsSecurity)
 		return
 	}
 
@@ -230,7 +243,7 @@ func SettingsPasswordPost(ctx *context.Context, form auth.ChangePasswordForm) {
 		ctx.Flash.Success(ctx.Tr("settings.change_password_success"))
 	}
 
-	ctx.Redirect(setting.AppSubURL + "/user/settings/password")
+	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
 }
 
 // SettingsEmails render user's emails page
@@ -509,30 +522,10 @@ func SettingsDeleteApplication(ctx *context.Context) {
 	})
 }
 
-// SettingsTwoFactor renders the 2FA page.
-func SettingsTwoFactor(ctx *context.Context) {
-	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsTwofa"] = true
-
-	enrolled := true
-	_, err := models.GetTwoFactorByUID(ctx.User.ID)
-	if err != nil {
-		if models.IsErrTwoFactorNotEnrolled(err) {
-			enrolled = false
-		} else {
-			ctx.Handle(500, "SettingsTwoFactor", err)
-			return
-		}
-	}
-
-	ctx.Data["TwofaEnrolled"] = enrolled
-	ctx.HTML(200, tplSettingsTwofa)
-}
-
 // SettingsTwoFactorRegenerateScratch regenerates the user's 2FA scratch code.
 func SettingsTwoFactorRegenerateScratch(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsTwofa"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 
 	t, err := models.GetTwoFactorByUID(ctx.User.ID)
 	if err != nil {
@@ -551,13 +544,13 @@ func SettingsTwoFactorRegenerateScratch(ctx *context.Context) {
 	}
 
 	ctx.Flash.Success(ctx.Tr("settings.twofa_scratch_token_regenerated", t.ScratchToken))
-	ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
+	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["PageIsSettingsTwofa"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 
 	t, err := models.GetTwoFactorByUID(ctx.User.ID)
 	if err != nil {
@@ -571,7 +564,7 @@ func SettingsTwoFactorDisable(ctx *context.Context) {
 	}
 
 	ctx.Flash.Success(ctx.Tr("settings.twofa_disabled"))
-	ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
+	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
 }
 
 func twofaGenerateSecretAndQr(ctx *context.Context) bool {
@@ -615,7 +608,7 @@ func twofaGenerateSecretAndQr(ctx *context.Context) bool {
 // SettingsTwoFactorEnroll shows the page where the user can enroll into 2FA.
 func SettingsTwoFactorEnroll(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsTwofa"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 
 	t, err := models.GetTwoFactorByUID(ctx.User.ID)
 	if t != nil {
@@ -638,7 +631,7 @@ func SettingsTwoFactorEnroll(ctx *context.Context) {
 // SettingsTwoFactorEnrollPost handles enrolling the user into 2FA.
 func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthForm) {
 	ctx.Data["Title"] = ctx.Tr("settings")
-	ctx.Data["PageIsSettingsTwofa"] = true
+	ctx.Data["PageIsSettingsSecurity"] = true
 
 	t, err := models.GetTwoFactorByUID(ctx.User.ID)
 	if t != nil {
@@ -691,7 +684,7 @@ func SettingsTwoFactorEnrollPost(ctx *context.Context, form auth.TwoFactorAuthFo
 	ctx.Session.Delete("twofaSecret")
 	ctx.Session.Delete("twofaUri")
 	ctx.Flash.Success(ctx.Tr("settings.twofa_enrolled", t.ScratchToken))
-	ctx.Redirect(setting.AppSubURL + "/user/settings/two_factor")
+	ctx.Redirect(setting.AppSubURL + "/user/settings/security")
 }
 
 // SettingsAccountLinks render the account links settings page
diff --git a/templates/user/settings/navbar.tmpl b/templates/user/settings/navbar.tmpl
index b0a3c9fcc3..a6c497794f 100644
--- a/templates/user/settings/navbar.tmpl
+++ b/templates/user/settings/navbar.tmpl
@@ -5,8 +5,8 @@
 	<a class="{{if .PageIsSettingsAvatar}}active{{end}} item" href="{{AppSubUrl}}/user/settings/avatar">
 		{{.i18n.Tr "settings.avatar"}}
 	</a>
-	<a class="{{if .PageIsSettingsPassword}}active{{end}} item" href="{{AppSubUrl}}/user/settings/password">
-		{{.i18n.Tr "settings.password"}}
+	<a class="{{if .PageIsSettingsSecurity}}active{{end}} item" href="{{AppSubUrl}}/user/settings/security">
+		{{.i18n.Tr "settings.security"}}
 	</a>
 	<a class="{{if .PageIsSettingsEmails}}active{{end}} item" href="{{AppSubUrl}}/user/settings/email">
 		{{.i18n.Tr "settings.emails"}}
@@ -22,9 +22,6 @@
 	<a class="{{if .PageIsSettingsApplications}}active{{end}} item" href="{{AppSubUrl}}/user/settings/applications">
 		{{.i18n.Tr "settings.applications"}}
 	</a>
-	<a class="{{if .PageIsSettingsTwofa}}active{{end}} item" href="{{AppSubUrl}}/user/settings/two_factor">
-		{{.i18n.Tr "settings.twofa"}}
-	</a>
 	<a class="{{if .PageIsSettingsAccountLink}}active{{end}} item" href="{{AppSubUrl}}/user/settings/account_link">
 		{{.i18n.Tr "settings.account_link"}}
 	</a>
diff --git a/templates/user/settings/password.tmpl b/templates/user/settings/password.tmpl
deleted file mode 100644
index 53872c901d..0000000000
--- a/templates/user/settings/password.tmpl
+++ /dev/null
@@ -1,41 +0,0 @@
-{{template "base/head" .}}
-<div class="user settings password">
-	{{template "user/settings/navbar" .}}
-	<div class="ui container">
-		{{template "base/alert" .}}
-		<h4 class="ui top attached header">
-			{{.i18n.Tr "settings.change_password"}}
-		</h4>
-		<div class="ui attached segment">
-			{{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}}
-			<form class="ui form" action="{{.Link}}" method="post">
-				{{.CsrfTokenHtml}}
-				{{if .SignedUser.IsPasswordSet}}
-				<div class="required field {{if .Err_OldPassword}}error{{end}}">
-					<label for="old_password">{{.i18n.Tr "settings.old_password"}}</label>
-					<input id="old_password" name="old_password" type="password" autocomplete="off" autofocus required>
-				</div>
-				{{end}}
-				<div class="required field {{if .Err_Password}}error{{end}}">
-					<label for="password">{{.i18n.Tr "settings.new_password"}}</label>
-					<input id="password" name="password" type="password" autocomplete="off" required>
-				</div>
-				<div class="required field {{if .Err_Password}}error{{end}}">
-					<label for="retype">{{.i18n.Tr "settings.retype_new_password"}}</label>
-					<input id="retype" name="retype" type="password" autocomplete="off" required>
-				</div>
-
-				<div class="field">
-					<button class="ui green button">{{$.i18n.Tr "settings.change_password"}}</button>
-					<a href="{{AppSubUrl}}/user/forgot_password?email={{.Email}}">{{.i18n.Tr "auth.forgot_password"}}</a>
-				</div>
-			</form>
-			{{else}}
-			<div class="ui info message">
-				<p class="text left">{{$.i18n.Tr "settings.password_change_disabled"}}</p>
-			</div>
-			{{end}}
-		</div>
-	</div>
-</div>
-{{template "base/footer" .}}
diff --git a/templates/user/settings/security.tmpl b/templates/user/settings/security.tmpl
new file mode 100644
index 0000000000..a956a3fcb3
--- /dev/null
+++ b/templates/user/settings/security.tmpl
@@ -0,0 +1,79 @@
+{{template "base/head" .}}
+<div class="user settings password">
+	{{template "user/settings/navbar" .}}
+	<div class="ui container">
+		{{template "base/alert" .}}
+		<h4 class="ui top attached header">
+			{{.i18n.Tr "settings.password"}}
+		</h4>
+		<div class="ui attached segment">
+			{{if or (.SignedUser.IsLocal) (.SignedUser.IsOAuth2)}}
+			<form class="ui form" action="{{.Link}}?tp=password" method="post">
+				{{.CsrfTokenHtml}}
+				{{if .SignedUser.IsPasswordSet}}
+				<div class="required field {{if .Err_OldPassword}}error{{end}}">
+					<label for="old_password">{{.i18n.Tr "settings.old_password"}}</label>
+					<input id="old_password" name="old_password" type="password" autocomplete="off" autofocus required>
+				</div>
+				{{end}}
+				<div class="required field {{if .Err_Password}}error{{end}}">
+					<label for="password">{{.i18n.Tr "settings.new_password"}}</label>
+					<input id="password" name="password" type="password" autocomplete="off" required>
+				</div>
+				<div class="required field {{if .Err_Password}}error{{end}}">
+					<label for="retype">{{.i18n.Tr "settings.retype_new_password"}}</label>
+					<input id="retype" name="retype" type="password" autocomplete="off" required>
+				</div>
+
+				<div class="field">
+					<button class="ui green button">{{$.i18n.Tr "settings.change_password"}}</button>
+					<a href="{{AppSubUrl}}/user/forgot_password?email={{.Email}}">{{.i18n.Tr "auth.forgot_password"}}</a>
+				</div>
+			</form>
+			{{else}}
+			<div class="ui info message">
+				<p class="text left">{{$.i18n.Tr "settings.password_change_disabled"}}</p>
+			</div>
+			{{end}}
+		</div>
+        <br/>
+
+        <h4 class="ui top attached header">
+			{{.i18n.Tr "settings.twofa"}}
+		</h4>
+        <div class="ui attached segment">
+            <p>{{.i18n.Tr "settings.twofa_desc"}}</p>
+            {{if .TwofaEnrolled}}
+            <p>{{$.i18n.Tr "settings.twofa_is_enrolled" | Str2html }}</p>
+            <form class="ui form" action="{{.Link}}/two_factor/regenerate_scratch" method="post" enctype="multipart/form-data">
+                {{.CsrfTokenHtml}}
+                <p>{{.i18n.Tr "settings.regenerate_scratch_token_desc"}}</p>
+                <button class="ui blue button">{{$.i18n.Tr "settings.twofa_scratch_token_regenerate"}}</button>
+            </form>
+            <form class="ui form" action="{{.Link}}/two_factor/disable" method="post" enctype="multipart/form-data" id="disable-form">
+                {{.CsrfTokenHtml}}
+                <p>{{.i18n.Tr "settings.twofa_disable_note"}}</p>
+                <div class="ui red button delete-button" data-type="form" data-form="#disable-form">{{$.i18n.Tr "settings.twofa_disable"}}</div>
+            </form>
+            {{else}}
+            <p>{{.i18n.Tr "settings.twofa_not_enrolled"}}</p>
+            <div class="inline field">
+                <a class="ui green button" href="{{.Link}}/two_factor/enroll">{{$.i18n.Tr "settings.twofa_enroll"}}</a>
+            </div>
+            {{end}}
+        </div>
+	</div>
+</div>
+
+<div class="ui small basic delete modal">
+	<div class="ui icon header">
+		<i class="trash icon"></i>
+		{{.i18n.Tr "settings.twofa_disable"}}
+	</div>
+	<div class="content">
+		<p>{{.i18n.Tr "settings.twofa_disable_desc"}}</p>
+	</div>
+	{{template "base/delete_modal_actions" .}}
+</div>
+
+{{template "base/footer" .}}