- 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.
198 lines
No EOL
5.4 KiB
Go
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)
|
|
} |