| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| package github |
| |
| import ( |
| "context" |
| "embed" |
| "encoding/json" |
| "fmt" |
| "time" |
| |
| "github.com/apache/answer-plugins/connector-github/i18n" |
| "github.com/apache/answer-plugins/util" |
| "github.com/apache/answer/plugin" |
| "github.com/google/go-github/v50/github" |
| "github.com/segmentfault/pacman/log" |
| "golang.org/x/oauth2" |
| oauth2GitHub "golang.org/x/oauth2/github" |
| ) |
| |
| //go:embed info.yaml |
| var Info embed.FS |
| |
| type Connector struct { |
| Config *ConnectorConfig |
| } |
| |
| type ConnectorConfig struct { |
| ClientID string `json:"client_id"` |
| ClientSecret string `json:"client_secret"` |
| } |
| |
| func init() { |
| plugin.Register(&Connector{ |
| Config: &ConnectorConfig{}, |
| }) |
| } |
| |
| func (g *Connector) Info() plugin.Info { |
| info := &util.Info{} |
| info.GetInfo(Info) |
| |
| return plugin.Info{ |
| Name: plugin.MakeTranslator(i18n.InfoName), |
| SlugName: info.SlugName, |
| Description: plugin.MakeTranslator(i18n.InfoDescription), |
| Author: info.Author, |
| Version: info.Version, |
| Link: info.Link, |
| } |
| } |
| |
| func (g *Connector) ConnectorLogoSVG() string { |
| return `PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMGMtNi42MjYgMC0xMiA1LjM3My0xMiAxMiAwIDUuMzAyIDMuNDM4IDkuOCA4LjIwNyAxMS4zODcuNTk5LjExMS43OTMtLjI2MS43OTMtLjU3N3YtMi4yMzRjLTMuMzM4LjcyNi00LjAzMy0xLjQxNi00LjAzMy0xLjQxNi0uNTQ2LTEuMzg3LTEuMzMzLTEuNzU2LTEuMzMzLTEuNzU2LTEuMDg5LS43NDUuMDgzLS43MjkuMDgzLS43MjkgMS4yMDUuMDg0IDEuODM5IDEuMjM3IDEuODM5IDEuMjM3IDEuMDcgMS44MzQgMi44MDcgMS4zMDQgMy40OTIuOTk3LjEwNy0uNzc1LjQxOC0xLjMwNS43NjItMS42MDQtMi42NjUtLjMwNS01LjQ2Ny0xLjMzNC01LjQ2Ny01LjkzMSAwLTEuMzExLjQ2OS0yLjM4MSAxLjIzNi0zLjIyMS0uMTI0LS4zMDMtLjUzNS0xLjUyNC4xMTctMy4xNzYgMCAwIDEuMDA4LS4zMjIgMy4zMDEgMS4yMy45NTctLjI2NiAxLjk4My0uMzk5IDMuMDAzLS40MDQgMS4wMi4wMDUgMi4wNDcuMTM4IDMuMDA2LjQwNCAyLjI5MS0xLjU1MiAzLjI5Ny0xLjIzIDMuMjk3LTEuMjMuNjUzIDEuNjUzLjI0MiAyLjg3NC4xMTggMy4xNzYuNzcuODQgMS4yMzUgMS45MTEgMS4yMzUgMy4yMjEgMCA0LjYwOS0yLjgwNyA1LjYyNC01LjQ3OSA1LjkyMS40My4zNzIuODIzIDEuMTAyLjgyMyAyLjIyMnYzLjI5M2MwIC4zMTkuMTkyLjY5NC44MDEuNTc2IDQuNzY1LTEuNTg5IDguMTk5LTYuMDg2IDguMTk5LTExLjM4NiAwLTYuNjI3LTUuMzczLTEyLTEyLTEyeiIvPjwvc3ZnPg==` |
| } |
| |
| func (g *Connector) ConnectorName() plugin.Translator { |
| return plugin.MakeTranslator(i18n.ConnectorName) |
| } |
| |
| func (g *Connector) ConnectorSlugName() string { |
| return "github" |
| } |
| |
| func (g *Connector) ConnectorSender(ctx *plugin.GinContext, receiverURL string) (redirectURL string) { |
| oauth2Config := &oauth2.Config{ |
| ClientID: g.Config.ClientID, |
| ClientSecret: g.Config.ClientSecret, |
| Endpoint: oauth2GitHub.Endpoint, |
| RedirectURL: receiverURL, |
| Scopes: []string{"user:email"}, |
| } |
| return oauth2Config.AuthCodeURL("state") |
| } |
| |
| func (g *Connector) ConnectorReceiver(ctx *plugin.GinContext, receiverURL string) (userInfo plugin.ExternalLoginUserInfo, err error) { |
| code := ctx.Query("code") |
| // Exchange code for token |
| oauth2Config := &oauth2.Config{ |
| ClientID: g.Config.ClientID, |
| ClientSecret: g.Config.ClientSecret, |
| Endpoint: oauth2GitHub.Endpoint, |
| } |
| token, err := oauth2Config.Exchange(context.Background(), code) |
| if err != nil { |
| return userInfo, fmt.Errorf("code exchange failed: %s", err.Error()) |
| } |
| |
| // Exchange token for user info |
| client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( |
| &oauth2.Token{AccessToken: token.AccessToken}, |
| )) |
| client.Timeout = 15 * time.Second |
| cli := github.NewClient(client) |
| resp, _, err := cli.Users.Get(context.Background(), "") |
| if err != nil { |
| return userInfo, fmt.Errorf("failed getting user info: %s", err.Error()) |
| } |
| |
| metaInfo, _ := json.Marshal(resp) |
| userInfo = plugin.ExternalLoginUserInfo{ |
| ExternalID: fmt.Sprintf("%d", resp.GetID()), |
| DisplayName: resp.GetName(), |
| Username: resp.GetLogin(), |
| Email: resp.GetEmail(), |
| MetaInfo: string(metaInfo), |
| Avatar: resp.GetAvatarURL(), |
| } |
| |
| // guarantee email was verified |
| userInfo.Email = g.guaranteeEmail(userInfo.Email, token.AccessToken) |
| return userInfo, nil |
| } |
| |
| func (g *Connector) guaranteeEmail(email string, accessToken string) string { |
| client := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( |
| &oauth2.Token{AccessToken: accessToken}, |
| )) |
| client.Timeout = 15 * time.Second |
| cli := github.NewClient(client) |
| |
| emails, _, err := cli.Users.ListEmails(context.Background(), &github.ListOptions{Page: 1}) |
| if err != nil { |
| log.Error(err) |
| return "" |
| } |
| for _, e := range emails { |
| if e.GetPrimary() { |
| return e.GetEmail() |
| } |
| } |
| return email |
| } |
| |
| func (g *Connector) ConfigFields() []plugin.ConfigField { |
| return []plugin.ConfigField{ |
| { |
| Name: "client_id", |
| Type: plugin.ConfigTypeInput, |
| Title: plugin.MakeTranslator(i18n.ConfigClientIDTitle), |
| Description: plugin.MakeTranslator(i18n.ConfigClientIDDescription), |
| Required: true, |
| UIOptions: plugin.ConfigFieldUIOptions{ |
| InputType: plugin.InputTypeText, |
| }, |
| Value: g.Config.ClientID, |
| }, |
| { |
| Name: "client_secret", |
| Type: plugin.ConfigTypeInput, |
| Title: plugin.MakeTranslator(i18n.ConfigClientSecretTitle), |
| Description: plugin.MakeTranslator(i18n.ConfigClientSecretDescription), |
| Required: true, |
| UIOptions: plugin.ConfigFieldUIOptions{ |
| InputType: plugin.InputTypeText, |
| }, |
| Value: g.Config.ClientSecret, |
| }, |
| } |
| } |
| |
| func (g *Connector) ConfigReceiver(config []byte) error { |
| c := &ConnectorConfig{} |
| _ = json.Unmarshal(config, c) |
| g.Config = c |
| return nil |
| } |