diff --git a/.gopmfile b/.gopmfile index 2f9ebb9620..a3af87e919 100644 --- a/.gopmfile +++ b/.gopmfile @@ -21,6 +21,7 @@ github.com/macaron-contrib/cache = commit:0bb9e6c9ef github.com/macaron-contrib/captcha = commit:3567dc48b8 github.com/macaron-contrib/csrf = commit:422b79675c github.com/macaron-contrib/i18n = +github.com/macaron-contrib/oauth2 = github.com/macaron-contrib/session = commit:f00d48fd4f github.com/macaron-contrib/toolbox = commit:57127bcc89 github.com/mattn/go-sqlite3 = commit:a80c27ba33 diff --git a/cmd/web.go b/cmd/web.go index f2a506bc19..6875ddb1d1 100644 --- a/cmd/web.go +++ b/cmd/web.go @@ -21,6 +21,7 @@ import ( "github.com/macaron-contrib/captcha" "github.com/macaron-contrib/csrf" "github.com/macaron-contrib/i18n" + "github.com/macaron-contrib/oauth2" "github.com/macaron-contrib/session" "github.com/macaron-contrib/toolbox" @@ -140,6 +141,11 @@ func newMacaron() *macaron.Macaron { }, }, })) + + // OAuth 2. + for _, info := range setting.OauthService.OauthInfos { + m.Use(oauth2.NewOAuth2Provider(info.Options, info.AuthUrl, info.TokenUrl)) + } m.Use(middleware.Contexter()) return m } @@ -213,7 +219,7 @@ func runWeb(*cli.Context) { m.Group("/user", func() { m.Get("/login", user.SignIn) m.Post("/login", bindIgnErr(auth.SignInForm{}), user.SignInPost) - m.Get("/login/:name", user.SocialSignIn) + m.Get("/info/:name", user.SocialSignIn) m.Get("/sign_up", user.SignUp) m.Post("/sign_up", bindIgnErr(auth.RegisterForm{}), user.SignUpPost) m.Get("/reset_password", user.ResetPasswd) @@ -406,7 +412,7 @@ func runWeb(*cli.Context) { m.Get("/issues2/", repo.Issues2) m.Get("/pulls2/", repo.PullRequest2) m.Get("/labels2/", repo.Labels2) - m.Get("/milestone2/",repo.Milestones2) + m.Get("/milestone2/", repo.Milestones2) m.Group("", func() { m.Get("/src/*", repo.Home) diff --git a/conf/app.ini b/conf/app.ini index 2bd32eca60..dba75ed1b5 100644 --- a/conf/app.ini +++ b/conf/app.ini @@ -125,13 +125,10 @@ TOKEN_URL = https://accounts.google.com/o/oauth2/token ENABLED = false CLIENT_ID = CLIENT_SECRET = -SCOPES = all +SCOPES = get_user_info ; QQ 互联 -; AUTH_URL = https://graph.qq.com/oauth2.0/authorize -; TOKEN_URL = https://graph.qq.com/oauth2.0/token -; Tencent weibo -AUTH_URL = https://open.t.qq.com/cgi-bin/oauth2/authorize -TOKEN_URL = https://open.t.qq.com/cgi-bin/oauth2/access_token +AUTH_URL = https://graph.qq.com/oauth2.0/authorize +TOKEN_URL = https://graph.qq.com/oauth2.0/token [oauth.weibo] ENABLED = false diff --git a/conf/locale/TRANSLATORS b/conf/locale/TRANSLATORS index a07e7d5e85..9899ea4eac 100644 --- a/conf/locale/TRANSLATORS +++ b/conf/locale/TRANSLATORS @@ -1,3 +1,5 @@ -# This file lists all individuals having contributed content to the translation. +# This file lists all PUBLIC individuals having contributed content to the translation. +# Order of name is meaningless. -Thomas Fanninger \ No newline at end of file +Thomas Fanninger +Łukasz Jan Niemier \ No newline at end of file diff --git a/gogs.go b/gogs.go index 00ac2456e9..c555b56ccc 100644 --- a/gogs.go +++ b/gogs.go @@ -17,7 +17,7 @@ import ( "github.com/gogits/gogs/modules/setting" ) -const APP_VER = "0.5.8.1125 Beta" +const APP_VER = "0.5.8.1128 Beta" func init() { runtime.GOMAXPROCS(runtime.NumCPU()) diff --git a/modules/setting/setting.go b/modules/setting/setting.go index a775847c1f..9737fa7c1d 100644 --- a/modules/setting/setting.go +++ b/modules/setting/setting.go @@ -17,6 +17,7 @@ import ( "github.com/Unknwon/com" "github.com/Unknwon/goconfig" + "github.com/macaron-contrib/oauth2" "github.com/macaron-contrib/session" "github.com/gogits/gogs/modules/log" @@ -434,9 +435,8 @@ type Mailer struct { } type OauthInfo struct { - ClientId, ClientSecret string - Scopes string - AuthUrl, TokenUrl string + oauth2.Options + AuthUrl, TokenUrl string } // Oauther represents oauth service. diff --git a/modules/social/social.go b/modules/social/social.go index f4d8598820..8406cd52cb 100644 --- a/modules/social/social.go +++ b/modules/social/social.go @@ -7,12 +7,12 @@ package social import ( "encoding/json" + "io/ioutil" "net/http" "net/url" "strconv" - "strings" - oauth "github.com/gogits/oauth2" + "github.com/macaron-contrib/oauth2" "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/log" @@ -27,16 +27,11 @@ type BasicUserInfo struct { type SocialConnector interface { Type() int - SetRedirectUrl(string) - UserInfo(*oauth.Token, *url.URL) (*BasicUserInfo, error) - - AuthCodeURL(string) string - Exchange(string) (*oauth.Token, error) + UserInfo(*oauth2.Token, *url.URL) (*BasicUserInfo, error) } var ( - SocialBaseUrl = "/user/login" - SocialMap = make(map[string]SocialConnector) + SocialMap = make(map[string]SocialConnector) ) func NewOauthService() { @@ -47,24 +42,29 @@ func NewOauthService() { setting.OauthService = &setting.Oauther{} setting.OauthService.OauthInfos = make(map[string]*setting.OauthInfo) - socialConfigs := make(map[string]*oauth.Config) + socialConfigs := make(map[string]*oauth2.Options) allOauthes := []string{"github", "google", "qq", "twitter", "weibo"} // Load all OAuth config data. for _, name := range allOauthes { - setting.OauthService.OauthInfos[name] = &setting.OauthInfo{ - ClientId: setting.Cfg.MustValue("oauth."+name, "CLIENT_ID"), - ClientSecret: setting.Cfg.MustValue("oauth."+name, "CLIENT_SECRET"), - Scopes: setting.Cfg.MustValue("oauth."+name, "SCOPES"), - AuthUrl: setting.Cfg.MustValue("oauth."+name, "AUTH_URL"), - TokenUrl: setting.Cfg.MustValue("oauth."+name, "TOKEN_URL"), + if !setting.Cfg.MustBool("oauth."+name, "ENABLED") { + continue } - socialConfigs[name] = &oauth.Config{ - ClientId: setting.OauthService.OauthInfos[name].ClientId, + setting.OauthService.OauthInfos[name] = &setting.OauthInfo{ + Options: oauth2.Options{ + ClientID: setting.Cfg.MustValue("oauth."+name, "CLIENT_ID"), + ClientSecret: setting.Cfg.MustValue("oauth."+name, "CLIENT_SECRET"), + Scopes: setting.Cfg.MustValueArray("oauth."+name, "SCOPES", " "), + PathLogin: "/user/login/oauth2/" + name, + PathCallback: setting.AppSubUrl + "/user/login/" + name, + RedirectURL: setting.AppUrl + "user/login/" + name, + }, + AuthUrl: setting.Cfg.MustValue("oauth."+name, "AUTH_URL"), + TokenUrl: setting.Cfg.MustValue("oauth."+name, "TOKEN_URL"), + } + socialConfigs[name] = &oauth2.Options{ + ClientID: setting.OauthService.OauthInfos[name].ClientID, ClientSecret: setting.OauthService.OauthInfos[name].ClientSecret, - RedirectURL: strings.TrimSuffix(setting.AppUrl, "/") + SocialBaseUrl + name, - Scope: setting.OauthService.OauthInfos[name].Scopes, - AuthURL: setting.OauthService.OauthInfos[name].AuthUrl, - TokenURL: setting.OauthService.OauthInfos[name].TokenUrl, + Scopes: setting.OauthService.OauthInfos[name].Scopes, } } enabledOauths := make([]string, 0, 10) @@ -91,11 +91,11 @@ func NewOauthService() { } // Twitter. - if setting.Cfg.MustBool("oauth.twitter", "ENABLED") { - setting.OauthService.Twitter = true - newTwitterOauth(socialConfigs["twitter"]) - enabledOauths = append(enabledOauths, "Twitter") - } + // if setting.Cfg.MustBool("oauth.twitter", "ENABLED") { + // setting.OauthService.Twitter = true + // newTwitterOauth(socialConfigs["twitter"]) + // enabledOauths = append(enabledOauths, "Twitter") + // } // Weibo. if setting.Cfg.MustBool("oauth.weibo", "ENABLED") { @@ -115,38 +115,25 @@ func NewOauthService() { // \/ \/ \/ type SocialGithub struct { - Token *oauth.Token - *oauth.Transport + opts *oauth2.Options +} + +func newGitHubOauth(opts *oauth2.Options) { + SocialMap["github"] = &SocialGithub{opts} } func (s *SocialGithub) Type() int { return int(models.GITHUB) } -func newGitHubOauth(config *oauth.Config) { - SocialMap["github"] = &SocialGithub{ - Transport: &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - }, - } -} - -func (s *SocialGithub) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { - transport := &oauth.Transport{ - Token: token, - } +func (s *SocialGithub) UserInfo(token *oauth2.Token, _ *url.URL) (*BasicUserInfo, error) { + transport := s.opts.NewTransportFromToken(token) var data struct { Id int `json:"id"` Name string `json:"login"` Email string `json:"email"` } - var err error - r, err := transport.Client().Get(s.Transport.Scope) + r, err := transport.Client().Get("https://api.github.com/user") if err != nil { return nil, err } @@ -169,38 +156,25 @@ func (s *SocialGithub) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, // \/ /_____/ \/ type SocialGoogle struct { - Token *oauth.Token - *oauth.Transport + opts *oauth2.Options } func (s *SocialGoogle) Type() int { return int(models.GOOGLE) } -func newGoogleOauth(config *oauth.Config) { - SocialMap["google"] = &SocialGoogle{ - Transport: &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - }, - } +func newGoogleOauth(opts *oauth2.Options) { + SocialMap["google"] = &SocialGoogle{opts} } -func (s *SocialGoogle) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { - transport := &oauth.Transport{Token: token} +func (s *SocialGoogle) UserInfo(token *oauth2.Token, _ *url.URL) (*BasicUserInfo, error) { + transport := s.opts.NewTransportFromToken(token) var data struct { Id string `json:"id"` Name string `json:"name"` Email string `json:"email"` } - var err error - - reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" - r, err := transport.Client().Get(reqUrl) + r, err := transport.Client().Get("https://www.googleapis.com/userinfo/v2/me") if err != nil { return nil, err } @@ -223,62 +197,35 @@ func (s *SocialGoogle) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, // \__> \__> type SocialTencent struct { - Token *oauth.Token - *oauth.Transport - reqUrl string + opts *oauth2.Options +} + +func newTencentOauth(opts *oauth2.Options) { + SocialMap["qq"] = &SocialTencent{opts} } func (s *SocialTencent) Type() int { return int(models.QQ) } -func newTencentOauth(config *oauth.Config) { - SocialMap["qq"] = &SocialTencent{ - reqUrl: "https://open.t.qq.com/api/user/info", - Transport: &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - }, - } -} - -func (s *SocialTencent) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} - -func (s *SocialTencent) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserInfo, error) { - var data struct { - Data struct { - Id string `json:"openid"` - Name string `json:"name"` - Email string `json:"email"` - } `json:"data"` - } - var err error - // https://open.t.qq.com/api/user/info? - //oauth_consumer_key=APP_KEY& - //access_token=ACCESSTOKEN&openid=openid - //clientip=CLIENTIP&oauth_version=2.a - //scope=all - var urls = url.Values{ - "oauth_consumer_key": {s.Transport.Config.ClientId}, - "access_token": {token.AccessToken}, - "openid": URL.Query()["openid"], - "oauth_version": {"2.a"}, - "scope": {"all"}, - } - r, err := http.Get(s.reqUrl + "?" + urls.Encode()) +func (s *SocialTencent) UserInfo(token *oauth2.Token, URL *url.URL) (*BasicUserInfo, error) { + r, err := http.Get("https://graph.z.qq.com/moc2/me?access_token=" + url.QueryEscape(token.AccessToken)) if err != nil { return nil, err } defer r.Body.Close() - if err = json.NewDecoder(r.Body).Decode(&data); err != nil { + + body, err := ioutil.ReadAll(r.Body) + if err != nil { return nil, err } + vals, err := url.ParseQuery(string(body)) + if err != nil { + return nil, err + } + return &BasicUserInfo{ - Identity: data.Data.Id, - Name: data.Data.Name, - Email: data.Data.Email, + Identity: vals.Get("openid"), }, nil } @@ -289,54 +236,54 @@ func (s *SocialTencent) UserInfo(token *oauth.Token, URL *url.URL) (*BasicUserIn // |____| \/\_/ |__||__| |__| \___ >__| // \/ -type SocialTwitter struct { - Token *oauth.Token - *oauth.Transport -} +// type SocialTwitter struct { +// Token *oauth2.Token +// *oauth2.Transport +// } -func (s *SocialTwitter) Type() int { - return int(models.TWITTER) -} +// func (s *SocialTwitter) Type() int { +// return int(models.TWITTER) +// } -func newTwitterOauth(config *oauth.Config) { - SocialMap["twitter"] = &SocialTwitter{ - Transport: &oauth.Transport{ - Config: config, - Transport: http.DefaultTransport, - }, - } -} +// func newTwitterOauth(config *oauth2.Config) { +// SocialMap["twitter"] = &SocialTwitter{ +// Transport: &oauth.Transport{ +// Config: config, +// Transport: http.DefaultTransport, +// }, +// } +// } -func (s *SocialTwitter) SetRedirectUrl(url string) { - s.Transport.Config.RedirectURL = url -} +// func (s *SocialTwitter) SetRedirectUrl(url string) { +// s.Transport.Config.RedirectURL = url +// } -//https://github.com/mrjones/oauth -func (s *SocialTwitter) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, error) { - // transport := &oauth.Transport{Token: token} - // var data struct { - // Id string `json:"id"` - // Name string `json:"name"` - // Email string `json:"email"` - // } - // var err error +// //https://github.com/mrjones/oauth +// func (s *SocialTwitter) UserInfo(token *oauth2.Token, _ *url.URL) (*BasicUserInfo, error) { +// // transport := &oauth.Transport{Token: token} +// // var data struct { +// // Id string `json:"id"` +// // Name string `json:"name"` +// // Email string `json:"email"` +// // } +// // var err error - // reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" - // 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, - // Email: data.Email, - // }, nil - return nil, nil -} +// // reqUrl := "https://www.googleapis.com/oauth2/v1/userinfo" +// // 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, +// // Email: data.Email, +// // }, nil +// return nil, nil +// } // __ __ ._____. // / \ / \ ____ |__\_ |__ ____ @@ -346,37 +293,25 @@ func (s *SocialTwitter) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo // \/ \/ \/ type SocialWeibo struct { - Token *oauth.Token - *oauth.Transport + opts *oauth2.Options +} + +func newWeiboOauth(opts *oauth2.Options) { + SocialMap["weibo"] = &SocialWeibo{opts} } func (s *SocialWeibo) Type() int { return int(models.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} +func (s *SocialWeibo) UserInfo(token *oauth2.Token, _ *url.URL) (*BasicUserInfo, error) { + transport := s.opts.NewTransportFromToken(token) var data struct { Name string `json:"name"` } - var err error - var urls = url.Values{ "access_token": {token.AccessToken}, - "uid": {token.Extra["id_token"]}, + "uid": {token.Extra("uid")}, } reqUrl := "https://api.weibo.com/2/users/show.json" r, err := transport.Client().Get(reqUrl + "?" + urls.Encode()) @@ -384,11 +319,12 @@ func (s *SocialWeibo) UserInfo(token *oauth.Token, _ *url.URL) (*BasicUserInfo, return nil, err } defer r.Body.Close() + if err = json.NewDecoder(r.Body).Decode(&data); err != nil { return nil, err } return &BasicUserInfo{ - Identity: token.Extra["id_token"], + Identity: token.Extra("uid"), Name: data.Name, }, nil } diff --git a/routers/user/social.go b/routers/user/social.go index 0bc1fa592f..5d2f2027c4 100644 --- a/routers/user/social.go +++ b/routers/user/social.go @@ -9,10 +9,12 @@ import ( "errors" "fmt" "net/url" - "strings" - "time" + // "strings" + // "time" - "github.com/gogits/gogs/models" + "github.com/macaron-contrib/oauth2" + + // "github.com/gogits/gogs/models" "github.com/gogits/gogs/modules/log" "github.com/gogits/gogs/modules/middleware" "github.com/gogits/gogs/modules/setting" @@ -29,79 +31,69 @@ func extractPath(next string) string { func SocialSignIn(ctx *middleware.Context) { if setting.OauthService == nil { - ctx.Handle(404, "social.SocialSignIn(oauth service not enabled)", nil) + ctx.Handle(404, "OAuth2 service not enabled", nil) + return + } + + info := ctx.Session.Get(oauth2.KEY_TOKEN) + if info == nil { + ctx.Redirect(setting.AppSubUrl + "/user/login") return } - next := extractPath(ctx.Query("next")) name := ctx.Params(":name") connect, ok := social.SocialMap[name] if !ok { - ctx.Handle(404, "social.SocialSignIn(social login not enabled)", errors.New(name)) - return - } - appUrl := strings.TrimSuffix(setting.AppUrl, "/") - if name == "weibo" { - appUrl = strings.Replace(appUrl, "localhost", "127.0.0.1", 1) - } - - code := ctx.Query("code") - if code == "" { - // redirect to social login page - connect.SetRedirectUrl(appUrl + ctx.Req.URL.Path) - ctx.Redirect(connect.AuthCodeURL(next)) + ctx.Handle(404, "social login not enabled", errors.New(name)) return } - // handle call back - tk, err := connect.Exchange(code) - if err != nil { - ctx.Handle(500, "social.SocialSignIn(Exchange)", err) + tk := new(oauth2.Token) + if err := json.Unmarshal(info.([]byte), tk); err != nil { + ctx.Handle(500, "Unmarshal token", err) return } - next = extractPath(ctx.Query("state")) - log.Trace("social.SocialSignIn(Got token)") ui, err := connect.UserInfo(tk, ctx.Req.URL) if err != nil { - ctx.Handle(500, fmt.Sprintf("social.SocialSignIn(get info from %s)", name), err) + ctx.Handle(500, fmt.Sprintf("UserInfo(%s)", name), err) return } log.Info("social.SocialSignIn(social login): %s", ui) - oa, err := models.GetOauth2(ui.Identity) - switch err { - case nil: - ctx.Session.Set("uid", oa.User.Id) - ctx.Session.Set("uname", oa.User.Name) - case models.ErrOauth2RecordNotExist: - raw, _ := json.Marshal(tk) - oa = &models.Oauth2{ - Uid: -1, - Type: connect.Type(), - Identity: ui.Identity, - Token: string(raw), - } - log.Trace("social.SocialSignIn(oa): %v", oa) - if err = models.AddOauth2(oa); err != nil { - log.Error(4, "social.SocialSignIn(add oauth2): %v", err) // 501 - return - } - case models.ErrOauth2NotAssociated: - next = setting.AppSubUrl + "/user/sign_up" - default: - ctx.Handle(500, "social.SocialSignIn(GetOauth2)", err) - return - } + // oa, err := models.GetOauth2(ui.Identity) + // switch err { + // case nil: + // ctx.Session.Set("uid", oa.User.Id) + // ctx.Session.Set("uname", oa.User.Name) + // case models.ErrOauth2RecordNotExist: + // raw, _ := json.Marshal(tk) + // oa = &models.Oauth2{ + // Uid: -1, + // Type: connect.Type(), + // Identity: ui.Identity, + // Token: string(raw), + // } + // log.Trace("social.SocialSignIn(oa): %v", oa) + // if err = models.AddOauth2(oa); err != nil { + // log.Error(4, "social.SocialSignIn(add oauth2): %v", err) // 501 + // return + // } + // case models.ErrOauth2NotAssociated: + // next = setting.AppSubUrl + "/user/sign_up" + // default: + // ctx.Handle(500, "social.SocialSignIn(GetOauth2)", err) + // return + // } - oa.Updated = time.Now() - if err = models.UpdateOauth2(oa); err != nil { - log.Error(4, "UpdateOauth2: %v", err) - } + // oa.Updated = time.Now() + // if err = models.UpdateOauth2(oa); err != nil { + // log.Error(4, "UpdateOauth2: %v", err) + // } - ctx.Session.Set("socialId", oa.Id) - ctx.Session.Set("socialName", ui.Name) - ctx.Session.Set("socialEmail", ui.Email) - log.Trace("social.SocialSignIn(social ID): %v", oa.Id) - ctx.Redirect(next) + // ctx.Session.Set("socialId", oa.Id) + // ctx.Session.Set("socialName", ui.Name) + // ctx.Session.Set("socialEmail", ui.Email) + // log.Trace("social.SocialSignIn(social ID): %v", oa.Id) + // ctx.Redirect(next) } diff --git a/templates/.VERSION b/templates/.VERSION index d244b039eb..c4127c31a3 100644 --- a/templates/.VERSION +++ b/templates/.VERSION @@ -1 +1 @@ -0.5.8.1125 Beta \ No newline at end of file +0.5.8.1128 Beta \ No newline at end of file diff --git a/templates/ng/base/social.tmpl b/templates/ng/base/social.tmpl index 2d4acd28eb..277b54fcbd 100644 --- a/templates/ng/base/social.tmpl +++ b/templates/ng/base/social.tmpl @@ -1,4 +1,4 @@ -{{if .OauthService.GitHub}}GitHub{{end}} -{{if .OauthService.Google}}Google +{{end}} -{{if .OauthService.Weibo}}新浪微博{{end}} -{{if .OauthService.Tencent}}腾讯 QQ {{end}} \ No newline at end of file +{{if .OauthService.GitHub}}GitHub{{end}} +{{if .OauthService.Google}}Google +{{end}} +{{if .OauthService.Weibo}}新浪微博{{end}} +{{if .OauthService.Tencent}}腾讯 QQ {{end}} \ No newline at end of file