Copilot commented on code in PR #8667:
URL: 
https://github.com/apache/incubator-devlake/pull/8667#discussion_r2656300323


##########
backend/plugins/github/models/connection.go:
##########
@@ -56,6 +56,21 @@ type GithubConn struct {
        helper.MultiAuth      `mapstructure:",squash"`
        GithubAccessToken     `mapstructure:",squash" authMethod:"AccessToken"`
        GithubAppKey          `mapstructure:",squash" authMethod:"AppKey"`
+       RefreshToken          string    `mapstructure:"refreshToken" 
json:"refreshToken" gorm:"type:text;serializer:encdec"`
+       TokenExpiresAt        time.Time `mapstructure:"tokenExpiresAt" 
json:"tokenExpiresAt"`
+       RefreshTokenExpiresAt time.Time `mapstructure:"refreshTokenExpiresAt" 
json:"refreshTokenExpiresAt"`
+}
+
+// UpdateToken updates the token and refresh token information

Review Comment:
   Missing documentation comment for the exported method UpdateToken. According 
to Go best practices, all exported methods should have documentation comments 
explaining their parameters and behavior, especially for a method that modifies 
critical security-related state.
   ```suggestion
   // UpdateToken updates the access token, refresh token and their expiration 
times
   // for this GithubConn instance. newToken is the access token value, 
newRefreshToken
   // is the corresponding refresh token, expiry is the time at which the 
access token
   // expires, and refreshExpiry is the time at which the refresh token expires.
   // In addition to updating the exported fields, this method also resets the 
internal
   // tokens slice and tokenIndex used by authentication logic to use the new 
access token.
   ```



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+
+type TokenProvider struct {
+       conn       *models.GithubConnection
+       dal        dal.Dal
+       httpClient *http.Client
+       logger     log.Logger
+       mu         sync.Mutex
+       refreshURL string
+}
+
+func NewTokenProvider(conn *models.GithubConnection, d dal.Dal, client 
*http.Client, logger log.Logger) *TokenProvider {
+       return &TokenProvider{
+               conn:       conn,
+               dal:        d,
+               httpClient: client,
+               logger:     logger,
+               refreshURL: "https://github.com/login/oauth/access_token";,
+       }
+}
+
+func (tp *TokenProvider) GetToken() (string, errors.Error) {
+       tp.mu.Lock()
+       defer tp.mu.Unlock()
+
+       if tp.needsRefresh() {
+               if err := tp.refreshToken(); err != nil {
+                       return "", err
+               }
+       }
+       return tp.conn.Token, nil
+}
+
+func (tp *TokenProvider) needsRefresh() bool {
+       if tp.conn.RefreshToken == "" {
+               return false
+       }
+
+       buffer := DefaultRefreshBuffer
+       if envBuffer := os.Getenv("GITHUB_TOKEN_REFRESH_BUFFER_MINUTES"); 
envBuffer != "" {
+               if val, err := strconv.Atoi(envBuffer); err == nil {
+                       buffer = time.Duration(val) * time.Minute
+               }
+       }
+
+       return time.Now().Add(buffer).After(tp.conn.TokenExpiresAt)
+}
+
+func (tp *TokenProvider) refreshToken() errors.Error {
+       tp.logger.Info("Refreshing GitHub token for connection %d", tp.conn.ID)
+
+       data := map[string]string{
+               "refresh_token": tp.conn.RefreshToken,
+               "grant_type":    "refresh_token",
+               "client_id":     tp.conn.AppId,
+               "client_secret": tp.conn.SecretKey,
+       }
+       jsonData, _ := json.Marshal(data)
+
+       ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+       defer cancel()
+
+       req, err := http.NewRequestWithContext(ctx, "POST", tp.refreshURL, 
bytes.NewBuffer(jsonData))
+       if err != nil {
+               return errors.Convert(err)
+       }
+       req.Header.Set("Content-Type", "application/json")
+       req.Header.Set("Accept", "application/json")
+
+       resp, err := tp.httpClient.Do(req)
+       if err != nil {
+               return errors.Convert(err)
+       }
+       defer resp.Body.Close()
+
+       body, err := io.ReadAll(resp.Body)
+       if err != nil {
+               return errors.Convert(err)
+       }
+
+       if resp.StatusCode != http.StatusOK {
+               // Log the response body to aid in debugging token refresh 
failures.
+               if tp.logger != nil {
+                       tp.logger.Error(nil, "failed to refresh token from 
GitHub, status=%d, body=%s", resp.StatusCode, string(body))
+               }
+               return errors.Default.New(fmt.Sprintf("failed to refresh token: 
%d, body: %s", resp.StatusCode, string(body)))
+       }
+       var result struct {
+               AccessToken           string `json:"access_token"`
+               RefreshToken          string `json:"refresh_token"`
+               ExpiresIn             int    `json:"expires_in"`
+               RefreshTokenExpiresIn int    `json:"refresh_token_expires_in"`
+       }
+       if err := json.Unmarshal(body, &result); err != nil {
+               return errors.Convert(err)
+       }
+
+       if result.AccessToken == "" {
+               bodyStr := string(body)
+               const maxBodySnippet = 512
+               if len(bodyStr) > maxBodySnippet {
+                       bodyStr = bodyStr[:maxBodySnippet] + "…"
+               }
+               return errors.Default.New(fmt.Sprintf("empty access token 
returned; response body: %s", bodyStr))
+       }
+
+       tp.conn.UpdateToken(
+               result.AccessToken,
+               result.RefreshToken,
+               time.Now().Add(time.Duration(result.ExpiresIn)*time.Second),
+               
time.Now().Add(time.Duration(result.RefreshTokenExpiresIn)*time.Second),
+       )
+
+       if tp.dal != nil {
+               err := tp.dal.UpdateColumns(tp.conn, []dal.DalSet{
+                       {ColumnName: "token", Value: tp.conn.Token},
+                       {ColumnName: "refresh_token", Value: 
tp.conn.RefreshToken},
+                       {ColumnName: "token_expires_at", Value: 
tp.conn.TokenExpiresAt},
+                       {ColumnName: "refresh_token_expires_at", Value: 
tp.conn.RefreshTokenExpiresAt},
+               })
+               if err != nil {
+                       tp.logger.Warn(err, "failed to persist refreshed token")
+               }
+       }
+
+       return nil
+}
+

Review Comment:
   Missing documentation comment for the exported method ForceRefresh. 
According to Go best practices, all exported methods should have documentation 
comments explaining their behavior, especially explaining the purpose of the 
oldToken parameter and how it's used to prevent redundant refreshes.
   ```suggestion
   
   // ForceRefresh refreshes the access token if the current token is still 
equal to oldToken.
   // The oldToken parameter should be the token value observed by the caller 
when it determined
   // that a refresh might be needed; if the token has changed since then, 
another goroutine has
   // already refreshed it and this method returns without performing a 
redundant refresh.
   ```



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+
+type TokenProvider struct {
+       conn       *models.GithubConnection
+       dal        dal.Dal
+       httpClient *http.Client
+       logger     log.Logger
+       mu         sync.Mutex
+       refreshURL string
+}
+

Review Comment:
   Missing documentation comment for the exported function NewTokenProvider. 
According to Go best practices, all exported functions should have 
documentation comments explaining their parameters and return values.
   ```suggestion
   
   // NewTokenProvider creates a TokenProvider for the given GitHub connection 
using
   // the provided DAL, HTTP client, and logger, and returns a pointer to it.
   ```



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+
+type TokenProvider struct {
+       conn       *models.GithubConnection
+       dal        dal.Dal
+       httpClient *http.Client
+       logger     log.Logger
+       mu         sync.Mutex
+       refreshURL string
+}
+
+func NewTokenProvider(conn *models.GithubConnection, d dal.Dal, client 
*http.Client, logger log.Logger) *TokenProvider {
+       return &TokenProvider{
+               conn:       conn,
+               dal:        d,
+               httpClient: client,
+               logger:     logger,
+               refreshURL: "https://github.com/login/oauth/access_token";,
+       }
+}
+
+func (tp *TokenProvider) GetToken() (string, errors.Error) {
+       tp.mu.Lock()
+       defer tp.mu.Unlock()
+
+       if tp.needsRefresh() {
+               if err := tp.refreshToken(); err != nil {
+                       return "", err
+               }
+       }
+       return tp.conn.Token, nil
+}
+
+func (tp *TokenProvider) needsRefresh() bool {
+       if tp.conn.RefreshToken == "" {
+               return false
+       }
+
+       buffer := DefaultRefreshBuffer
+       if envBuffer := os.Getenv("GITHUB_TOKEN_REFRESH_BUFFER_MINUTES"); 
envBuffer != "" {
+               if val, err := strconv.Atoi(envBuffer); err == nil {
+                       buffer = time.Duration(val) * time.Minute
+               }
+       }

Review Comment:
   The environment variable check is performed on every call to needsRefresh(), 
which could be inefficient. Consider caching the buffer value once during 
initialization or using sync.Once for lazy initialization to avoid repeated 
environment variable lookups and parsing.



##########
backend/plugins/github/token/round_tripper.go:
##########
@@ -0,0 +1,90 @@
+/*
+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 token
+
+import (
+       "net/http"
+)
+
+// RefreshRoundTripper is an HTTP transport middleware that automatically 
manages OAuth token refreshes.
+// It wraps an underlying http.RoundTripper and provides token refresh on auth 
failures.
+// On 401's the round tripper will:
+// - Force a refresh of the OAuth token via the TokenProvider
+// - Retry the original request with the new token
+type RefreshRoundTripper struct {
+       base          http.RoundTripper
+       tokenProvider *TokenProvider
+}
+
+func NewRefreshRoundTripper(base http.RoundTripper, tp *TokenProvider) 
*RefreshRoundTripper {
+       return &RefreshRoundTripper{
+               base:          base,
+               tokenProvider: tp,
+       }
+}
+
+// RoundTrip implements the http.RoundTripper interface and handles automatic 
token refresh on 401 responses.
+// It clones the request, adds the Authorization header, and retries once with 
a refreshed token if needed.
+func (rt *RefreshRoundTripper) RoundTrip(req *http.Request) (*http.Response, 
error) {
+       return rt.roundTripWithRetry(req, false)
+}
+
+// roundTripWithRetry performs the actual request with retry on 401.
+// The refreshAttempted parameter tracks whether a refresh has already been 
tried for this request
+// to prevent infinite retry loops if token refresh itself fails.
+func (rt *RefreshRoundTripper) roundTripWithRetry(req *http.Request, 
refreshAttempted bool) (*http.Response, error) {
+       // Get token
+       token, err := rt.tokenProvider.GetToken()
+       if err != nil {
+               return nil, err
+       }
+
+       // Clone request before modifying
+       reqClone := req.Clone(req.Context())

Review Comment:
   The req.Clone() method does not clone the request body, which means if the 
original request has a body (e.g., POST/PUT requests), attempting to retry the 
request after a 401 error will fail because the body has already been read. The 
body needs to be preserved or made replayable for retry scenarios. Consider 
using req.GetBody if available, or document that this implementation only 
supports requests without bodies.



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+
+type TokenProvider struct {
+       conn       *models.GithubConnection
+       dal        dal.Dal
+       httpClient *http.Client
+       logger     log.Logger
+       mu         sync.Mutex
+       refreshURL string
+}
+
+func NewTokenProvider(conn *models.GithubConnection, d dal.Dal, client 
*http.Client, logger log.Logger) *TokenProvider {
+       return &TokenProvider{
+               conn:       conn,
+               dal:        d,
+               httpClient: client,
+               logger:     logger,
+               refreshURL: "https://github.com/login/oauth/access_token";,
+       }
+}
+

Review Comment:
   Missing documentation comment for the exported method GetToken. According to 
Go best practices, all exported methods should have documentation comments 
explaining their behavior, return values, and any important side effects.
   ```suggestion
   
   // GetToken returns the current access token for the GitHub connection.
   // If the token is close to expiry and a refresh token is available, it
   // refreshes the token under a mutex lock and returns the updated value.
   // On failure to refresh, it returns a non-nil errors.Error.
   ```



##########
backend/plugins/github/token/round_tripper.go:
##########
@@ -0,0 +1,90 @@
+/*
+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 token
+
+import (
+       "net/http"
+)
+
+// RefreshRoundTripper is an HTTP transport middleware that automatically 
manages OAuth token refreshes.
+// It wraps an underlying http.RoundTripper and provides token refresh on auth 
failures.
+// On 401's the round tripper will:
+// - Force a refresh of the OAuth token via the TokenProvider
+// - Retry the original request with the new token
+type RefreshRoundTripper struct {
+       base          http.RoundTripper
+       tokenProvider *TokenProvider
+}
+
+func NewRefreshRoundTripper(base http.RoundTripper, tp *TokenProvider) 
*RefreshRoundTripper {
+       return &RefreshRoundTripper{
+               base:          base,
+               tokenProvider: tp,
+       }
+}
+
+// RoundTrip implements the http.RoundTripper interface and handles automatic 
token refresh on 401 responses.
+// It clones the request, adds the Authorization header, and retries once with 
a refreshed token if needed.
+func (rt *RefreshRoundTripper) RoundTrip(req *http.Request) (*http.Response, 
error) {
+       return rt.roundTripWithRetry(req, false)
+}
+
+// roundTripWithRetry performs the actual request with retry on 401.
+// The refreshAttempted parameter tracks whether a refresh has already been 
tried for this request
+// to prevent infinite retry loops if token refresh itself fails.
+func (rt *RefreshRoundTripper) roundTripWithRetry(req *http.Request, 
refreshAttempted bool) (*http.Response, error) {
+       // Get token
+       token, err := rt.tokenProvider.GetToken()
+       if err != nil {
+               return nil, err
+       }
+
+       // Clone request before modifying
+       reqClone := req.Clone(req.Context())
+       reqClone.Header.Set("Authorization", "Bearer "+token)
+
+       // Execute request
+       resp, reqErr := rt.base.RoundTrip(reqClone)
+       if reqErr != nil {
+               return nil, reqErr
+       }
+
+       // Reactive refresh on 401
+       if resp.StatusCode == http.StatusUnauthorized && !refreshAttempted {
+               // Close previous response body
+               resp.Body.Close()
+
+               // Force refresh
+               if err := rt.tokenProvider.ForceRefresh(token); err != nil {
+                       return nil, err
+               }
+
+               // Get new token
+               newToken, err := rt.tokenProvider.GetToken()
+               if err != nil {
+                       return nil, err
+               }
+
+               // Retry request with new token
+               reqRetry := req.Clone(req.Context())

Review Comment:
   The req.Clone() method is called again for the retry request, but request 
bodies cannot be replayed after being read. If this is a POST/PUT/PATCH request 
with a body, the retry will fail or send an empty body. This is the same issue 
as line 58 - the request body needs to be preserved or made replayable.



##########
backend/plugins/github/models/connection.go:
##########
@@ -56,6 +56,21 @@ type GithubConn struct {
        helper.MultiAuth      `mapstructure:",squash"`
        GithubAccessToken     `mapstructure:",squash" authMethod:"AccessToken"`
        GithubAppKey          `mapstructure:",squash" authMethod:"AppKey"`
+       RefreshToken          string    `mapstructure:"refreshToken" 
json:"refreshToken" gorm:"type:text;serializer:encdec"`
+       TokenExpiresAt        time.Time `mapstructure:"tokenExpiresAt" 
json:"tokenExpiresAt"`
+       RefreshTokenExpiresAt time.Time `mapstructure:"refreshTokenExpiresAt" 
json:"refreshTokenExpiresAt"`
+}
+
+// UpdateToken updates the token and refresh token information
+func (conn *GithubConn) UpdateToken(newToken, newRefreshToken string, expiry, 
refreshExpiry time.Time) {
+       conn.Token = newToken
+       conn.RefreshToken = newRefreshToken
+       conn.TokenExpiresAt = expiry
+       conn.RefreshTokenExpiresAt = refreshExpiry
+
+       // Update the internal tokens slice used by SetupAuthentication
+       conn.tokens = []string{newToken}
+       conn.tokenIndex = 0
 }

Review Comment:
   The new UpdateToken method lacks test coverage. Since this method modifies 
critical security-related state (tokens and expiry times), it should have unit 
tests to verify that all fields are updated correctly, including the internal 
tokens slice and tokenIndex.



##########
backend/plugins/github/token/round_tripper.go:
##########
@@ -0,0 +1,90 @@
+/*
+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 token
+
+import (
+       "net/http"
+)
+
+// RefreshRoundTripper is an HTTP transport middleware that automatically 
manages OAuth token refreshes.
+// It wraps an underlying http.RoundTripper and provides token refresh on auth 
failures.
+// On 401's the round tripper will:
+// - Force a refresh of the OAuth token via the TokenProvider
+// - Retry the original request with the new token
+type RefreshRoundTripper struct {
+       base          http.RoundTripper
+       tokenProvider *TokenProvider
+}
+

Review Comment:
   Missing documentation comment for the exported function 
NewRefreshRoundTripper. According to Go best practices, all exported functions 
should have documentation comments explaining their parameters and return 
values.
   ```suggestion
   
   // NewRefreshRoundTripper creates a new RefreshRoundTripper that wraps the 
given base RoundTripper
   // and uses the provided TokenProvider to obtain and refresh OAuth tokens. 
It returns a configured
   // RefreshRoundTripper that can be used as an http.RoundTripper.
   ```



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+
+type TokenProvider struct {
+       conn       *models.GithubConnection
+       dal        dal.Dal
+       httpClient *http.Client
+       logger     log.Logger
+       mu         sync.Mutex
+       refreshURL string
+}
+
+func NewTokenProvider(conn *models.GithubConnection, d dal.Dal, client 
*http.Client, logger log.Logger) *TokenProvider {
+       return &TokenProvider{
+               conn:       conn,
+               dal:        d,
+               httpClient: client,
+               logger:     logger,
+               refreshURL: "https://github.com/login/oauth/access_token";,
+       }
+}
+
+func (tp *TokenProvider) GetToken() (string, errors.Error) {
+       tp.mu.Lock()
+       defer tp.mu.Unlock()
+
+       if tp.needsRefresh() {
+               if err := tp.refreshToken(); err != nil {
+                       return "", err
+               }
+       }
+       return tp.conn.Token, nil
+}
+
+func (tp *TokenProvider) needsRefresh() bool {
+       if tp.conn.RefreshToken == "" {
+               return false
+       }
+
+       buffer := DefaultRefreshBuffer
+       if envBuffer := os.Getenv("GITHUB_TOKEN_REFRESH_BUFFER_MINUTES"); 
envBuffer != "" {
+               if val, err := strconv.Atoi(envBuffer); err == nil {
+                       buffer = time.Duration(val) * time.Minute
+               }
+       }
+
+       return time.Now().Add(buffer).After(tp.conn.TokenExpiresAt)
+}
+
+func (tp *TokenProvider) refreshToken() errors.Error {
+       tp.logger.Info("Refreshing GitHub token for connection %d", tp.conn.ID)
+
+       data := map[string]string{
+               "refresh_token": tp.conn.RefreshToken,
+               "grant_type":    "refresh_token",
+               "client_id":     tp.conn.AppId,
+               "client_secret": tp.conn.SecretKey,
+       }
+       jsonData, _ := json.Marshal(data)

Review Comment:
   The error from json.Marshal is silently ignored. While marshaling a simple 
map[string]string is unlikely to fail, errors should still be handled properly 
for robustness and to follow best practices. Consider returning or logging the 
error if it occurs.
   ```suggestion
        jsonData, err := json.Marshal(data)
        if err != nil {
                return errors.Convert(err)
        }
   ```



##########
backend/plugins/github/token/token_provider.go:
##########
@@ -0,0 +1,179 @@
+/*
+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 token
+
+import (
+       "bytes"
+       "context"
+       "encoding/json"
+       "fmt"
+       "io"
+       "net/http"
+       "os"
+       "strconv"
+       "sync"
+       "time"
+
+       "github.com/apache/incubator-devlake/core/dal"
+       "github.com/apache/incubator-devlake/core/errors"
+       "github.com/apache/incubator-devlake/core/log"
+       "github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+const (
+       DefaultRefreshBuffer = 5 * time.Minute
+)
+

Review Comment:
   Missing documentation comment for the exported type TokenProvider. According 
to Go best practices, all exported types should have documentation comments 
that describe their purpose and usage.
   ```suggestion
   
   // TokenProvider provides GitHub access tokens for a connection and 
refreshes them when necessary.
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to