mirror of
https://github.com/Dvorinka/Containr.git
synced 2026-06-04 04:22:57 +00:00
overhaul
This commit is contained in:
@@ -0,0 +1,784 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"containr/internal/database"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// GitProvider represents a Git provider (GitHub, GitLab, Bitbucket)
|
||||
type GitProvider struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"` // github, gitlab, bitbucket
|
||||
DisplayName string `json:"display_name" db:"display_name"`
|
||||
APIUrl string `json:"api_url" db:"api_url"`
|
||||
WebhookUrl string `json:"webhook_url" db:"webhook_url"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
AccessToken string `json:"-" db:"access_token"` // Hidden in JSON responses
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// GitRepository represents a connected Git repository
|
||||
type GitRepository struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
ProviderID string `json:"provider_id" db:"provider_id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
FullName string `json:"full_name" db:"full_name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
CloneURL string `json:"clone_url" db:"clone_url"`
|
||||
WebhookURL string `json:"webhook_url" db:"webhook_url"`
|
||||
DefaultBranch string `json:"default_branch" db:"default_branch"`
|
||||
IsPrivate bool `json:"is_private" db:"is_private"`
|
||||
UserID string `json:"user_id" db:"user_id"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// GitWebhook represents a webhook configuration
|
||||
type GitWebhook struct {
|
||||
ID string `json:"id" db:"id"`
|
||||
RepoID string `json:"repo_id" db:"repo_id"`
|
||||
ProviderID string `json:"provider_id" db:"provider_id"`
|
||||
Events string `json:"events" db:"events"` // JSON array of events
|
||||
Secret string `json:"-" db:"webhook_secret"` // Hidden in JSON responses
|
||||
RemoteWebhookID string `json:"remote_webhook_id" db:"remote_webhook_id"`
|
||||
Active bool `json:"active" db:"active"`
|
||||
BranchFilter string `json:"branch_filter,omitempty" db:"branch_filter"`
|
||||
CreatedAt string `json:"created_at" db:"created_at"`
|
||||
UpdatedAt string `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
// CreateGitProviderRequest represents a request to create a Git provider
|
||||
type CreateGitProviderRequest struct {
|
||||
Name string `json:"name" binding:"required,oneof=github gitlab bitbucket"`
|
||||
DisplayName string `json:"display_name" binding:"required"`
|
||||
AccessToken string `json:"access_token" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateGitRepoRequest represents a request to connect a Git repository
|
||||
type CreateGitRepoRequest struct {
|
||||
ProviderID string `json:"provider_id" binding:"required"`
|
||||
RepoFullName string `json:"repo_full_name" binding:"required"`
|
||||
}
|
||||
|
||||
// CreateWebhookRequest represents a request to create a webhook
|
||||
type CreateWebhookRequest struct {
|
||||
RepoID string `json:"repo_id" binding:"required"`
|
||||
Events []string `json:"events" binding:"required"`
|
||||
Branch string `json:"branch"`
|
||||
}
|
||||
|
||||
type GitBranch struct {
|
||||
Name string `json:"name"`
|
||||
CommitHash string `json:"commit_hash"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type githubRepo struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Description string `json:"description"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
Private bool `json:"private"`
|
||||
}
|
||||
|
||||
type githubBranch struct {
|
||||
Name string `json:"name"`
|
||||
Commit struct {
|
||||
SHA string `json:"sha"`
|
||||
} `json:"commit"`
|
||||
}
|
||||
|
||||
type githubUser struct {
|
||||
Login string `json:"login"`
|
||||
}
|
||||
|
||||
func handleGetGitProviders(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT id, name, display_name, api_url, webhook_url, user_id, created_at, updated_at
|
||||
FROM git_providers
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`, userID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var providers []GitProvider
|
||||
for rows.Next() {
|
||||
var provider GitProvider
|
||||
if err := rows.Scan(&provider.ID, &provider.Name, &provider.DisplayName, &provider.APIUrl,
|
||||
&provider.WebhookUrl, &provider.UserID, &provider.CreatedAt, &provider.UpdatedAt); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"providers": providers})
|
||||
}
|
||||
|
||||
func handleCreateGitProvider(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateGitProviderRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the access token by making a test API call
|
||||
if !validateGitToken(req.Name, req.AccessToken) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid access token for " + req.Name})
|
||||
return
|
||||
}
|
||||
|
||||
provider := GitProvider{
|
||||
ID: uuid.New().String(),
|
||||
Name: req.Name,
|
||||
DisplayName: req.DisplayName,
|
||||
AccessToken: req.AccessToken,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
// Set provider-specific URLs
|
||||
switch req.Name {
|
||||
case "github":
|
||||
provider.APIUrl = "https://api.github.com"
|
||||
provider.WebhookUrl = "https://api.github.com"
|
||||
case "gitlab":
|
||||
provider.APIUrl = "https://gitlab.com/api/v4"
|
||||
provider.WebhookUrl = "https://gitlab.com"
|
||||
case "bitbucket":
|
||||
provider.APIUrl = "https://api.bitbucket.org/2.0"
|
||||
provider.WebhookUrl = "https://api.bitbucket.org/2.0"
|
||||
}
|
||||
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO git_providers (id, name, display_name, api_url, webhook_url, access_token, user_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())
|
||||
`, provider.ID, provider.Name, provider.DisplayName, provider.APIUrl,
|
||||
provider.WebhookUrl, provider.AccessToken, provider.UserID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Git provider"})
|
||||
return
|
||||
}
|
||||
|
||||
// Return provider without access token
|
||||
provider.AccessToken = ""
|
||||
c.JSON(http.StatusCreated, provider)
|
||||
}
|
||||
|
||||
func handleGetGitRepositories(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
providerID := c.Param("providerId")
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(providerID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider info
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT id, name, access_token, api_url
|
||||
FROM git_providers
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`, providerID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch repositories from the Git provider
|
||||
repos, err := fetchGitRepositories(provider.Name, provider.AccessToken, provider.APIUrl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repositories"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"repositories": repos})
|
||||
}
|
||||
|
||||
func handleConnectGitRepository(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateGitRepoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUID
|
||||
if _, err := uuid.Parse(req.ProviderID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get provider info
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT id, name, access_token, api_url
|
||||
FROM git_providers
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`, req.ProviderID, userID).Scan(&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Git provider not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch repository details from Git provider
|
||||
repoDetails, err := fetchGitRepositoryDetails(provider.Name, req.RepoFullName, provider.AccessToken, provider.APIUrl)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch repository details"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if repository is already connected
|
||||
var existingID string
|
||||
err = db.QueryRow(`
|
||||
SELECT id FROM git_repositories
|
||||
WHERE provider_id = $1 AND full_name = $2
|
||||
`, req.ProviderID, req.RepoFullName).Scan(&existingID)
|
||||
|
||||
if err == nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Repository already connected", "repository_id": existingID})
|
||||
return
|
||||
}
|
||||
|
||||
// Create repository record
|
||||
repo := GitRepository{
|
||||
ID: uuid.New().String(),
|
||||
ProviderID: req.ProviderID,
|
||||
Name: repoDetails["name"].(string),
|
||||
FullName: req.RepoFullName,
|
||||
Description: repoDetails["description"].(string),
|
||||
CloneURL: repoDetails["clone_url"].(string),
|
||||
DefaultBranch: repoDetails["default_branch"].(string),
|
||||
IsPrivate: repoDetails["private"].(bool),
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO git_repositories (id, provider_id, name, full_name, description, clone_url,
|
||||
default_branch, is_private, user_id, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW(), NOW())
|
||||
`, repo.ID, repo.ProviderID, repo.Name, repo.FullName, repo.Description,
|
||||
repo.CloneURL, repo.DefaultBranch, repo.IsPrivate, repo.UserID)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to connect repository"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, repo)
|
||||
}
|
||||
|
||||
func handleCreateWebhook(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
var req CreateWebhookRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate UUIDs
|
||||
if _, err := uuid.Parse(req.RepoID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid repository ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get repository and provider info
|
||||
var repo GitRepository
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT r.id, r.provider_id, r.full_name, r.user_id,
|
||||
p.id, p.name, p.access_token, p.api_url, p.webhook_url
|
||||
FROM git_repositories r
|
||||
JOIN git_providers p ON r.provider_id = p.id
|
||||
WHERE r.id = $1 AND r.user_id = $2
|
||||
`, req.RepoID, userID).Scan(&repo.ID, &repo.ProviderID, &repo.FullName, &repo.UserID,
|
||||
&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl, &provider.WebhookUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Convert events to JSON
|
||||
eventsJSON, _ := json.Marshal(req.Events)
|
||||
webhookSecret := generateWebhookSecret()
|
||||
|
||||
// Create webhook on Git provider
|
||||
webhookURL := fmt.Sprintf("%s/api/v1/webhooks/git/%s", publicBaseURL(c), req.RepoID)
|
||||
remoteWebhookID, err := createGitWebhook(provider.Name, repo.FullName, provider.AccessToken,
|
||||
provider.APIUrl, webhookURL, req.Events, webhookSecret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook on Git provider: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create webhook record
|
||||
webhook := GitWebhook{
|
||||
ID: uuid.New().String(),
|
||||
RepoID: req.RepoID,
|
||||
ProviderID: provider.ID,
|
||||
Events: string(eventsJSON),
|
||||
Secret: webhookSecret,
|
||||
RemoteWebhookID: remoteWebhookID,
|
||||
Active: true,
|
||||
BranchFilter: req.Branch,
|
||||
}
|
||||
|
||||
_, err = db.Exec(`
|
||||
INSERT INTO git_webhooks (id, repo_id, provider_id, events, webhook_secret, remote_webhook_id, active, branch_filter, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW(), NOW())
|
||||
`, webhook.ID, webhook.RepoID, webhook.ProviderID, webhook.Events, webhook.Secret, webhook.RemoteWebhookID, webhook.Active, nullableString(webhook.BranchFilter))
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create webhook"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"webhook": webhook,
|
||||
"remote_webhook_id": remoteWebhookID,
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetConnectedRepositories(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
|
||||
offset := (page - 1) * limit
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT r.id, r.provider_id, r.name, r.full_name, r.description, r.clone_url,
|
||||
r.default_branch, r.is_private, r.user_id, r.created_at, r.updated_at,
|
||||
p.name as provider_name, p.display_name
|
||||
FROM git_repositories r
|
||||
JOIN git_providers p ON r.provider_id = p.id
|
||||
WHERE r.user_id = $1
|
||||
ORDER BY r.updated_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`, userID, limit, offset)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var repositories []map[string]interface{}
|
||||
for rows.Next() {
|
||||
var repo GitRepository
|
||||
var providerName, providerDisplayName string
|
||||
if err := rows.Scan(&repo.ID, &repo.ProviderID, &repo.Name, &repo.FullName, &repo.Description,
|
||||
&repo.CloneURL, &repo.DefaultBranch, &repo.IsPrivate, &repo.UserID, &repo.CreatedAt, &repo.UpdatedAt,
|
||||
&providerName, &providerDisplayName); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
repositories = append(repositories, map[string]interface{}{
|
||||
"id": repo.ID,
|
||||
"provider_id": repo.ProviderID,
|
||||
"name": repo.Name,
|
||||
"full_name": repo.FullName,
|
||||
"description": repo.Description,
|
||||
"clone_url": repo.CloneURL,
|
||||
"default_branch": repo.DefaultBranch,
|
||||
"is_private": repo.IsPrivate,
|
||||
"created_at": repo.CreatedAt,
|
||||
"updated_at": repo.UpdatedAt,
|
||||
"provider": map[string]string{
|
||||
"name": providerName,
|
||||
"display_name": providerDisplayName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Get total count
|
||||
var total int
|
||||
err = db.QueryRow(`
|
||||
SELECT COUNT(*) FROM git_repositories WHERE user_id = $1
|
||||
`, userID).Scan(&total)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"repositories": repositories,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func handleGetRepositoryBranches(c *gin.Context) {
|
||||
userID := c.MustGet("user_id").(string)
|
||||
db := c.MustGet("db").(*database.DB)
|
||||
repoID := c.Param("repoId")
|
||||
|
||||
if _, err := uuid.Parse(repoID); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid repository ID"})
|
||||
return
|
||||
}
|
||||
|
||||
var repo GitRepository
|
||||
var provider GitProvider
|
||||
err := db.QueryRow(`
|
||||
SELECT r.id, r.full_name, r.default_branch,
|
||||
p.id, p.name, p.access_token, p.api_url
|
||||
FROM git_repositories r
|
||||
JOIN git_providers p ON r.provider_id = p.id
|
||||
WHERE r.id = $1 AND r.user_id = $2
|
||||
`, repoID, userID).Scan(&repo.ID, &repo.FullName, &repo.DefaultBranch,
|
||||
&provider.ID, &provider.Name, &provider.AccessToken, &provider.APIUrl)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "Repository not found"})
|
||||
return
|
||||
}
|
||||
|
||||
branches, err := fetchGitBranches(provider.Name, provider.AccessToken, provider.APIUrl, repo.FullName, repo.DefaultBranch)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch branches: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"branches": branches})
|
||||
}
|
||||
|
||||
// Helper functions (these would need to be implemented with actual Git provider API calls)
|
||||
|
||||
func validateGitToken(provider, token string) bool {
|
||||
if strings.TrimSpace(token) == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if provider != "github" {
|
||||
return true
|
||||
}
|
||||
|
||||
body, status, err := gitProviderRequest("GET", "https://api.github.com", token, "/user", nil)
|
||||
if err != nil || status < 200 || status >= 300 {
|
||||
return false
|
||||
}
|
||||
|
||||
var user githubUser
|
||||
return json.Unmarshal(body, &user) == nil && user.Login != ""
|
||||
}
|
||||
|
||||
func fetchGitRepositories(provider, token, apiUrl string) ([]map[string]interface{}, error) {
|
||||
if provider != "github" {
|
||||
return nil, fmt.Errorf("%s repository listing is not implemented yet", provider)
|
||||
}
|
||||
|
||||
body, status, err := gitProviderRequest(
|
||||
"GET",
|
||||
apiUrl,
|
||||
token,
|
||||
"/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return nil, providerStatusError(status, body)
|
||||
}
|
||||
|
||||
var repos []githubRepo
|
||||
if err := json.Unmarshal(body, &repos); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]map[string]interface{}, 0, len(repos))
|
||||
for _, repo := range repos {
|
||||
result = append(result, map[string]interface{}{
|
||||
"name": repo.Name,
|
||||
"full_name": repo.FullName,
|
||||
"description": repo.Description,
|
||||
"clone_url": repo.CloneURL,
|
||||
"default_branch": repo.DefaultBranch,
|
||||
"private": repo.Private,
|
||||
"html_url": repo.HTMLURL,
|
||||
})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func fetchGitRepositoryDetails(provider, repoFullName, token, apiUrl string) (map[string]interface{}, error) {
|
||||
if provider != "github" {
|
||||
return nil, fmt.Errorf("%s repository details are not implemented yet", provider)
|
||||
}
|
||||
|
||||
path, err := githubRepoPath(repoFullName, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return nil, providerStatusError(status, body)
|
||||
}
|
||||
|
||||
var repo githubRepo
|
||||
if err := json.Unmarshal(body, &repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"name": repo.Name,
|
||||
"description": repo.Description,
|
||||
"clone_url": repo.CloneURL,
|
||||
"default_branch": repo.DefaultBranch,
|
||||
"private": repo.Private,
|
||||
"html_url": repo.HTMLURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func fetchGitBranches(provider, token, apiUrl, repoFullName, defaultBranch string) ([]GitBranch, error) {
|
||||
if provider != "github" {
|
||||
return nil, fmt.Errorf("%s branch listing is not implemented yet", provider)
|
||||
}
|
||||
|
||||
path, err := githubRepoPath(repoFullName, "/branches?per_page=100")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return nil, providerStatusError(status, body)
|
||||
}
|
||||
|
||||
var rawBranches []githubBranch
|
||||
if err := json.Unmarshal(body, &rawBranches); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
branches := make([]GitBranch, 0, len(rawBranches))
|
||||
for _, branch := range rawBranches {
|
||||
branches = append(branches, GitBranch{
|
||||
Name: branch.Name,
|
||||
CommitHash: branch.Commit.SHA,
|
||||
IsDefault: branch.Name == defaultBranch,
|
||||
})
|
||||
}
|
||||
|
||||
return branches, nil
|
||||
}
|
||||
|
||||
func createGitWebhook(provider, repoFullName, token, apiUrl, targetURL string, events []string, secret string) (string, error) {
|
||||
if provider != "github" {
|
||||
return "", fmt.Errorf("%s webhooks are not implemented yet", provider)
|
||||
}
|
||||
|
||||
path, err := githubRepoPath(repoFullName, "/hooks")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"name": "web",
|
||||
"active": true,
|
||||
"events": events,
|
||||
"config": map[string]string{
|
||||
"url": targetURL,
|
||||
"content_type": "json",
|
||||
"secret": secret,
|
||||
"insecure_ssl": "0",
|
||||
},
|
||||
}
|
||||
data, _ := json.Marshal(payload)
|
||||
|
||||
body, status, err := gitProviderRequest("POST", apiUrl, token, path, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return "", providerStatusError(status, body)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if response.ID == 0 {
|
||||
return "", fmt.Errorf("GitHub returned an empty webhook ID")
|
||||
}
|
||||
|
||||
return strconv.FormatInt(response.ID, 10), nil
|
||||
}
|
||||
|
||||
func generateWebhookSecret() string {
|
||||
return "webhook-secret-" + uuid.New().String()
|
||||
}
|
||||
|
||||
func gitProviderRequest(method, apiUrl, token, path string, body io.Reader) ([]byte, int, error) {
|
||||
base := strings.TrimRight(apiUrl, "/")
|
||||
if base == "" {
|
||||
base = "https://api.github.com"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, base+path, body)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
if strings.TrimSpace(token) != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, resp.StatusCode, err
|
||||
}
|
||||
|
||||
return data, resp.StatusCode, nil
|
||||
}
|
||||
|
||||
func githubRepoPath(repoFullName, suffix string) (string, error) {
|
||||
parts := strings.Split(repoFullName, "/")
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
return "", fmt.Errorf("repository must use owner/name format")
|
||||
}
|
||||
|
||||
return "/repos/" + url.PathEscape(parts[0]) + "/" + url.PathEscape(parts[1]) + suffix, nil
|
||||
}
|
||||
|
||||
func providerStatusError(status int, body []byte) error {
|
||||
var response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &response); err == nil && response.Message != "" {
|
||||
return fmt.Errorf("provider returned %d: %s", status, response.Message)
|
||||
}
|
||||
|
||||
return fmt.Errorf("provider returned %d", status)
|
||||
}
|
||||
|
||||
func publicBaseURL(c *gin.Context) string {
|
||||
for _, key := range []string{"CONTAINR_PUBLIC_URL", "PUBLIC_URL", "APP_URL"} {
|
||||
if value := strings.TrimRight(os.Getenv(key), "/"); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
host := c.Request.Host
|
||||
if forwardedHost := c.GetHeader("X-Forwarded-Host"); forwardedHost != "" {
|
||||
host = forwardedHost
|
||||
}
|
||||
|
||||
return scheme + "://" + host
|
||||
}
|
||||
|
||||
func nullableString(value string) any {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func fetchGitHubFile(provider, token, apiUrl, repoFullName, branch, filePath string) ([]byte, error) {
|
||||
if provider != "github" {
|
||||
return nil, fmt.Errorf("%s file fetch is not implemented yet", provider)
|
||||
}
|
||||
|
||||
suffix := "/contents/" + strings.TrimLeft(filePath, "/")
|
||||
if branch != "" {
|
||||
suffix += "?ref=" + url.QueryEscape(branch)
|
||||
}
|
||||
|
||||
path, err := githubRepoPath(repoFullName, suffix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, status, err := gitProviderRequest("GET", apiUrl, token, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if status < 200 || status >= 300 {
|
||||
return nil, providerStatusError(status, body)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Content string `json:"content"`
|
||||
Encoding string `json:"encoding"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if response.Encoding != "base64" {
|
||||
return nil, fmt.Errorf("unsupported GitHub content encoding: %s", response.Encoding)
|
||||
}
|
||||
|
||||
decoded, err := base64.StdEncoding.DecodeString(strings.ReplaceAll(response.Content, "\n", ""))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return decoded, nil
|
||||
}
|
||||
Reference in New Issue
Block a user