core/repo-integrations-core/internal/api/github.go
Tiago Yamamoto a52bd4519d refactor: optimize Dockerfiles and documentation for core services
- Use Google Distroless images for all services (Go & Node.js).
- Standardize documentation with [PROJECT-NAME].md.
- Add .dockerignore and .gitignore to all projects.
- Remove docker-compose.yml in favor of docker run instructions.
- Fix Go version and dependency issues in observability, repo-integrations, and security-governance.
- Add Podman support (fully qualified image names).
- Update Dashboard to use Node.js static server for Distroless compatibility.
2025-12-30 13:22:34 -03:00

198 lines
No EOL
5.4 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/google/go-github/v53/github"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"github.com/lab/repo-integrations-core/internal/config"
"github.com/lab/repo-integrations-core/internal/crypto"
"github.com/lab/repo-integrations-core/internal/db"
"golang.org/x/oauth2"
oauth2github "golang.org/x/oauth2/github"
)
type API struct {
config *config.Config
queries *db.Queries
}
func New(config *config.Config, queries *db.Queries) *API {
return &API{
config: config,
queries: queries,
}
}
func (a *API) getGithubOAuthConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: a.config.GithubClientID,
ClientSecret: a.config.GithubSecret,
Endpoint: oauth2github.Endpoint,
RedirectURL: "http://localhost:8080/integrations/github/callback",
Scopes: []string{"repo", "admin:repo_hook"},
}
}
func (a *API) ConnectGithubHandler(w http.ResponseWriter, r *http.Request) {
// For now, we'll hardcode the tenant_id. In a real app, this would come from the JWT.
tenantID := uuid.New()
url := a.getGithubOAuthConfig().AuthCodeURL(tenantID.String(), oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
type githubUser struct {
ID int64 `json:"id"`
Login string `json:"login"`
}
func (a *API) ConnectGithubCallbackHandler(w http.ResponseWriter, r *http.Request) {
state := r.URL.Query().Get("state")
code := r.URL.Query().Get("code")
tenantID, err := uuid.Parse(state)
if err != nil {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
githubOauthConfig := a.getGithubOAuthConfig()
token, err := githubOauthConfig.Exchange(context.Background(), code)
if err != nil {
http.Error(w, "Failed to exchange token", http.StatusInternalServerError)
return
}
// Get user info from GitHub
client := githubOauthConfig.Client(context.Background(), token)
resp, err := client.Get("https://api.github.com/user")
if err != nil {
http.Error(w, "Failed to get user info", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, "Failed to read user info", http.StatusInternalServerError)
return
}
var user githubUser
if err := json.Unmarshal(body, &user); err != nil {
http.Error(w, "Failed to parse user info", http.StatusInternalServerError)
return
}
encryptedAccessToken, err := crypto.Encrypt(token.AccessToken, a.config.EncryptionKey)
if err != nil {
http.Error(w, "Failed to encrypt token", http.StatusInternalServerError)
return
}
var encryptedRefreshToken string
if token.RefreshToken != "" {
encryptedRefreshToken, err = crypto.Encrypt(token.RefreshToken, a.config.EncryptionKey)
if err != nil {
http.Error(w, "Failed to encrypt refresh token", http.StatusInternalServerError)
return
}
}
params := db.CreateRepoAccountParams{
TenantID: pgtype.UUID{Bytes: tenantID, Valid: true},
Provider: string(db.GitProviderGithub),
AccountID: fmt.Sprintf("%d", user.ID),
Username: user.Login,
EncryptedAccessToken: []byte(encryptedAccessToken),
}
if encryptedRefreshToken != "" {
params.EncryptedRefreshToken = []byte(encryptedRefreshToken)
}
_, err = a.queries.CreateRepoAccount(context.Background(), params)
if err != nil {
http.Error(w, "Failed to save account", http.StatusInternalServerError)
return
}
w.Write([]byte("Successfully connected to GitHub!"))
}
func (a *API) GithubWebhookHandler(w http.ResponseWriter, r *http.Request) {
// TODO: Implement webhook signature validation
tenantIDStr := r.URL.Query().Get("tenant_id")
tenantID, err := uuid.Parse(tenantIDStr)
if err != nil {
http.Error(w, "Invalid tenant_id", http.StatusBadRequest)
return
}
payload, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()
event, err := github.ParseWebHook(github.WebHookType(r), payload)
if err != nil {
http.Error(w, "Could not parse webhook", http.StatusBadRequest)
return
}
var eventType db.EventType
var repoExternalID string
var repoID pgtype.UUID
switch e := event.(type) {
case *github.PushEvent:
eventType = db.EventTypePush
repoExternalID = fmt.Sprintf("%d", e.Repo.GetID())
case *github.PullRequestEvent:
eventType = db.EventTypePullRequest
repoExternalID = fmt.Sprintf("%d", e.Repo.GetID())
case *github.ReleaseEvent:
eventType = db.EventTypeRelease
repoExternalID = fmt.Sprintf("%d", e.Repo.GetID())
default:
w.WriteHeader(http.StatusOK)
return
}
repo, err := a.queries.GetRepositoryByExternalID(r.Context(), db.GetRepositoryByExternalIDParams{
TenantID: pgtype.UUID{Bytes: tenantID, Valid: true},
ExternalID: repoExternalID,
})
if err != nil {
http.Error(w, "Repository not found", http.StatusNotFound)
return
}
repoID = repo.ID
jsonPayload, err := json.Marshal(event)
if err != nil {
http.Error(w, "Failed to marshal event payload", http.StatusInternalServerError)
return
}
_, err = a.queries.CreateRepoEvent(r.Context(), db.CreateRepoEventParams{
TenantID: pgtype.UUID{Bytes: tenantID, Valid: true},
RepositoryID: repoID,
EventType: string(eventType),
Payload: jsonPayload,
})
if err != nil {
http.Error(w, "Failed to create repo event", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}