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) }