Skip to content

GitHub integration #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions app/client/workspace/workspace_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,55 @@ type WorkspaceServiceClient struct {
slackAlert *monitoring.SlackAlert
}

func (ws *WorkspaceServiceClient) ImportGitRepository(importRepository *request.ImportGitRepository) (createWorkspaceResponse *response.CreateWorkspaceResponse, err error) {
payload, err := json.Marshal(importRepository)
if err != nil {
log.Printf("failed to marshal import git repository request: %v", err)
return
}

req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/workspaces/import", ws.endpoint), bytes.NewBuffer(payload))
if err != nil {
log.Printf("failed to create import git repository request: %v", err)
return
}

res, err := ws.client.Do(req)
if err != nil {
log.Printf("failed to send import git repository request: %v", err)
return
}

if res.StatusCode < 200 || res.StatusCode > 299 {
err := ws.slackAlert.SendAlert(fmt.Sprintf("failed to import git repository: %s", res.Status), map[string]string{
"workspace_id": importRepository.WorkspaceId,
"repository": importRepository.Repository,
})
if err != nil {
log.Printf("failed to send slack alert: %v", err)
return nil, err
}
return nil, errors.New(fmt.Sprintf("invalid res from workspace service for import git repository request"))
}

defer func(Body io.ReadCloser) {
_ = Body.Close()
}(res.Body)

responseBody, err := io.ReadAll(res.Body)
if err != nil {
log.Printf("failed to read res payload: %v", err)
return
}

createWorkspaceResponse = &response.CreateWorkspaceResponse{}
if err = json.Unmarshal(responseBody, &createWorkspaceResponse); err != nil {
log.Printf("failed to unmarshal create workspace res: %v", err)
return
}
return
}

func (ws *WorkspaceServiceClient) CreateWorkspace(createWorkspaceRequest *request.CreateWorkspaceRequest) (createWorkspaceResponse *response.CreateWorkspaceResponse, err error) {
payload, err := json.Marshal(createWorkspaceRequest)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion app/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func LoadConfig() (*koanf.Koanf, error) {
return config, err
}

// Get returns the value for a given key.
// Deprecated: This is a misuse of the config package.
func Get(key string) interface{} {
return config.Get(key)
}
23 changes: 23 additions & 0 deletions app/config/github_integration_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package config

import "github.com/knadh/koanf/v2"

type GithubIntegrationConfig struct {
config *koanf.Koanf
}

func (gic *GithubIntegrationConfig) GetClientID() string {
return config.String("github.integration.client.id")
}

func (gic *GithubIntegrationConfig) GetClientSecret() string {
return config.String("github.integration.client.secret")
}

func (gic *GithubIntegrationConfig) GetRedirectURL() string {
return config.String("github.integration.client.redirecturl")
}

func NewGithubIntegrationConfig(config *koanf.Koanf) *GithubIntegrationConfig {
return &GithubIntegrationConfig{config}
}
8 changes: 4 additions & 4 deletions app/config/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import "go.uber.org/zap"
var Logger *zap.Logger

func InitLogger() {
var err error
Logger, err = zap.NewProduction()
if err != nil {
panic(err)
if AppEnv() == "development" {
Logger, _ = zap.NewDevelopment(zap.IncreaseLevel(zap.DebugLevel))
} else {
Logger, _ = zap.NewProduction()
}
}
88 changes: 88 additions & 0 deletions app/controllers/github_integration_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package controllers

import (
"ai-developer/app/config"
"ai-developer/app/services/integrations"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"net/http"
)

type GithubIntegrationController struct {
githubIntegrationService *integrations.GithubIntegrationService
logger *zap.Logger
}

func (gic *GithubIntegrationController) Authorize(c *gin.Context) {
userId, _ := c.Get("user_id")
gic.logger.Debug(
"Authorizing github integration",
zap.Any("user_id", userId),
)
authCodeUrl := gic.githubIntegrationService.GetRedirectUrl(uint64(userId.(int)))
c.Redirect(http.StatusTemporaryRedirect, authCodeUrl)
}

func (gic *GithubIntegrationController) DeleteIntegration(c *gin.Context) {
userId, _ := c.Get("user_id")
err := gic.githubIntegrationService.DeleteIntegration(uint64(userId.(int)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Integration deleted successfully"})
}

func (gic *GithubIntegrationController) CheckIfIntegrationExists(c *gin.Context) {
userId, _ := c.Get("user_id")
hasIntegration, err := gic.githubIntegrationService.HasGithubIntegration(uint64(userId.(int)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"integrated": hasIntegration})
}

func (gic *GithubIntegrationController) GetRepositories(c *gin.Context) {
userId, _ := c.Get("user_id")
repositories, err := gic.githubIntegrationService.GetRepositories(uint64(userId.(int)))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
response := make([]map[string]interface{}, 0)
for _, repo := range repositories {
response = append(response, map[string]interface{}{
"id": repo.GetID(),
"url": repo.GetCloneURL(),
"name": repo.GetFullName(),
})
}
c.JSON(http.StatusOK, gin.H{"repositories": response})
}

func (gic *GithubIntegrationController) HandleCallback(c *gin.Context) {
code := c.Query("code")
state := c.Query("state")

gic.logger.Debug(
"Handling github integration callback",
zap.String("code", code),
zap.String("state", state),
)

_ = gic.githubIntegrationService.GenerateAndSaveAccessToken(code, state)
redirectUrl := fmt.Sprintf("%s/settings?page=integrations", config.GithubFrontendURL())
c.Redirect(http.StatusTemporaryRedirect, redirectUrl)
}

func NewGithubIntegrationController(
githubIntegrationService *integrations.GithubIntegrationService,
logger *zap.Logger,
) *GithubIntegrationController {
return &GithubIntegrationController{
githubIntegrationService: githubIntegrationService,
logger: logger,
}
}
10 changes: 9 additions & 1 deletion app/controllers/project_controller.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package controllers

import (
"ai-developer/app/models"
"ai-developer/app/services"
"ai-developer/app/types/request"
"net/http"
Expand Down Expand Up @@ -72,7 +73,14 @@ func (controller *ProjectController) CreateProject(context *gin.Context) {
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
return
}
project, err := controller.projectService.CreateProject(int(user.OrganisationID), createProjectRequest)

var project *models.Project
if createProjectRequest.Repository == nil {
project, err = controller.projectService.CreateProject(int(user.OrganisationID), createProjectRequest)
} else {
project, err = controller.projectService.CreateProjectFromGit(user.ID, user.OrganisationID, createProjectRequest)
}

if err != nil {
context.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE integrations;
15 changes: 15 additions & 0 deletions app/db/migrations/20240718105113_create_integrations_table.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE integrations
(
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
deleted_at TIMESTAMP,
user_id BIGINT NOT NULL,
integration_type VARCHAR(255) NOT NULL,
access_token VARCHAR(255) NOT NULL,
refresh_token VARCHAR(255),
metadata JSONB
);

CREATE UNIQUE INDEX IF NOT EXISTS idx_user_integration ON integrations (user_id, integration_type);
CREATE INDEX IF NOT EXISTS idx_user ON integrations (user_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE projects DROP repository;
ALTER TABLE projects DROP repository_url;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE projects ADD repository VARCHAR(1024);
ALTER TABLE projects ADD repository_url VARCHAR(1024);
8 changes: 8 additions & 0 deletions app/models/dtos/integrations/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package integrations

type GithubIntegrationDetails struct {
UserId uint64
GithubUserId string
AccessToken string
RefreshToken *string
}
21 changes: 21 additions & 0 deletions app/models/integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package models

import (
"ai-developer/app/models/types"
"gorm.io/gorm"
)

type Integration struct {
*gorm.Model
ID uint `gorm:"primaryKey, autoIncrement"`

UserId uint64 `gorm:"column:user_id;not null"`
User User `gorm:"foreignKey:UserId;uniqueIndex:idx_user_integration"`

IntegrationType string `gorm:"column:integration_type;type:varchar(255);not null;uniqueIndex:idx_user_integration"`

AccessToken string `gorm:"type:varchar(255);not null"`
RefreshToken *string `gorm:"type:varchar(255);null"`

Metadata *types.JSONMap `gorm:"type:json;null"`
}
2 changes: 2 additions & 0 deletions app/models/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type Project struct {
Name string `gorm:"type:varchar(100);"`
BackendFramework string `gorm:"type:varchar(100);not null"`
FrontendFramework string `gorm:"type:varchar(100);not null"`
Repository *string `gorm:"type:varchar(1024);"`
RepositoryUrl *string `gorm:"type:varchar(1024);"`
Description string `gorm:"type:text"`
OrganisationID uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Expand Down
87 changes: 87 additions & 0 deletions app/repositories/integrations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package repositories

import (
"ai-developer/app/models"
"ai-developer/app/models/types"
"errors"
"go.uber.org/zap"
"gorm.io/gorm"
)

type IntegrationsRepository struct {
db *gorm.DB
logger *zap.Logger
}

func (ir *IntegrationsRepository) FindIntegrationIdByUserIdAndType(userId uint64, integrationType string) (integration *models.Integration, err error) {
err = ir.db.Model(models.Integration{
UserId: userId,
IntegrationType: integrationType,
}).First(&integration).Error
if err != nil {
return nil, err
}
return
}

func (ir *IntegrationsRepository) DeleteIntegration(userId uint64, integrationType string) (err error) {
ir.logger.Info(
"Deleting integration",
zap.Uint64("userId", userId),
zap.String("integrationType", integrationType),
)
err = ir.db.Unscoped().Where(&models.Integration{
UserId: userId,
IntegrationType: integrationType,
}).Delete(&models.Integration{
UserId: userId,
IntegrationType: integrationType,
}).Error
return
}

func (ir *IntegrationsRepository) AddOrUpdateIntegration(
userId uint64,
integrationType string,
accessToken string,
refreshToken *string,
metadata *types.JSONMap,
) (err error) {
integration, err := ir.FindIntegrationIdByUserIdAndType(userId, integrationType)
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}

if integration != nil {
ir.logger.Info(
"Updating integration",
zap.Uint64("userId", integration.UserId),
zap.String("integrationType", integration.IntegrationType),
)
integration.AccessToken = accessToken
integration.RefreshToken = refreshToken
integration.Metadata = metadata
return ir.db.Save(integration).Error
} else {
integration = &models.Integration{
UserId: userId,
IntegrationType: integrationType,
AccessToken: accessToken,
RefreshToken: refreshToken,
Metadata: metadata,
}
ir.logger.Info(
"Adding new integration",
zap.Uint64("userId", userId),
zap.String("integrationType", integrationType),
)
return ir.db.Create(integration).Error
}
}

func NewIntegrationsRepository(db *gorm.DB, logger *zap.Logger) *IntegrationsRepository {
return &IntegrationsRepository{
db: db,
logger: logger.Named("IntegrationsRepository"),
}
}
Loading