Browse Source

add personal access token panel #12

Unknwon 10 years ago
parent
commit
8c9338a537

+ 2 - 4
cmd/web.go

@@ -64,7 +64,7 @@ func checkVersion() {
 
 	// Check dependency version.
 	macaronVer := git.MustParseVersion(strings.Join(strings.Split(macaron.Version(), ".")[:3], "."))
-	if macaronVer.LessThan(git.MustParseVersion("0.4.0")) {
+	if macaronVer.LessThan(git.MustParseVersion("0.4.2")) {
 		log.Fatal(4, "Package macaron version is too old, did you forget to update?(github.com/Unknwon/macaron)")
 	}
 	i18nVer := git.MustParseVersion(i18n.Version())
@@ -199,6 +199,7 @@ func runWeb(*cli.Context) {
 		m.Get("/ssh", user.SettingsSSHKeys)
 		m.Post("/ssh", bindIgnErr(auth.AddSSHKeyForm{}), user.SettingsSSHKeysPost)
 		m.Get("/social", user.SettingsSocial)
+		m.Combo("/applications").Get(user.SettingsApplications).Post(bindIgnErr(auth.NewAccessTokenForm{}), user.SettingsApplicationsPost)
 		m.Route("/delete", "GET,POST", user.SettingsDelete)
 	}, reqSignIn)
 	m.Group("/user", func() {
@@ -210,9 +211,6 @@ func runWeb(*cli.Context) {
 		m.Get("/logout", user.SignOut)
 	})
 
-	// FIXME: Legacy
-	m.Get("/user/:username", ignSignIn, user.Profile)
-
 	// Gravatar service.
 	avt := avatar.CacheServer("public/img/avatar/", "public/img/avatar_default.jpg")
 	os.MkdirAll("public/img/avatar/", os.ModePerm)

+ 11 - 0
conf/locale/locale_en-US.ini

@@ -184,6 +184,7 @@ profile = Profile
 password = Password
 ssh_keys = SSH Keys
 social = Social Accounts
+applications = Applications
 orgs = Organizations
 delete = Delete Account
 uid = Uid
@@ -224,6 +225,16 @@ social_desc = This is a list of associated social accounts. Remove any binding t
 unbind = Unbind
 unbind_success = Social account has been unbound.
 
+manage_access_token = Manage Personal Access Tokens
+generate_new_token = Generate New Token
+tokens_desc = Tokens you have generated that can be used to access the Gogs API.
+new_token_desc = As for now, every token will have full access to your account.
+token_name = Token Name
+generate_token = Generate Token
+generate_token_succees = New access token has been generated successfully! Make sure to copy your new personal access token now. You won't be able to see it again!
+delete_token = Delete
+delete_token_success = Personal access token has been deleted successfully! Don't forget to update your applications as well.
+
 delete_account = Delete Your Account
 delete_prompt = The operation will delete your account permanently, and <strong>CANNOT</strong> be undone!
 confirm_delete_account = Confirm Deletion

+ 1 - 1
gogs.go

@@ -17,7 +17,7 @@ import (
 	"github.com/gogits/gogs/modules/setting"
 )
 
-const APP_VER = "0.5.7.1110 Beta"
+const APP_VER = "0.5.8.1112 Beta"
 
 func init() {
 	runtime.GOMAXPROCS(runtime.NumCPU())

+ 2 - 1
models/models.go

@@ -39,7 +39,8 @@ var (
 )
 
 func init() {
-	tables = append(tables, new(User), new(PublicKey), new(Follow), new(Oauth2),
+	tables = append(tables,
+		new(User), new(PublicKey), new(Follow), new(Oauth2), new(AccessToken),
 		new(Repository), new(Watch), new(Star), new(Action), new(Access),
 		new(Issue), new(Comment), new(Attachment), new(IssueUser), new(Label), new(Milestone),
 		new(Mirror), new(Release), new(LoginSource), new(Webhook),

+ 3 - 3
models/publickey.go

@@ -236,10 +236,10 @@ func GetPublicKeyById(keyId int64) (*PublicKey, error) {
 	return key, nil
 }
 
-// ListPublicKey returns a list of all public keys that user has.
-func ListPublicKey(uid int64) ([]*PublicKey, error) {
+// ListPublicKeys returns a list of public keys belongs to given user.
+func ListPublicKeys(uid int64) ([]*PublicKey, error) {
 	keys := make([]*PublicKey, 0, 5)
-	err := x.Find(&keys, &PublicKey{OwnerId: uid})
+	err := x.Where("owner_id=?", uid).Find(&keys)
 	if err != nil {
 		return nil, err
 	}

+ 69 - 0
models/token.go

@@ -0,0 +1,69 @@
+// 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 models
+
+import (
+	"errors"
+	"time"
+
+	"github.com/gogits/gogs/modules/base"
+	"github.com/gogits/gogs/modules/uuid"
+)
+
+var (
+	ErrAccessTokenNotExist = errors.New("Access token does not exist")
+)
+
+// AccessToken represents a personal access token.
+type AccessToken struct {
+	Id                int64
+	Uid               int64
+	Name              string
+	Sha1              string    `xorm:"UNIQUE VARCHAR(40)"`
+	Created           time.Time `xorm:"CREATED"`
+	Updated           time.Time
+	HasRecentActivity bool `xorm:"-"`
+	HasUsed           bool `xorm:"-"`
+}
+
+// NewAccessToken creates new access token.
+func NewAccessToken(t *AccessToken) error {
+	t.Sha1 = base.EncodeSha1(uuid.NewV4().String())
+	_, err := x.Insert(t)
+	return err
+}
+
+// GetAccessTokenBySha returns access token by given sha1.
+func GetAccessTokenBySha(sha string) (*AccessToken, error) {
+	t := &AccessToken{Sha1: sha}
+	has, err := x.Get(t)
+	if err != nil {
+		return nil, err
+	} else if !has {
+		return nil, ErrAccessTokenNotExist
+	}
+	return t, nil
+}
+
+// ListAccessTokens returns a list of access tokens belongs to given user.
+func ListAccessTokens(uid int64) ([]*AccessToken, error) {
+	tokens := make([]*AccessToken, 0, 5)
+	err := x.Where("uid=?", uid).Desc("id").Find(&tokens)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, t := range tokens {
+		t.HasUsed = t.Updated.After(t.Created)
+		t.HasRecentActivity = t.Updated.Add(7 * 24 * time.Hour).After(time.Now())
+	}
+	return tokens, nil
+}
+
+// DeleteAccessTokenById deletes access token by given ID.
+func DeleteAccessTokenById(id int64) error {
+	_, err := x.Id(id).Delete(new(AccessToken))
+	return err
+}

+ 23 - 5
modules/auth/auth.go

@@ -20,7 +20,7 @@ import (
 )
 
 // SignedInId returns the id of signed in user.
-func SignedInId(header http.Header, sess session.Store) int64 {
+func SignedInId(req *http.Request, sess session.Store) int64 {
 	if !models.HasEngine {
 		return 0
 	}
@@ -38,20 +38,38 @@ func SignedInId(header http.Header, sess session.Store) int64 {
 		}
 		return id
 	}
+
+	// API calls also need to check access token.
+	if strings.HasPrefix(req.URL.Path, "/api/") {
+		auHead := req.Header.Get("Authorization")
+		if len(auHead) > 0 {
+			auths := strings.Fields(auHead)
+			if len(auths) == 2 && auths[0] == "token" {
+				t, err := models.GetAccessTokenBySha(auths[1])
+				if err != nil {
+					if err != models.ErrAccessTokenNotExist {
+						log.Error(4, "GetAccessTokenBySha: %v", err)
+					}
+					return 0
+				}
+				return t.Uid
+			}
+		}
+	}
 	return 0
 }
 
 // SignedInUser returns the user object of signed user.
-func SignedInUser(header http.Header, sess session.Store) *models.User {
+func SignedInUser(req *http.Request, sess session.Store) *models.User {
 	if !models.HasEngine {
 		return nil
 	}
 
-	uid := SignedInId(header, sess)
+	uid := SignedInId(req, sess)
 
 	if uid <= 0 {
 		if setting.Service.EnableReverseProxyAuth {
-			webAuthUser := header.Get(setting.ReverseProxyAuthUser)
+			webAuthUser := req.Header.Get(setting.ReverseProxyAuthUser)
 			if len(webAuthUser) > 0 {
 				u, err := models.GetUserByName(webAuthUser)
 				if err != nil {
@@ -65,7 +83,7 @@ func SignedInUser(header http.Header, sess session.Store) *models.User {
 		}
 
 		// Check with basic auth.
-		baHead := header.Get("Authorization")
+		baHead := req.Header.Get("Authorization")
 		if len(baHead) > 0 {
 			auths := strings.Fields(baHead)
 			if len(auths) == 2 && auths[0] == "Basic" {

+ 0 - 19
modules/auth/publickey_form.go

@@ -1,19 +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 auth
-
-import (
-	"github.com/Unknwon/macaron"
-	"github.com/macaron-contrib/binding"
-)
-
-type AddSSHKeyForm struct {
-	SSHTitle string `form:"title" binding:"Required"`
-	Content  string `form:"content" binding:"Required"`
-}
-
-func (f *AddSSHKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
-	return validate(errs, ctx.Data, f, ctx.Locale)
-}

+ 17 - 0
modules/auth/user_form.go

@@ -95,3 +95,20 @@ type ChangePasswordForm struct {
 func (f *ChangePasswordForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
 	return validate(errs, ctx.Data, f, ctx.Locale)
 }
+
+type AddSSHKeyForm struct {
+	SSHTitle string `form:"title" binding:"Required"`
+	Content  string `form:"content" binding:"Required"`
+}
+
+func (f *AddSSHKeyForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}
+
+type NewAccessTokenForm struct {
+	Name string `form:"name" binding:"Required"`
+}
+
+func (f *NewAccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors {
+	return validate(errs, ctx.Data, f, ctx.Locale)
+}

+ 7 - 0
modules/base/tool.go

@@ -33,6 +33,13 @@ func EncodeMd5(str string) string {
 	return hex.EncodeToString(m.Sum(nil))
 }
 
+// Encode string to sha1 hex value.
+func EncodeSha1(str string) string {
+	h := sha1.New()
+	h.Write([]byte(str))
+	return hex.EncodeToString(h.Sum(nil))
+}
+
 func BasicAuthDecode(encoded string) (user string, name string, err error) {
 	var s []byte
 	s, err = base64.StdEncoding.DecodeString(encoded)

+ 1 - 1
modules/middleware/context.go

@@ -172,7 +172,7 @@ func Contexter() macaron.Handler {
 		ctx.Data["PageStartTime"] = time.Now()
 
 		// Get user from session if logined.
-		ctx.User = auth.SignedInUser(ctx.Req.Header, ctx.Session)
+		ctx.User = auth.SignedInUser(ctx.Req.Request, ctx.Session)
 
 		if ctx.User != nil {
 			ctx.IsSigned = true

+ 31 - 8
public/ng/css/gogs.css

@@ -1699,18 +1699,21 @@ The register and sign-in page style
 #repo-hooks-panel,
 #repo-hooks-history-panel,
 #user-social-panel,
+#user-applications-panel,
 #user-ssh-panel {
   margin-bottom: 20px;
 }
 #repo-hooks-panel .setting-list,
 #repo-hooks-history-panel .setting-list,
 #user-social-panel .setting-list,
+#user-applications-panel .setting-list,
 #user-ssh-panel .setting-list {
   background-color: #FFF;
 }
 #repo-hooks-panel .setting-list li,
 #repo-hooks-history-panel .setting-list li,
 #user-social-panel .setting-list li,
+#user-applications-panel .setting-list li,
 #user-ssh-panel .setting-list li {
   padding: 8px 20px;
   border-bottom: 1px solid #eaeaea;
@@ -1718,18 +1721,21 @@ The register and sign-in page style
 #repo-hooks-panel .setting-list li.ssh:hover,
 #repo-hooks-history-panel .setting-list li.ssh:hover,
 #user-social-panel .setting-list li.ssh:hover,
+#user-applications-panel .setting-list li.ssh:hover,
 #user-ssh-panel .setting-list li.ssh:hover {
   background-color: #ffffEE;
 }
 #repo-hooks-panel .setting-list li i,
 #repo-hooks-history-panel .setting-list li i,
 #user-social-panel .setting-list li i,
+#user-applications-panel .setting-list li i,
 #user-ssh-panel .setting-list li i {
   padding-right: 5px;
 }
 #repo-hooks-panel .active-icon,
 #repo-hooks-history-panel .active-icon,
 #user-social-panel .active-icon,
+#user-applications-panel .active-icon,
 #user-ssh-panel .active-icon {
   width: 10px;
   height: 10px;
@@ -1741,43 +1747,60 @@ The register and sign-in page style
 #repo-hooks-panel .ssh-content,
 #repo-hooks-history-panel .ssh-content,
 #user-social-panel .ssh-content,
+#user-applications-panel .ssh-content,
 #user-ssh-panel .ssh-content {
   margin-left: 24px;
 }
 #repo-hooks-panel .ssh-content .octicon,
 #repo-hooks-history-panel .ssh-content .octicon,
 #user-social-panel .ssh-content .octicon,
+#user-applications-panel .ssh-content .octicon,
 #user-ssh-panel .ssh-content .octicon {
   margin-right: 4px;
 }
 #repo-hooks-panel .ssh-content .print,
 #repo-hooks-history-panel .ssh-content .print,
 #user-social-panel .ssh-content .print,
+#user-applications-panel .ssh-content .print,
 #user-ssh-panel .ssh-content .print,
+#repo-hooks-panel .ssh-content .access,
+#repo-hooks-history-panel .ssh-content .access,
+#user-social-panel .ssh-content .access,
+#user-applications-panel .ssh-content .access,
+#user-ssh-panel .ssh-content .access,
 #repo-hooks-panel .ssh-content .activity,
 #repo-hooks-history-panel .ssh-content .activity,
 #user-social-panel .ssh-content .activity,
+#user-applications-panel .ssh-content .activity,
 #user-ssh-panel .ssh-content .activity {
   color: #888;
 }
-#repo-hooks-panel .ssh-delete-btn,
-#repo-hooks-history-panel .ssh-delete-btn,
-#user-social-panel .ssh-delete-btn,
-#user-ssh-panel .ssh-delete-btn {
+#repo-hooks-panel .ssh-content .access,
+#repo-hooks-history-panel .ssh-content .access,
+#user-social-panel .ssh-content .access,
+#user-applications-panel .ssh-content .access,
+#user-ssh-panel .ssh-content .access {
+  max-width: 500px;
+}
+#repo-hooks-panel .ssh-btn,
+#repo-hooks-history-panel .ssh-btn,
+#user-social-panel .ssh-btn,
+#user-applications-panel .ssh-btn,
+#user-ssh-panel .ssh-btn {
   margin-top: 6px;
 }
-#user-ssh-add-form .panel-body {
+.form-settings-add .panel-body {
   background-color: #FFF;
   padding: 30px 0;
 }
-#user-ssh-add-form .ipt {
+.form-settings-add .ipt {
   width: 500px;
 }
-#user-ssh-add-form textarea {
+.form-settings-add textarea {
   height: 120px;
   margin-left: 3px;
 }
-#user-ssh-add-form .field {
+.form-settings-add .field {
   margin-bottom: 24px;
 }
 .pr-main {

+ 7 - 4
public/ng/js/gogs.js

@@ -300,8 +300,11 @@ function initCore() {
         $.magnificPopup.close();
     });
 
-    // Collapse.
+    // Plugins.
     $('.collapse').hide();
+    $('.tipsy-tooltip').tipsy({
+        fade: true
+    });
 }
 
 function initUserSetting() {
@@ -329,9 +332,9 @@ function initUserSetting() {
         $profile_form.submit();
     });
 
-    // Show add SSH key panel.
-    $('#ssh-add').click(function () {
-        $('#user-ssh-add-form').removeClass("hide");
+    // Show panels.
+    $('.show-form-btn').click(function () {
+        $($(this).data('target-form')).removeClass("hide");
     });
 
     // Confirmation of delete account.

File diff suppressed because it is too large
+ 0 - 0
public/ng/js/min/gogs-min.js


+ 7 - 2
public/ng/less/gogs/settings.less

@@ -68,6 +68,7 @@
 #repo-hooks-panel,
 #repo-hooks-history-panel,
 #user-social-panel,
+#user-applications-panel,
 #user-ssh-panel {
     margin-bottom: 20px;
     .setting-list {
@@ -97,16 +98,20 @@
             margin-right: 4px;
         }
         .print,
+        .access,
         .activity {
             color: #888;
         }
+        .access {
+            max-width: 500px;
+        }
     }
-    .ssh-delete-btn {
+    .ssh-btn {
         margin-top: 6px;
     }
 }
 
-#user-ssh-add-form {
+.form-settings-add {
   .panel-body {
     background-color: #FFF;
     padding: 30px 0;

+ 66 - 9
routers/user/setting.go

@@ -18,13 +18,14 @@ import (
 )
 
 const (
-	SETTINGS_PROFILE  base.TplName = "user/settings/profile"
-	SETTINGS_PASSWORD base.TplName = "user/settings/password"
-	SETTINGS_SSH_KEYS base.TplName = "user/settings/sshkeys"
-	SETTINGS_SOCIAL   base.TplName = "user/settings/social"
-	SETTINGS_DELETE   base.TplName = "user/settings/delete"
-	NOTIFICATION      base.TplName = "user/notification"
-	SECURITY          base.TplName = "user/security"
+	SETTINGS_PROFILE      base.TplName = "user/settings/profile"
+	SETTINGS_PASSWORD     base.TplName = "user/settings/password"
+	SETTINGS_SSH_KEYS     base.TplName = "user/settings/sshkeys"
+	SETTINGS_SOCIAL       base.TplName = "user/settings/social"
+	SETTINGS_APPLICATIONS base.TplName = "user/settings/applications"
+	SETTINGS_DELETE       base.TplName = "user/settings/delete"
+	NOTIFICATION          base.TplName = "user/notification"
+	SECURITY              base.TplName = "user/security"
 )
 
 func Settings(ctx *middleware.Context) {
@@ -129,7 +130,7 @@ func SettingsSSHKeys(ctx *middleware.Context) {
 	ctx.Data["PageIsSettingsSSHKeys"] = true
 
 	var err error
-	ctx.Data["Keys"], err = models.ListPublicKey(ctx.User.Id)
+	ctx.Data["Keys"], err = models.ListPublicKeys(ctx.User.Id)
 	if err != nil {
 		ctx.Handle(500, "ssh.ListPublicKey", err)
 		return
@@ -144,7 +145,7 @@ func SettingsSSHKeysPost(ctx *middleware.Context, form auth.AddSSHKeyForm) {
 	ctx.Data["PageIsSettingsSSHKeys"] = true
 
 	var err error
-	ctx.Data["Keys"], err = models.ListPublicKey(ctx.User.Id)
+	ctx.Data["Keys"], err = models.ListPublicKeys(ctx.User.Id)
 	if err != nil {
 		ctx.Handle(500, "ssh.ListPublicKey", err)
 		return
@@ -235,6 +236,62 @@ func SettingsSocial(ctx *middleware.Context) {
 	ctx.HTML(200, SETTINGS_SOCIAL)
 }
 
+func SettingsApplications(ctx *middleware.Context) {
+	ctx.Data["Title"] = ctx.Tr("settings")
+	ctx.Data["PageIsUserSettings"] = true
+	ctx.Data["PageIsSettingsApplications"] = true
+
+	// Delete access token.
+	remove, _ := com.StrTo(ctx.Query("remove")).Int64()
+	if remove > 0 {
+		if err := models.DeleteAccessTokenById(remove); err != nil {
+			ctx.Handle(500, "DeleteAccessTokenById", err)
+			return
+		}
+		ctx.Flash.Success(ctx.Tr("settings.delete_token_success"))
+		ctx.Redirect(setting.AppSubUrl + "/user/settings/applications")
+		return
+	}
+
+	tokens, err := models.ListAccessTokens(ctx.User.Id)
+	if err != nil {
+		ctx.Handle(500, "ListAccessTokens", err)
+		return
+	}
+	ctx.Data["Tokens"] = tokens
+
+	ctx.HTML(200, SETTINGS_APPLICATIONS)
+}
+
+// FIXME: split to two different functions and pages to handle access token and oauth2
+func SettingsApplicationsPost(ctx *middleware.Context, form auth.NewAccessTokenForm) {
+	ctx.Data["Title"] = ctx.Tr("settings")
+	ctx.Data["PageIsUserSettings"] = true
+	ctx.Data["PageIsSettingsApplications"] = true
+
+	switch ctx.Query("type") {
+	case "token":
+		if ctx.HasError() {
+			ctx.HTML(200, SETTINGS_APPLICATIONS)
+			return
+		}
+
+		t := &models.AccessToken{
+			Uid:  ctx.User.Id,
+			Name: form.Name,
+		}
+		if err := models.NewAccessToken(t); err != nil {
+			ctx.Handle(500, "NewAccessToken", err)
+			return
+		}
+
+		ctx.Flash.Success(ctx.Tr("settings.generate_token_succees"))
+		ctx.Flash.Info(t.Sha1)
+	}
+
+	ctx.Redirect(setting.AppSubUrl + "/user/settings/applications")
+}
+
 func SettingsDelete(ctx *middleware.Context) {
 	ctx.Data["Title"] = ctx.Tr("settings")
 	ctx.Data["PageIsUserSettings"] = true

+ 1 - 1
templates/.VERSION

@@ -1 +1 @@
-0.5.7.1110 Beta
+0.5.8.1112 Beta

+ 56 - 0
templates/user/settings/applications.tmpl

@@ -0,0 +1,56 @@
+{{template "ng/base/head" .}}
+{{template "ng/base/header" .}}
+<div id="setting-wrapper" class="main-wrapper">
+    <div id="user-profile-setting" class="container clear">
+        {{template "user/settings/nav" .}}
+        <div class="grid-4-5 left">
+            <div class="setting-content">
+                {{template "ng/base/alert" .}}
+                <div id="setting-content">
+                    <div id="user-applications-panel" class="panel panel-radius">
+                        <div class="panel-header">
+                        	<a class="show-form-btn" data-target-form="#access-add-form">
+                        		<button class="btn btn-medium btn-black btn-radius right">{{.i18n.Tr "settings.generate_new_token"}}</button>
+                        	</a>
+                        	<strong>{{.i18n.Tr "settings.manage_access_token"}}</strong>
+                        </div>
+                        <ul class="panel-body setting-list">
+                            <li>{{.i18n.Tr "settings.tokens_desc"}}</li>
+                            {{range .Tokens}}
+                            <li class="ssh clear">
+                                <span class="active-icon left label label-{{if .HasRecentActivity}}green{{else}}gray{{end}} label-radius"></span>
+                                <i class="fa fa-send fa-2x left"></i>
+                                <div class="ssh-content left">
+                                    <p><strong>{{.Name}}</strong></p>
+                                    <p class="activity"><i>{{$.i18n.Tr "settings.add_on"}} {{DateFormat .Created "M d, Y"}} —  <i class="octicon octicon-info"></i>{{if .HasUsed}}{{$.i18n.Tr "settings.last_used"}} {{DateFormat .Updated "M d, Y"}}{{else}}{{$.i18n.Tr "settings.no_activity"}}{{end}}</i></p>
+                                </div>
+                                <a href="{{AppSubUrl}}/user/settings/applications?remove={{.Id}}">
+                                	<button class="btn btn-small btn-red btn-radius ssh-btn right">{{$.i18n.Tr "settings.delete_token"}}</button>
+                                </a>
+                            </li>
+                            {{end}}
+                        </ul>
+                    </div>
+                    <br>
+                    <form class="panel panel-radius form form-align form-settings-add hide" id="access-add-form" action="{{AppSubUrl}}/user/settings/applications" method="post">
+                        {{.CsrfTokenHtml}}
+                        <p class="panel-header"><strong>{{.i18n.Tr "settings.generate_new_token"}}</strong></p>
+                        <div class="panel-body">
+                        	<div class="text-center panel-desc">{{.i18n.Tr "settings.new_token_desc"}}</div>
+                        	<input type="hidden" name="type" value="token">
+                            <p class="field">
+                                <label class="req" for="token-name">{{.i18n.Tr "settings.token_name"}}</label>
+                                <input class="ipt ipt-radius" id="token-name" name="name" required />
+                            </p>
+                            <p class="field">
+                                <label></label>
+                                <button class="btn btn-green btn-medium btn-radius" id="ssh-add-btn">{{.i18n.Tr "settings.generate_token"}}</button>
+                            </p>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+{{template "ng/base/footer" .}}

+ 1 - 0
templates/user/settings/nav.tmpl

@@ -6,6 +6,7 @@
             <li {{if .PageIsSettingsPassword}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/password">{{.i18n.Tr "settings.password"}}</a></li>
             <li {{if .PageIsSettingsSSHKeys}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/ssh">{{.i18n.Tr "settings.ssh_keys"}}</a></li>
             <li {{if .PageIsSettingsSocial}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/social">{{.i18n.Tr "settings.social"}}</a></li>
+            <li {{if .PageIsSettingsApplications}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/applications">{{.i18n.Tr "settings.applications"}}</a></li>
             <li {{if .PageIsSettingsDelete}}class="current"{{end}}><a href="{{AppSubUrl}}/user/settings/delete">{{.i18n.Tr "settings.delete"}}</a></li>
         </ul>
     </div>

+ 5 - 3
templates/user/settings/sshkeys.tmpl

@@ -9,7 +9,9 @@
                 <div id="user-ssh-setting-content">
                     <div id="user-ssh-panel" class="panel panel-radius">
                         <div class="panel-header">
-                            <a class="btn btn-small btn-black btn-header btn-radius right" id="ssh-add">{{.i18n.Tr "settings.add_key"}}</a>
+                            <a class="show-form-btn" data-target-form="#user-ssh-add-form">
+                                <button class="btn btn-medium btn-black btn-radius right">{{.i18n.Tr "settings.add_key"}}</button>
+                            </a>
                             <strong>{{.i18n.Tr "settings.manage_ssh_keys"}}</strong>
                         </div>
                         <ul class="panel-body setting-list">
@@ -27,7 +29,7 @@
                                     {{$.CsrfTokenHtml}}
                                     <input name="_method" type="hidden" value="DELETE">
                                     <input name="id" type="hidden" value="{{.Id}}">
-                                    <button class="right ssh-delete-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_key"}}</button>
+                                    <button class="right ssh-btn btn btn-red btn-radius btn-small">{{$.i18n.Tr "settings.delete_key"}}</button>
                                 </form>
                             </li>
                             {{end}}
@@ -35,7 +37,7 @@
                     </div>
                     <p>{{.i18n.Tr "settings.ssh_helper" "https://help.github.com/articles/generating-ssh-keys" "https://help.github.com/ssh-issues/" | Str2html}}</p>
                     <br>
-                    <form class="panel panel-radius form form-align hide" id="user-ssh-add-form" action="{{AppSubUrl}}/user/settings/ssh" method="post">
+                    <form class="panel panel-radius form form-align form-settings-add hide" id="user-ssh-add-form" action="{{AppSubUrl}}/user/settings/ssh" method="post">
                         {{.CsrfTokenHtml}}
                         <p class="panel-header"><strong>{{.i18n.Tr "settings.add_new_key"}}</strong></p>
                         <div class="panel-body">

Some files were not shown because too many files changed in this diff