]> source.dussan.org Git - gitea.git/commitdiff
Add weibo oauth
authorUnknown <joe2010xtmf@163.com>
Mon, 14 Apr 2014 01:00:12 +0000 (21:00 -0400)
committerUnknown <joe2010xtmf@163.com>
Mon, 14 Apr 2014 01:00:12 +0000 (21:00 -0400)
20 files changed:
README.md
README_ZH.md
conf/app.ini
gogs.go
models/action.go
models/oauth2.go
models/repo.go
modules/base/conf.go
modules/base/template.go
modules/middleware/repo.go
modules/social/social.go
routers/admin/admin.go
routers/user/setting.go
templates/admin/config.tmpl
templates/repo/toolbar.tmpl
templates/user/publickey.tmpl
templates/user/setting_nav.tmpl
templates/user/signin.tmpl
templates/user/social.tmpl [new file with mode: 0644]
web.go

index 34f8b66f06745bf93a87a695e44d6ccac601f4bc..20dc8806dd7c736e8dd7a87a3a88019c2a726f65 100644 (file)
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ Gogs(Go Git Service) is a Self Hosted Git Service in the Go Programming Language
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### Current version: 0.2.8 Alpha
+##### Current version: 0.2.9 Alpha
 
 ### NOTICES
 
@@ -40,6 +40,7 @@ More importantly, Gogs only needs one binary to setup your own project hosting o
 - Mail service(register, issue).
 - Administration panel.
 - Supports MySQL, PostgreSQL and SQLite3.
+- Social account login(GitHub, Google, QQ, Weibo)
 
 ## Installation
 
index beb5a1050b2440a2654fa6b58ca763f02318a7b4..97ab07ff23bdc0f7713c3b1960d786c7fd67ed49 100644 (file)
@@ -5,7 +5,7 @@ Gogs(Go Git Service) 是一个由 Go 语言编写的自助 Git 托管服务。
 
 ![Demo](http://gowalker.org/public/gogs_demo.gif)
 
-##### 当前版本:0.2.8 Alpha
+##### 当前版本:0.2.9 Alpha
 
 ## 开发目的
 
@@ -31,6 +31,7 @@ Gogs 完全使用 Go 语言来实现对 Git 数据的操作,实现 **零** 依
 - 邮件服务(注册、Issue)
 - 管理员面板
 - 支持 MySQL、PostgreSQL 以及 SQLite3
+- 社交帐号登录(GitHub、Google、QQ、微博)
 
 ## 安装部署
 
index 4eaf0a33c2509ab1f52ea2a5ee6803376831f1b0..c70919961cdaf43b8850b2de9b81e529962c178b 100644 (file)
@@ -109,6 +109,14 @@ SCOPES = all
 AUTH_URL = https://api.twitter.com/oauth/authorize
 TOKEN_URL = https://api.twitter.com/oauth/access_token
 
+[oauth.weibo]
+ENABLED = false
+CLIENT_ID = 
+CLIENT_SECRET = 
+SCOPES = all
+AUTH_URL = https://api.weibo.com/oauth2/authorize
+TOKEN_URL = https://api.weibo.com/oauth2/access_token
+
 [cache]
 ; Either "memory", "redis", or "memcache", default is "memory"
 ADAPTER = memory
diff --git a/gogs.go b/gogs.go
index 7a7d3ac873a6506849185397fe2a6b66eb458eed..de2bbc777726ea2bb99c084c6d2d01c8ab929914 100644 (file)
--- a/gogs.go
+++ b/gogs.go
@@ -19,7 +19,7 @@ import (
 // Test that go1.2 tag above is included in builds. main.go refers to this definition.
 const go12tag = true
 
-const APP_VER = "0.2.8.0413 Alpha"
+const APP_VER = "0.2.9.0413 Alpha"
 
 func init() {
        base.AppVer = APP_VER
index a642a82c98a96df1d7854de390334ba2318ae037..3edb884e274a5b59ed745c0e5533532c9211c676 100644 (file)
@@ -8,6 +8,8 @@ import (
        "encoding/json"
        "time"
 
+       // "github.com/gogits/git"
+
        "github.com/gogits/gogs/modules/base"
        "github.com/gogits/gogs/modules/log"
 )
@@ -22,6 +24,7 @@ const (
        OP_CREATE_ISSUE
        OP_PULL_REQUEST
        OP_TRANSFER_REPO
+       OP_PUSH_TAG
 )
 
 // Action represents user operation type and other information to repository.,
@@ -67,7 +70,14 @@ func (a Action) GetContent() string {
 // CommitRepoAction adds new action for committing repository.
 func CommitRepoAction(userId int64, userName, actEmail string,
        repoId int64, repoName string, refName string, commit *base.PushCommits) error {
-       log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName)
+       // log.Trace("action.CommitRepoAction(start): %d/%s", userId, repoName)
+
+       opType := OP_COMMIT_REPO
+       // Check it's tag push or branch.
+       // if git.IsTagExist(RepoPath(userName, repoName), refName) {
+       //      opType = OP_PUSH_TAG
+       //      commit = &base.PushCommits{}
+       // }
 
        bs, err := json.Marshal(commit)
        if err != nil {
@@ -76,7 +86,7 @@ func CommitRepoAction(userId int64, userName, actEmail string,
        }
 
        if err = NotifyWatchers(&Action{ActUserId: userId, ActUserName: userName, ActEmail: actEmail,
-               OpType: OP_COMMIT_REPO, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil {
+               OpType: opType, Content: string(bs), RepoId: repoId, RepoName: repoName, RefName: refName}); err != nil {
                log.Error("action.CommitRepoAction(notify watchers): %d/%s", userId, repoName)
                return err
        }
index 38d21fda1ceb1f3c324741ffac363731cf0ef2a3..d1ae4611b85398029cea151ef7d5c4ce53ae4b31 100644 (file)
@@ -68,3 +68,9 @@ func GetOauth2ById(id int64) (oa *Oauth2, err error) {
        }
        return oa, nil
 }
+
+// GetOauthByUserId returns list of oauthes that are releated to given user.
+func GetOauthByUserId(uid int64) (oas []*Oauth2, err error) {
+       err = orm.Find(&oas, Oauth2{Uid: uid})
+       return oas, err
+}
index 1a5a95f047308266ffef57801881727454380e53..bb0c164e245b7fbaa654295a7924669291f2e313 100644 (file)
@@ -75,9 +75,9 @@ type Repository struct {
        NumStars        int
        NumForks        int
        NumIssues       int
-       NumReleases     int `xorm:"NOT NULL"`
        NumClosedIssues int
        NumOpenIssues   int `xorm:"-"`
+       NumTags         int `xorm:"-"`
        IsPrivate       bool
        IsMirror        bool
        IsBare          bool
index 0eca5f4fcbdc3adbc21e034417966d39cd3475df..957ec57b4d6c78768a64318ff6e81b5b65579b50 100644 (file)
@@ -38,7 +38,7 @@ type OauthInfo struct {
 // Oauther represents oauth service.
 type Oauther struct {
        GitHub, Google, Tencent bool
-       Twitter                 bool
+       Twitter, Weibo          bool
        OauthInfos              map[string]*OauthInfo
 }
 
index 6241497969737b45ba007d0a30f03535e0022ff6..79aeeb9d77d250385b14cac5aa27c27d3cf02d32 100644 (file)
@@ -92,6 +92,7 @@ var TemplateFuncs template.FuncMap = map[string]interface{}{
        "DiffTypeToStr":     DiffTypeToStr,
        "DiffLineTypeToStr": DiffLineTypeToStr,
        "ShortSha":          ShortSha,
+       "Oauth2Icon":        Oauth2Icon,
 }
 
 type Actioner interface {
@@ -109,7 +110,7 @@ func ActionIcon(opType int) string {
        switch opType {
        case 1: // Create repository.
                return "plus-circle"
-       case 5: // Commit repository.
+       case 5, 9: // Commit repository.
                return "arrow-circle-o-right"
        case 6: // Create issue.
                return "exclamation-circle"
@@ -127,6 +128,7 @@ const (
        TPL_CREATE_ISSUE   = `<a href="/user/%s">%s</a> opened issue <a href="/%s/issues/%s">%s#%s</a>
 <div><img src="%s?s=16" alt="user-avatar"/> %s</div>`
        TPL_TRANSFER_REPO = `<a href="/user/%s">%s</a> transfered repository <code>%s</code> to <a href="/%s">%s</a>`
+       TPL_PUSH_TAG      = `<a href="/user/%s">%s</a> pushed tag <a href="/%s/src/%s">%s</a> at <a href="/%s">%s</a>`
 )
 
 type PushCommit struct {
@@ -174,6 +176,8 @@ func ActionDesc(act Actioner) string {
        case 8: // Transfer repository.
                newRepoLink := content + "/" + repoName
                return fmt.Sprintf(TPL_TRANSFER_REPO, actUserName, actUserName, repoLink, newRepoLink, newRepoLink)
+       case 9: // Push tag.
+               return fmt.Sprintf(TPL_PUSH_TAG, actUserName, actUserName, repoLink, branch, branch, repoLink, repoLink)
        default:
                return "invalid type"
        }
@@ -197,3 +201,19 @@ func DiffLineTypeToStr(diffType int) string {
        }
        return "same"
 }
+
+func Oauth2Icon(t int) string {
+       switch t {
+       case 1:
+               return "fa-github-square"
+       case 2:
+               return "fa-google-plus-square"
+       case 3:
+               return "fa-twitter-square"
+       case 4:
+               return "fa-linux"
+       case 5:
+               return "fa-weibo"
+       }
+       return ""
+}
index 1e79ce9870e1ae1f66bf36dfe53eca8a429bba0c..82c1c2dbf60089252b26935be22a3cecd48b6da0 100644 (file)
@@ -123,6 +123,13 @@ func RepoAssignment(redirect bool, args ...bool) martini.Handler {
                ctx.Repo.GitRepo = gitRepo
                ctx.Repo.RepoLink = "/" + user.Name + "/" + repo.Name
 
+               tags, err := ctx.Repo.GitRepo.GetTags()
+               if err != nil {
+                       ctx.Handle(500, "RepoAssignment(GetTags))", err)
+                       return
+               }
+               ctx.Repo.Repository.NumTags = len(tags)
+
                ctx.Data["Title"] = user.Name + "/" + repo.Name
                ctx.Data["Repository"] = repo
                ctx.Data["Owner"] = user
index 230f478fe4ab6a1c2e6440c6c246cda6a18453ef..c2ee541776472ee2b606ad7e4515e3caae390b1a 100644 (file)
@@ -48,7 +48,7 @@ func NewOauthService() {
        base.OauthService.OauthInfos = make(map[string]*base.OauthInfo)
 
        socialConfigs := make(map[string]*oauth.Config)
-       allOauthes := []string{"github", "google", "qq", "twitter"}
+       allOauthes := []string{"github", "google", "qq", "twitter", "weibo"}
        // Load all OAuth config data.
        for _, name := range allOauthes {
                base.OauthService.OauthInfos[name] = &base.OauthInfo{
@@ -98,6 +98,13 @@ func NewOauthService() {
                enabledOauths = append(enabledOauths, "Twitter")
        }
 
+       // Weibo.
+       if base.Cfg.MustBool("oauth.weibo", "ENABLED") {
+               base.OauthService.Weibo = true
+               newWeiboOauth(socialConfigs["weibo"])
+               enabledOauths = append(enabledOauths, "Weibo")
+       }
+
        log.Info("Oauth Service Enabled %s", enabledOauths)
 }
 
@@ -331,3 +338,56 @@ func (s *SocialTwitter) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo
        // }, nil
        return nil, nil
 }
+
+//  __      __       ._____.
+// /  \    /  \ ____ |__\_ |__   ____
+// \   \/\/   // __ \|  || __ \ /  _ \
+//  \        /\  ___/|  || \_\ (  <_> )
+//   \__/\  /  \___  >__||___  /\____/
+//        \/       \/        \/
+
+type SocialWeibo struct {
+       Token *oauth.Token
+       *oauth.Transport
+}
+
+func (s *SocialWeibo) Type() int {
+       return models.OT_WEIBO
+}
+
+func newWeiboOauth(config *oauth.Config) {
+       SocialMap["weibo"] = &SocialWeibo{
+               Transport: &oauth.Transport{
+                       Config:    config,
+                       Transport: http.DefaultTransport,
+               },
+       }
+}
+
+func (s *SocialWeibo) SetRedirectUrl(url string) {
+       s.Transport.Config.RedirectURL = url
+}
+
+func (s *SocialWeibo) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) {
+       transport := &oauth.Transport{Token: token}
+       var data struct {
+               Id   string `json:"id"`
+               Name string `json:"name"`
+       }
+       var err error
+
+       reqUrl := "https://api.weibo.com/2/users/show.json"
+       r, err := transport.Client().Get(reqUrl)
+       if err != nil {
+               return nil, err
+       }
+       defer r.Body.Close()
+       if err = json.NewDecoder(r.Body).Decode(&data); err != nil {
+               return nil, err
+       }
+       return &BasicUserInfo{
+               Identity: data.Id,
+               Name:     data.Name,
+       }, nil
+       return nil, nil
+}
index 18a43ff8179aa26b13f9550c1cbadec979aa73b6..d0f737e645e466b70bad0491cb8f7a4c9b8b520f 100644 (file)
@@ -153,6 +153,12 @@ func Config(ctx *middleware.Context) {
                ctx.Data["Mailer"] = base.MailService
        }
 
+       ctx.Data["OauthEnabled"] = false
+       if base.OauthService != nil {
+               ctx.Data["OauthEnabled"] = true
+               ctx.Data["Oauther"] = base.OauthService
+       }
+
        ctx.Data["CacheAdapter"] = base.CacheAdapter
        ctx.Data["CacheConfig"] = base.CacheConfig
 
index 7e66ad35999c126bc00be79d610eba42e0bb8768..a8fdc116c6decf04a0bb40927b6e8bcd65cc953a 100644 (file)
@@ -69,6 +69,20 @@ func SettingPost(ctx *middleware.Context, form auth.UpdateProfileForm) {
        ctx.Redirect("/user/setting")
 }
 
+func SettingSocial(ctx *middleware.Context) {
+       ctx.Data["Title"] = "Social Account"
+       ctx.Data["PageIsUserSetting"] = true
+       ctx.Data["IsUserPageSettingSocial"] = true
+       socials, err := models.GetOauthByUserId(ctx.User.Id)
+       if err != nil {
+               ctx.Handle(500, "user.SettingSocial", err)
+               return
+       }
+
+       ctx.Data["Socials"] = socials
+       ctx.HTML(200, "user/social")
+}
+
 func SettingPassword(ctx *middleware.Context) {
        ctx.Data["Title"] = "Password"
        ctx.Data["PageIsUserSetting"] = true
@@ -147,7 +161,7 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
 
        // Add new SSH key.
        if ctx.Req.Method == "POST" {
-               if hasErr, ok := ctx.Data["HasError"]; ok && hasErr.(bool) {
+               if ctx.HasError() {
                        ctx.HTML(200, "user/publickey")
                        return
                }
@@ -162,11 +176,13 @@ func SettingSSHKeys(ctx *middleware.Context, form auth.AddSSHKeyForm) {
                                ctx.RenderWithErr("Public key name has been used", "user/publickey", &form)
                                return
                        }
-                       ctx.Handle(200, "ssh.AddPublicKey", err)
-                       log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+                       ctx.Handle(500, "ssh.AddPublicKey", err)
                        return
                } else {
-                       ctx.Data["AddSSHKeySuccess"] = true
+                       log.Trace("%s User SSH key added: %s", ctx.Req.RequestURI, ctx.User.LowerName)
+                       ctx.Flash.Success("New SSH Key has been added!")
+                       ctx.Redirect("/user/setting/ssh")
+                       return
                }
        }
 
index 31cfb77bad67e00d4a69fedb7fff5941940fde8a..757a800c205698d58172582dde49e9e534eb7b82 100644 (file)
                 <dl class="dl-horizontal admin-dl-horizontal">
                     <dt>Enabled</dt>
                     <dd><i class="fa fa{{if .MailerEnabled}}-check{{end}}-square-o"></i></dd>
-                    <dt>Name</dt>
+                    {{if .MailerEnabled}}<dt>Name</dt>
                     <dd>{{.Mailer.Name}}</dd>
                     <dt>Host</dt>
                     <dd>{{.Mailer.Host}}</dd>
                     <dt>User</dt>
-                    <dd>{{.Mailer.User}}</dd>
+                    <dd>{{.Mailer.User}}</dd>{{end}}
+                </dl>
+            </div>
+        </div>
+
+        <div class="panel panel-default">
+            <div class="panel-heading">
+                OAuth Configuration
+            </div>
+
+            <div class="panel-body">
+                <dl class="dl-horizontal admin-dl-horizontal">
+                    <dt>Enabled</dt>
+                    <dd><i class="fa fa{{if .OauthEnabled}}-check{{end}}-square-o"></i></dd>
+                    {{if .OauthEnabled}}<dt>GitHub</dt>
+                    <dd><i class="fa fa{{if .Oauther.GitHub}}-check{{end}}-square-o"></i></dd>
+                    <dt>Google</dt>
+                    <dd><i class="fa fa{{if .Oauther.Google}}-check{{end}}-square-o"></i></dd>
+                    <dt>Tencent QQ</dt>
+                    <dd><i class="fa fa{{if .Oauther.Tencent}}-check{{end}}-square-o"></i></dd>
+                    <dt>Weibo</dt>
+                    <dd><i class="fa fa{{if .Oauther.Weibo}}-check{{end}}-square-o"></i></dd>
+                    <dd>{{.Mailer.User}}</dd>{{end}}
                 </dl>
             </div>
         </div>
index 9c137e5179d0550b0680a15135295f92109f4f58..bde5bc29e1211cdcda15f883f07893213398dc28 100644 (file)
@@ -13,7 +13,7 @@
                     <li class="tmp">{{if .IsRepoToolbarIssuesList}}<a href="{{.RepoLink}}/issues/new"><button class="btn btn-primary btn-sm">New Issue</button>
                     </a>{{end}}</li>
                     {{end}}
-                    <li class="{{if .IsRepoToolbarReleases}}active{{end}}"><a href="{{.RepoLink}}/releases">{{if .Repository.NumReleases}}<span class="badge">{{.Repository.NumReleases}}</span> {{end}}Releases</a></li>
+                    <li class="{{if .IsRepoToolbarReleases}}active{{end}}"><a href="{{.RepoLink}}/releases">{{if .Repository.NumTags}}<span class="badge">{{.Repository.NumTags}}</span> {{end}}Releases</a></li>
                     {{if .IsRepoToolbarReleases}}
                     <li class="tmp">{{if not .IsRepoReleaseNew}}<a href="{{.RepoLink}}/releases/new"><button class="btn btn-primary btn-sm">New Release</button></a>{{end}}</li>
                     {{end}}
index ecdeb035d2f89e24368af84e8fb819e484c793fc..29cfd8f0ebb03302b0ea10f44e49ddaeb093e162 100644 (file)
@@ -4,8 +4,8 @@
     {{template "user/setting_nav" .}}
     <div id="user-setting-container" class="col-md-9">
         <div id="ssh-keys">
-            <h4>SSH Keys</h4>{{if .AddSSHKeySuccess}}
-            <p class="alert alert-success">New SSH Key has been added !</p>{{else if .HasError}}<p class="alert alert-danger">{{.ErrorMsg}}</p>{{end}}
+            <h4>SSH Keys</h4>
+            {{template "base/alert" .}}
             <ul id="ssh-keys-list" class="list-group">
                 <li class="list-group-item"><span class="name">SSH Key's name</span></li>
                 {{range .Keys}}
index c0f2ae03dda6c7a80aaf01884c121ab6f5334ae1..9c7ae5208ff8177c357c9e61a8aa2b54804e4791 100644 (file)
@@ -2,6 +2,7 @@
     <h4>Account Setting</h4>
     <ul class="list-group">
         <li class="list-group-item{{if .IsUserPageSetting}} list-group-item-success{{end}}"><a href="/user/setting">Account Profile</a></li>
+        <li class="list-group-item{{if .IsUserPageSettingSocial}} list-group-item-success{{end}}"><a href="/user/setting/social">Social Account</a></li>
         <li class="list-group-item{{if .IsUserPageSettingPasswd}} list-group-item-success{{end}}"><a href="/user/setting/password">Password</a></li>
         <!-- <li class="list-group-item{{if .IsUserPageSettingNotify}} list-group-item-success{{end}}"><a href="/user/setting/notification">Notifications</a></li> -->
         <li class="list-group-item{{if .IsUserPageSettingSSH}} list-group-item-success{{end}}"><a href="/user/setting/ssh/">SSH Keys</a></li>
index d402c2923807c1815c283b9bddef975a96f76809..955c82f430b529cec9171b8343a9f9b63eb6295e 100644 (file)
@@ -61,8 +61,9 @@
             </a>-->
             {{if .OauthService.GitHub}}<a href="/user/login/github?next=/user/sign_up" class="btn btn-default"><i class="fa fa-github-square fa-2x"></i><span>GitHub</span></a>{{end}}
             {{if .OauthService.Google}}<a href="/user/login/google?next=/user/sign_up" class="btn btn-default"><i class="fa fa-google-plus-square fa-2x"></i><span>Google</span></a>{{end}}
-            {{if .OauthService.Tencent}}<a href="/user/login/twitter?next=/user/sign_up" class="btn btn-default"><i class="fa fa-twitter-square fa-2x"></i><span>Twitter</span></a>{{end}}
-            {{if .OauthService.Tencent}}<a href="/user/login/qq?next=/user/sign_up" class="btn btn-default"><i class="fa fa-linux fa-2x"></i><span>QQ</span></a>{{end}}
+            {{if .OauthService.Twitter}}<a href="/user/login/twitter?next=/user/sign_up" class="btn btn-default"><i class="fa fa-twitter-square fa-2x"></i><span>Twitter</span></a>{{end}}
+            {{if .OauthService.Tencent}}<a href="/user/login/qq?next=/user/sign_up" class="btn btn-default"><i class="fa fa-linux fa-2x"></i><span>Tencent QQ</span></a>{{end}}
+            {{if .OauthService.Weibo}}<a href="/user/login/weibo?next=/user/sign_up" class="btn btn-default"><i class="fa fa-weibo fa-2x"></i><span>Weibo</span></a>{{end}}
         </div>
         {{end}}{{end}}
     </form>
diff --git a/templates/user/social.tmpl b/templates/user/social.tmpl
new file mode 100644 (file)
index 0000000..f0b1132
--- /dev/null
@@ -0,0 +1,17 @@
+{{template "base/head" .}}
+{{template "base/navbar" .}}
+<div id="body" class="container" data-page="user">
+    {{template "user/setting_nav" .}}
+    <div id="user-setting-container" class="col-md-9">
+        <div id="ssh-keys">
+            <h4>Social Account</h4>
+            {{template "base/alert" .}}
+            <ul id="ssh-keys-list" class="list-group">
+                {{range .Socials}}
+                    <i class="fa {{Oauth2Icon .Type}} fa-3x"></i>
+                {{end}}
+            </ul>
+        </div>
+    </div>
+</div>
+{{template "base/footer" .}}
\ No newline at end of file
diff --git a/web.go b/web.go
index 8ae074ec01fca865d07d77ec3f9c692c82e254cf..268d9e71da8b0f7e3468ea03cc7d4b7e56f0bfc0 100644 (file)
--- a/web.go
+++ b/web.go
@@ -63,7 +63,7 @@ func runWeb(*cli.Context) {
                SignInRequire: base.Service.RequireSignInView,
                DisableCsrf:   true,
        })
-
+       
        reqSignOut := middleware.Toggle(&middleware.ToggleOptions{SignOutRequire: true})
 
        bindIgnErr := middleware.BindIgnErr
@@ -108,6 +108,7 @@ func runWeb(*cli.Context) {
                r.Post("/forget_password", user.ForgotPasswdPost)
        })
        m.Group("/user/setting", func(r martini.Router) {
+               r.Get("/social", user.SettingSocial)
                r.Get("/password", user.SettingPassword)
                r.Post("/password", bindIgnErr(auth.UpdatePasswdForm{}), user.SettingPasswordPost)
                r.Any("/ssh", bindIgnErr(auth.AddSSHKeyForm{}), user.SettingSSHKeys)