mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-21 16:40:22 +00:00
Merge pull request #291 from howarddong711/feat/copilot-email-name
feat(copilot): fetch and persist user email and display name on login
This commit is contained in:
@@ -1929,8 +1929,6 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
|||||||
state := fmt.Sprintf("gh-%d", time.Now().UnixNano())
|
state := fmt.Sprintf("gh-%d", time.Now().UnixNano())
|
||||||
|
|
||||||
// Initialize Copilot auth service
|
// Initialize Copilot auth service
|
||||||
// We need to import "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/copilot" first if not present
|
|
||||||
// Assuming copilot package is imported as "copilot"
|
|
||||||
deviceClient := copilot.NewDeviceFlowClient(h.cfg)
|
deviceClient := copilot.NewDeviceFlowClient(h.cfg)
|
||||||
|
|
||||||
// Initiate device flow
|
// Initiate device flow
|
||||||
@@ -1944,7 +1942,7 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
|||||||
authURL := deviceCode.VerificationURI
|
authURL := deviceCode.VerificationURI
|
||||||
userCode := deviceCode.UserCode
|
userCode := deviceCode.UserCode
|
||||||
|
|
||||||
RegisterOAuthSession(state, "github")
|
RegisterOAuthSession(state, "github-copilot")
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Printf("Please visit %s and enter code: %s\n", authURL, userCode)
|
fmt.Printf("Please visit %s and enter code: %s\n", authURL, userCode)
|
||||||
@@ -1956,9 +1954,13 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
username, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken)
|
userInfo, errUser := deviceClient.FetchUserInfo(ctx, tokenData.AccessToken)
|
||||||
if errUser != nil {
|
if errUser != nil {
|
||||||
log.Warnf("Failed to fetch user info: %v", errUser)
|
log.Warnf("Failed to fetch user info: %v", errUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
username := userInfo.Login
|
||||||
|
if username == "" {
|
||||||
username = "github-user"
|
username = "github-user"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1967,18 +1969,26 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
|||||||
TokenType: tokenData.TokenType,
|
TokenType: tokenData.TokenType,
|
||||||
Scope: tokenData.Scope,
|
Scope: tokenData.Scope,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Email: userInfo.Email,
|
||||||
|
Name: userInfo.Name,
|
||||||
Type: "github-copilot",
|
Type: "github-copilot",
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := fmt.Sprintf("github-%s.json", username)
|
fileName := fmt.Sprintf("github-copilot-%s.json", username)
|
||||||
|
label := userInfo.Email
|
||||||
|
if label == "" {
|
||||||
|
label = username
|
||||||
|
}
|
||||||
record := &coreauth.Auth{
|
record := &coreauth.Auth{
|
||||||
ID: fileName,
|
ID: fileName,
|
||||||
Provider: "github",
|
Provider: "github-copilot",
|
||||||
|
Label: label,
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"email": username,
|
"email": userInfo.Email,
|
||||||
"username": username,
|
"username": username,
|
||||||
|
"name": userInfo.Name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1992,7 +2002,7 @@ func (h *Handler) RequestGitHubToken(c *gin.Context) {
|
|||||||
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||||
fmt.Println("You can now use GitHub Copilot services through this CLI")
|
fmt.Println("You can now use GitHub Copilot services through this CLI")
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
CompleteOAuthSessionsByProvider("github")
|
CompleteOAuthSessionsByProvider("github-copilot")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
|
|||||||
@@ -82,15 +82,21 @@ func (c *CopilotAuth) WaitForAuthorization(ctx context.Context, deviceCode *Devi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the GitHub username
|
// Fetch the GitHub username
|
||||||
username, err := c.deviceClient.FetchUserInfo(ctx, tokenData.AccessToken)
|
userInfo, err := c.deviceClient.FetchUserInfo(ctx, tokenData.AccessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("copilot: failed to fetch user info: %v", err)
|
log.Warnf("copilot: failed to fetch user info: %v", err)
|
||||||
username = "unknown"
|
}
|
||||||
|
|
||||||
|
username := userInfo.Login
|
||||||
|
if username == "" {
|
||||||
|
username = "github-user"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CopilotAuthBundle{
|
return &CopilotAuthBundle{
|
||||||
TokenData: tokenData,
|
TokenData: tokenData,
|
||||||
Username: username,
|
Username: username,
|
||||||
|
Email: userInfo.Email,
|
||||||
|
Name: userInfo.Name,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,12 +156,12 @@ func (c *CopilotAuth) ValidateToken(ctx context.Context, accessToken string) (bo
|
|||||||
return false, "", nil
|
return false, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
username, err := c.deviceClient.FetchUserInfo(ctx, accessToken)
|
userInfo, err := c.deviceClient.FetchUserInfo(ctx, accessToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", err
|
return false, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, username, nil
|
return true, userInfo.Login, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTokenStorage creates a new CopilotTokenStorage from auth bundle.
|
// CreateTokenStorage creates a new CopilotTokenStorage from auth bundle.
|
||||||
@@ -165,6 +171,8 @@ func (c *CopilotAuth) CreateTokenStorage(bundle *CopilotAuthBundle) *CopilotToke
|
|||||||
TokenType: bundle.TokenData.TokenType,
|
TokenType: bundle.TokenData.TokenType,
|
||||||
Scope: bundle.TokenData.Scope,
|
Scope: bundle.TokenData.Scope,
|
||||||
Username: bundle.Username,
|
Username: bundle.Username,
|
||||||
|
Email: bundle.Email,
|
||||||
|
Name: bundle.Name,
|
||||||
Type: "github-copilot",
|
Type: "github-copilot",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient {
|
|||||||
func (c *DeviceFlowClient) RequestDeviceCode(ctx context.Context) (*DeviceCodeResponse, error) {
|
func (c *DeviceFlowClient) RequestDeviceCode(ctx context.Context) (*DeviceCodeResponse, error) {
|
||||||
data := url.Values{}
|
data := url.Values{}
|
||||||
data.Set("client_id", copilotClientID)
|
data.Set("client_id", copilotClientID)
|
||||||
data.Set("scope", "user:email")
|
data.Set("scope", "read:user user:email")
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, copilotDeviceCodeURL, strings.NewReader(data.Encode()))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, copilotDeviceCodeURL, strings.NewReader(data.Encode()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -211,15 +211,25 @@ func (c *DeviceFlowClient) exchangeDeviceCode(ctx context.Context, deviceCode st
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchUserInfo retrieves the GitHub username for the authenticated user.
|
// GitHubUserInfo holds GitHub user profile information.
|
||||||
func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string) (string, error) {
|
type GitHubUserInfo struct {
|
||||||
|
// Login is the GitHub username.
|
||||||
|
Login string
|
||||||
|
// Email is the primary email address (may be empty if not public).
|
||||||
|
Email string
|
||||||
|
// Name is the display name.
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// FetchUserInfo retrieves the GitHub user profile for the authenticated user.
|
||||||
|
func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string) (GitHubUserInfo, error) {
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("access token is empty"))
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("access token is empty"))
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, copilotUserInfoURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, copilotUserInfoURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, err)
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
@@ -227,7 +237,7 @@ func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string
|
|||||||
|
|
||||||
resp, err := c.httpClient.Do(req)
|
resp, err := c.httpClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, err)
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
if errClose := resp.Body.Close(); errClose != nil {
|
if errClose := resp.Body.Close(); errClose != nil {
|
||||||
@@ -237,19 +247,25 @@ func (c *DeviceFlowClient) FetchUserInfo(ctx context.Context, accessToken string
|
|||||||
|
|
||||||
if !isHTTPSuccess(resp.StatusCode) {
|
if !isHTTPSuccess(resp.StatusCode) {
|
||||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes)))
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("status %d: %s", resp.StatusCode, string(bodyBytes)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var userInfo struct {
|
var raw struct {
|
||||||
Login string `json:"login"`
|
Login string `json:"login"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
if err = json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
|
if err = json.NewDecoder(resp.Body).Decode(&raw); err != nil {
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, err)
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if userInfo.Login == "" {
|
if raw.Login == "" {
|
||||||
return "", NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("empty username"))
|
return GitHubUserInfo{}, NewAuthenticationError(ErrUserInfoFailed, fmt.Errorf("empty username"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return userInfo.Login, nil
|
return GitHubUserInfo{
|
||||||
|
Login: raw.Login,
|
||||||
|
Email: raw.Email,
|
||||||
|
Name: raw.Name,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
213
internal/auth/copilot/oauth_test.go
Normal file
213
internal/auth/copilot/oauth_test.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package copilot
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// roundTripFunc lets us inject a custom transport for testing.
|
||||||
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||||
|
|
||||||
|
func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
||||||
|
|
||||||
|
// newTestClient returns an *http.Client whose requests are redirected to the given test server,
|
||||||
|
// regardless of the original URL host.
|
||||||
|
func newTestClient(srv *httptest.Server) *http.Client {
|
||||||
|
return &http.Client{
|
||||||
|
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
|
||||||
|
req2 := req.Clone(req.Context())
|
||||||
|
req2.URL.Scheme = "http"
|
||||||
|
req2.URL.Host = strings.TrimPrefix(srv.URL, "http://")
|
||||||
|
return srv.Client().Transport.RoundTrip(req2)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchUserInfo_FullProfile verifies that FetchUserInfo returns login, email, and name.
|
||||||
|
func TestFetchUserInfo_FullProfile(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"login": "octocat",
|
||||||
|
"email": "octocat@github.com",
|
||||||
|
"name": "The Octocat",
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := &DeviceFlowClient{httpClient: newTestClient(srv)}
|
||||||
|
info, err := client.FetchUserInfo(context.Background(), "test-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if info.Login != "octocat" {
|
||||||
|
t.Errorf("Login: got %q, want %q", info.Login, "octocat")
|
||||||
|
}
|
||||||
|
if info.Email != "octocat@github.com" {
|
||||||
|
t.Errorf("Email: got %q, want %q", info.Email, "octocat@github.com")
|
||||||
|
}
|
||||||
|
if info.Name != "The Octocat" {
|
||||||
|
t.Errorf("Name: got %q, want %q", info.Name, "The Octocat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchUserInfo_EmptyEmail verifies graceful handling when email is absent (private account).
|
||||||
|
func TestFetchUserInfo_EmptyEmail(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
// GitHub returns null for private emails.
|
||||||
|
_, _ = w.Write([]byte(`{"login":"privateuser","email":null,"name":"Private User"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := &DeviceFlowClient{httpClient: newTestClient(srv)}
|
||||||
|
info, err := client.FetchUserInfo(context.Background(), "test-token")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if info.Login != "privateuser" {
|
||||||
|
t.Errorf("Login: got %q, want %q", info.Login, "privateuser")
|
||||||
|
}
|
||||||
|
if info.Email != "" {
|
||||||
|
t.Errorf("Email: got %q, want empty string", info.Email)
|
||||||
|
}
|
||||||
|
if info.Name != "Private User" {
|
||||||
|
t.Errorf("Name: got %q, want %q", info.Name, "Private User")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchUserInfo_EmptyToken verifies error is returned for empty access token.
|
||||||
|
func TestFetchUserInfo_EmptyToken(t *testing.T) {
|
||||||
|
client := &DeviceFlowClient{httpClient: http.DefaultClient}
|
||||||
|
_, err := client.FetchUserInfo(context.Background(), "")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty token, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchUserInfo_EmptyLogin verifies error is returned when API returns no login.
|
||||||
|
func TestFetchUserInfo_EmptyLogin(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"email":"someone@example.com","name":"No Login"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := &DeviceFlowClient{httpClient: newTestClient(srv)}
|
||||||
|
_, err := client.FetchUserInfo(context.Background(), "test-token")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for empty login, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFetchUserInfo_HTTPError verifies error is returned on non-2xx response.
|
||||||
|
func TestFetchUserInfo_HTTPError(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
_, _ = w.Write([]byte(`{"message":"Bad credentials"}`))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
client := &DeviceFlowClient{httpClient: newTestClient(srv)}
|
||||||
|
_, err := client.FetchUserInfo(context.Background(), "bad-token")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for 401 response, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCopilotTokenStorage_EmailNameFields verifies Email and Name serialise correctly.
|
||||||
|
func TestCopilotTokenStorage_EmailNameFields(t *testing.T) {
|
||||||
|
ts := &CopilotTokenStorage{
|
||||||
|
AccessToken: "ghu_abc",
|
||||||
|
TokenType: "bearer",
|
||||||
|
Scope: "read:user user:email",
|
||||||
|
Username: "octocat",
|
||||||
|
Email: "octocat@github.com",
|
||||||
|
Name: "The Octocat",
|
||||||
|
Type: "github-copilot",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out map[string]any
|
||||||
|
if err = json.Unmarshal(data, &out); err != nil {
|
||||||
|
t.Fatalf("unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, key := range []string{"access_token", "username", "email", "name", "type"} {
|
||||||
|
if _, ok := out[key]; !ok {
|
||||||
|
t.Errorf("expected key %q in JSON output, not found", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out["email"] != "octocat@github.com" {
|
||||||
|
t.Errorf("email: got %v, want %q", out["email"], "octocat@github.com")
|
||||||
|
}
|
||||||
|
if out["name"] != "The Octocat" {
|
||||||
|
t.Errorf("name: got %v, want %q", out["name"], "The Octocat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCopilotTokenStorage_OmitEmptyEmailName verifies email/name are omitted when empty (omitempty).
|
||||||
|
func TestCopilotTokenStorage_OmitEmptyEmailName(t *testing.T) {
|
||||||
|
ts := &CopilotTokenStorage{
|
||||||
|
AccessToken: "ghu_abc",
|
||||||
|
Username: "octocat",
|
||||||
|
Type: "github-copilot",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.Marshal(ts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var out map[string]any
|
||||||
|
if err = json.Unmarshal(data, &out); err != nil {
|
||||||
|
t.Fatalf("unmarshal error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := out["email"]; ok {
|
||||||
|
t.Error("email key should be omitted when empty (omitempty), but was present")
|
||||||
|
}
|
||||||
|
if _, ok := out["name"]; ok {
|
||||||
|
t.Error("name key should be omitted when empty (omitempty), but was present")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCopilotAuthBundle_EmailNameFields verifies bundle carries email and name through the pipeline.
|
||||||
|
func TestCopilotAuthBundle_EmailNameFields(t *testing.T) {
|
||||||
|
bundle := &CopilotAuthBundle{
|
||||||
|
TokenData: &CopilotTokenData{AccessToken: "ghu_abc"},
|
||||||
|
Username: "octocat",
|
||||||
|
Email: "octocat@github.com",
|
||||||
|
Name: "The Octocat",
|
||||||
|
}
|
||||||
|
if bundle.Email != "octocat@github.com" {
|
||||||
|
t.Errorf("bundle.Email: got %q, want %q", bundle.Email, "octocat@github.com")
|
||||||
|
}
|
||||||
|
if bundle.Name != "The Octocat" {
|
||||||
|
t.Errorf("bundle.Name: got %q, want %q", bundle.Name, "The Octocat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGitHubUserInfo_Struct verifies the exported GitHubUserInfo struct fields are accessible.
|
||||||
|
func TestGitHubUserInfo_Struct(t *testing.T) {
|
||||||
|
info := GitHubUserInfo{
|
||||||
|
Login: "octocat",
|
||||||
|
Email: "octocat@github.com",
|
||||||
|
Name: "The Octocat",
|
||||||
|
}
|
||||||
|
if info.Login == "" || info.Email == "" || info.Name == "" {
|
||||||
|
t.Error("GitHubUserInfo fields should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,10 @@ type CopilotTokenStorage struct {
|
|||||||
ExpiresAt string `json:"expires_at,omitempty"`
|
ExpiresAt string `json:"expires_at,omitempty"`
|
||||||
// Username is the GitHub username associated with this token.
|
// Username is the GitHub username associated with this token.
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
|
// Email is the GitHub email address associated with this token.
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
// Name is the GitHub display name associated with this token.
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
// Type indicates the authentication provider type, always "github-copilot" for this storage.
|
// Type indicates the authentication provider type, always "github-copilot" for this storage.
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
@@ -46,6 +50,10 @@ type CopilotAuthBundle struct {
|
|||||||
TokenData *CopilotTokenData
|
TokenData *CopilotTokenData
|
||||||
// Username is the GitHub username.
|
// Username is the GitHub username.
|
||||||
Username string
|
Username string
|
||||||
|
// Email is the GitHub email address.
|
||||||
|
Email string
|
||||||
|
// Name is the GitHub display name.
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeviceCodeResponse represents GitHub's device code response.
|
// DeviceCodeResponse represents GitHub's device code response.
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ func (a GitHubCopilotAuthenticator) Login(ctx context.Context, cfg *config.Confi
|
|||||||
metadata := map[string]any{
|
metadata := map[string]any{
|
||||||
"type": "github-copilot",
|
"type": "github-copilot",
|
||||||
"username": authBundle.Username,
|
"username": authBundle.Username,
|
||||||
|
"email": authBundle.Email,
|
||||||
|
"name": authBundle.Name,
|
||||||
"access_token": authBundle.TokenData.AccessToken,
|
"access_token": authBundle.TokenData.AccessToken,
|
||||||
"token_type": authBundle.TokenData.TokenType,
|
"token_type": authBundle.TokenData.TokenType,
|
||||||
"scope": authBundle.TokenData.Scope,
|
"scope": authBundle.TokenData.Scope,
|
||||||
@@ -98,13 +100,18 @@ func (a GitHubCopilotAuthenticator) Login(ctx context.Context, cfg *config.Confi
|
|||||||
|
|
||||||
fileName := fmt.Sprintf("github-copilot-%s.json", authBundle.Username)
|
fileName := fmt.Sprintf("github-copilot-%s.json", authBundle.Username)
|
||||||
|
|
||||||
|
label := authBundle.Email
|
||||||
|
if label == "" {
|
||||||
|
label = authBundle.Username
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("\nGitHub Copilot authentication successful for user: %s\n", authBundle.Username)
|
fmt.Printf("\nGitHub Copilot authentication successful for user: %s\n", authBundle.Username)
|
||||||
|
|
||||||
return &coreauth.Auth{
|
return &coreauth.Auth{
|
||||||
ID: fileName,
|
ID: fileName,
|
||||||
Provider: a.Provider(),
|
Provider: a.Provider(),
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
Label: authBundle.Username,
|
Label: label,
|
||||||
Storage: tokenStorage,
|
Storage: tokenStorage,
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user