mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-03 20:12:58 +00:00
uppdate
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
ShutdownTimeout time.Duration
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
App AppConfig
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Version string
|
||||
DemoMode bool
|
||||
GinMode string
|
||||
JWTSecret string
|
||||
CorsOrigins string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: getEnvWithDefault("PORT", "8080"),
|
||||
ReadTimeout: getDurationEnv("READ_TIMEOUT", 15*time.Second),
|
||||
WriteTimeout: getDurationEnv("WRITE_TIMEOUT", 15*time.Second),
|
||||
IdleTimeout: getDurationEnv("IDLE_TIMEOUT", 60*time.Second),
|
||||
ShutdownTimeout: getDurationEnv("SHUTDOWN_TIMEOUT", 30*time.Second),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Host: getEnvWithDefault("DB_HOST", "localhost"),
|
||||
Port: getEnvWithDefault("DB_PORT", "5432"),
|
||||
User: getEnvWithDefault("DB_USER", "trackeep"),
|
||||
Password: os.Getenv("DB_PASSWORD"),
|
||||
Name: getEnvWithDefault("DB_NAME", "trackeep"),
|
||||
SSLMode: getEnvWithDefault("DB_SSL_MODE", "disable"),
|
||||
},
|
||||
App: AppConfig{
|
||||
Version: getEnvWithDefault("APP_VERSION", "1.0.0"),
|
||||
DemoMode: os.Getenv("VITE_DEMO_MODE") == "true",
|
||||
GinMode: getEnvWithDefault("GIN_MODE", "debug"),
|
||||
JWTSecret: os.Getenv("JWT_SECRET"),
|
||||
CorsOrigins: getEnvWithDefault("CORS_ALLOWED_ORIGINS", ""),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
if c.Database.Password == "" && !c.App.DemoMode {
|
||||
return fmt.Errorf("DB_PASSWORD environment variable is required")
|
||||
}
|
||||
|
||||
if c.App.GinMode == "release" && c.App.CorsOrigins == "" {
|
||||
return fmt.Errorf("CORS_ALLOWED_ORIGINS must be set in production mode")
|
||||
}
|
||||
|
||||
if c.App.GinMode == "release" && c.App.JWTSecret == "" {
|
||||
return fmt.Errorf("JWT_SECRET must be set in production mode")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) DSN() string {
|
||||
return fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s",
|
||||
c.Database.Host,
|
||||
c.Database.User,
|
||||
c.Database.Password,
|
||||
c.Database.Name,
|
||||
c.Database.Port,
|
||||
c.Database.SSLMode,
|
||||
)
|
||||
}
|
||||
|
||||
func getEnvWithDefault(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getDurationEnv(key string, defaultValue time.Duration) time.Duration {
|
||||
value := os.Getenv(key)
|
||||
if value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
seconds, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
duration, err := time.ParseDuration(value)
|
||||
if err != nil {
|
||||
return defaultValue
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func GetAPIConfig(c *gin.Context) {
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
host := c.Request.Host
|
||||
if host == "" {
|
||||
host = os.Getenv("HOST")
|
||||
if host == "" {
|
||||
host = "localhost:8080"
|
||||
}
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s://%s/api/v1", scheme, host)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"api_url": apiURL,
|
||||
"demo_mode": os.Getenv("VITE_DEMO_MODE") == "true",
|
||||
"version": "1.0.0",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/trackeep/backend/config"
|
||||
)
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func HealthCheck(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
dbStatus := "connected"
|
||||
var dbPingTime time.Duration = 0
|
||||
|
||||
if db == nil {
|
||||
dbStatus = "disconnected"
|
||||
} else {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
dbStatus = "error"
|
||||
} else {
|
||||
start := time.Now()
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
dbStatus = "error"
|
||||
} else {
|
||||
dbPingTime = time.Since(start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sessionStatus := "ok"
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
uptime := time.Since(startTime)
|
||||
|
||||
health := gin.H{
|
||||
"status": "ok",
|
||||
"message": "Trackeep API is running",
|
||||
"version": "1.0.0",
|
||||
"uptime": uptime.String(),
|
||||
"database": gin.H{
|
||||
"status": dbStatus,
|
||||
"ping_time": dbPingTime.String(),
|
||||
},
|
||||
"sessions": gin.H{
|
||||
"status": sessionStatus,
|
||||
},
|
||||
"system": gin.H{
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"memory": gin.H{
|
||||
"alloc_mb": memStats.Alloc / 1024 / 1024,
|
||||
"total_alloc_mb": memStats.TotalAlloc / 1024 / 1024,
|
||||
"sys_mb": memStats.Sys / 1024 / 1024,
|
||||
},
|
||||
},
|
||||
"timestamp": gin.H{
|
||||
"human": time.Now().Format(time.RFC3339),
|
||||
"unix": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
overallStatus := "healthy"
|
||||
if dbStatus != "connected" {
|
||||
overallStatus = "degraded"
|
||||
health["status"] = "degraded"
|
||||
}
|
||||
if sessionStatus != "ok" {
|
||||
overallStatus = "degraded"
|
||||
health["status"] = "degraded"
|
||||
}
|
||||
|
||||
statusCode := 200
|
||||
if overallStatus == "degraded" {
|
||||
statusCode = 503
|
||||
}
|
||||
|
||||
c.JSON(statusCode, health)
|
||||
}
|
||||
|
||||
func ReadinessCheck(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
if db == nil {
|
||||
c.JSON(503, gin.H{"status": "not_ready", "reason": "database_not_connected"})
|
||||
return
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
c.JSON(503, gin.H{"status": "not_ready", "reason": "database_ping_failed"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"status": "ready"})
|
||||
}
|
||||
|
||||
func LivenessCheck(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "alive", "timestamp": time.Now().Unix()})
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -25,10 +27,8 @@ type BraveSearchResponse struct {
|
||||
}
|
||||
|
||||
type BraveNewsResponse struct {
|
||||
News struct {
|
||||
Results []map[string]interface{} `json:"results"`
|
||||
} `json:"news"`
|
||||
Query struct {
|
||||
Results []map[string]interface{} `json:"results"`
|
||||
Query struct {
|
||||
Original string `json:"original"`
|
||||
Display string `json:"display"`
|
||||
} `json:"query"`
|
||||
@@ -47,6 +47,7 @@ type BraveSearchResult struct {
|
||||
|
||||
// SearchWeb handles POST /api/v1/search/web
|
||||
func SearchWeb(c *gin.Context) {
|
||||
fmt.Printf("DEBUG: SearchWeb function called\n")
|
||||
var req struct {
|
||||
Query string `json:"query" binding:"required"`
|
||||
Count int `json:"count"`
|
||||
@@ -135,6 +136,7 @@ func SearchWeb(c *gin.Context) {
|
||||
}
|
||||
|
||||
func SearchNews(c *gin.Context) {
|
||||
fmt.Printf("DEBUG: SearchNews function called\n")
|
||||
var req struct {
|
||||
Query string `json:"query" binding:"required"`
|
||||
Count int `json:"count"`
|
||||
@@ -174,20 +176,35 @@ func SearchNews(c *gin.Context) {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "Failed to contact Brave News API"})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Brave News API error: %d", resp.StatusCode)})
|
||||
return
|
||||
}
|
||||
|
||||
// Read the response body for debugging
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read response body"})
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("DEBUG: Raw Brave News API response: %s\n", string(bodyBytes))
|
||||
|
||||
var braveResp BraveNewsResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&braveResp); err != nil {
|
||||
if err := json.NewDecoder(bytes.NewReader(bodyBytes)).Decode(&braveResp); err != nil {
|
||||
fmt.Printf("DEBUG: JSON decode error: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode Brave news response"})
|
||||
return
|
||||
}
|
||||
|
||||
resultsRaw := braveResp.News.Results
|
||||
// Debug logging
|
||||
fmt.Printf("DEBUG: Parsed BraveNewsResponse: %+v\n", braveResp)
|
||||
fmt.Printf("DEBUG: Number of results: %d\n", len(braveResp.Results))
|
||||
|
||||
resultsRaw := braveResp.Results
|
||||
results := make([]BraveSearchResult, 0, len(resultsRaw))
|
||||
for _, r := range resultsRaw {
|
||||
title, _ := r["title"].(string)
|
||||
|
||||
+596
-46
@@ -1,6 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -9,10 +13,13 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// UpdateInfo represents information about an available update
|
||||
@@ -22,6 +29,9 @@ type UpdateInfo struct {
|
||||
DownloadURL string `json:"downloadUrl"`
|
||||
Mandatory bool `json:"mandatory"`
|
||||
Size string `json:"size"`
|
||||
Checksum string `json:"checksum"`
|
||||
PublishedAt string `json:"publishedAt"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
}
|
||||
|
||||
// UpdateStatus represents the current status of an update
|
||||
@@ -44,6 +54,7 @@ var (
|
||||
updateMutex sync.RWMutex
|
||||
currentUpdate *UpdateInfo
|
||||
updateProgress *UpdateStatus
|
||||
backupPath string // Store backup path for rollback
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -68,18 +79,32 @@ func CheckForUpdates(c *gin.Context) {
|
||||
currentVersion = "1.0.0"
|
||||
}
|
||||
|
||||
// In a real implementation, this would check against a remote update server
|
||||
// For demo purposes, we'll simulate checking for updates
|
||||
latestVersion, updateAvailable := simulateUpdateCheck(currentVersion)
|
||||
// Get GitHub token from OAuth service (required)
|
||||
githubToken := getGitHubTokenFromContext(c)
|
||||
if githubToken == "" {
|
||||
log.Printf("No GitHub token from OAuth service - update check failed")
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "OAuth service not available",
|
||||
"message": "Please ensure OAuth service is running and you are authenticated",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Using GitHub token from OAuth service for update check")
|
||||
|
||||
// Check for updates using GitHub API
|
||||
updateInfo, updateAvailable, err := checkForUpdatesWithGitHub(currentVersion, githubToken)
|
||||
if err != nil {
|
||||
log.Printf("Failed to check for updates: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to check for updates",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if updateAvailable {
|
||||
currentUpdate = &UpdateInfo{
|
||||
Version: latestVersion,
|
||||
ReleaseNotes: "• New AI features added\n• Performance improvements\n• Bug fixes and security patches\n• Enhanced user interface",
|
||||
DownloadURL: "https://github.com/trackeep/trackeep/releases/latest",
|
||||
Mandatory: false,
|
||||
Size: "~25MB",
|
||||
}
|
||||
currentUpdate = updateInfo
|
||||
updateProgress.Available = true
|
||||
} else {
|
||||
currentUpdate = nil
|
||||
@@ -89,7 +114,7 @@ func CheckForUpdates(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"updateAvailable": updateAvailable,
|
||||
"currentVersion": currentVersion,
|
||||
"latestVersion": latestVersion,
|
||||
"latestVersion": updateInfo.Version,
|
||||
"updateInfo": currentUpdate,
|
||||
})
|
||||
}
|
||||
@@ -140,17 +165,275 @@ func UpdateProgressWebSocket(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// simulateUpdateCheck simulates checking for updates
|
||||
func simulateUpdateCheck(currentVersion string) (string, bool) {
|
||||
// Simulate version check - in reality this would call an update API
|
||||
versions := []string{"1.0.1", "1.1.0", "1.2.0"}
|
||||
// checkForUpdatesWithGitHub checks for updates using GitHub API
|
||||
func checkForUpdatesWithGitHub(currentVersion, githubToken string) (*UpdateInfo, bool, error) {
|
||||
// GitHub repository information
|
||||
owner := "Dvorinka"
|
||||
repo := "Trackeep"
|
||||
|
||||
// For demo, always return a newer version
|
||||
if len(versions) > 0 {
|
||||
return versions[0], true
|
||||
// Log which token source we're using
|
||||
if githubToken != "" {
|
||||
log.Printf("Using GitHub token from OAuth service")
|
||||
} else {
|
||||
log.Printf("No GitHub token available - OAuth service should be running")
|
||||
return nil, false, fmt.Errorf("OAuth service not available - please ensure OAuth service is running")
|
||||
}
|
||||
|
||||
return currentVersion, false
|
||||
// Create HTTP request to GitHub API
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", owner, repo)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Add authorization header if token is available
|
||||
if githubToken != "" {
|
||||
req.Header.Set("Authorization", "token "+githubToken)
|
||||
}
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
|
||||
// Make the request
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to fetch releases: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, false, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Parse the release response
|
||||
var release struct {
|
||||
TagName string `json:"tag_name"`
|
||||
Name string `json:"name"`
|
||||
Body string `json:"body"`
|
||||
PublishedAt string `json:"published_at"`
|
||||
Prerelease bool `json:"prerelease"`
|
||||
Assets []struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
} `json:"assets"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, false, fmt.Errorf("failed to parse release response: %w", err)
|
||||
}
|
||||
|
||||
// Compare versions (simple semantic version comparison)
|
||||
if !isNewerVersion(release.TagName, currentVersion) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Find the appropriate asset for the current platform
|
||||
var downloadURL, size, checksum string
|
||||
for _, asset := range release.Assets {
|
||||
// Look for platform-specific binaries
|
||||
if isPlatformAsset(asset.Name) {
|
||||
downloadURL = asset.BrowserDownloadURL
|
||||
size = formatBytes(asset.Size)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If no platform-specific asset found, use the first one
|
||||
if downloadURL == "" && len(release.Assets) > 0 {
|
||||
downloadURL = release.Assets[0].BrowserDownloadURL
|
||||
size = formatBytes(release.Assets[0].Size)
|
||||
}
|
||||
|
||||
// Try to get checksum from release notes or assets
|
||||
checksum = extractChecksum(release.Body)
|
||||
|
||||
updateInfo := &UpdateInfo{
|
||||
Version: release.TagName,
|
||||
ReleaseNotes: release.Body,
|
||||
DownloadURL: downloadURL,
|
||||
Mandatory: false, // Could be determined from release notes or tags
|
||||
Size: size,
|
||||
Checksum: checksum,
|
||||
PublishedAt: release.PublishedAt,
|
||||
Prerelease: release.Prerelease,
|
||||
}
|
||||
|
||||
return updateInfo, true, nil
|
||||
}
|
||||
|
||||
// getGitHubTokenFromContext extracts GitHub token from request context
|
||||
func getGitHubTokenFromContext(c *gin.Context) string {
|
||||
// Extract Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Remove "Bearer " prefix
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
if tokenString == authHeader {
|
||||
// No Bearer prefix found
|
||||
return ""
|
||||
}
|
||||
|
||||
// Parse JWT token
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(os.Getenv("JWT_SECRET")), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Extract claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get GitHub access token from claims
|
||||
githubToken, ok := claims["access_token"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if token is still valid
|
||||
expiresAt, ok := claims["expires_at"]
|
||||
if ok {
|
||||
if expTime, ok := expiresAt.(float64); ok {
|
||||
if time.Now().Unix() > int64(expTime) {
|
||||
return "" // Token expired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return githubToken.(string)
|
||||
}
|
||||
|
||||
// Helper functions for GitHub update functionality
|
||||
|
||||
// getGitHubTokenFromOAuth attempts to get GitHub token from OAuth service
|
||||
func getGitHubTokenFromOAuth() string {
|
||||
// Try to get token from current user session
|
||||
// This would typically be extracted from the JWT token in the request context
|
||||
// For now, we'll implement a basic version that checks for a logged-in user
|
||||
|
||||
// In a real implementation, this would:
|
||||
// 1. Extract the JWT from the current request context
|
||||
// 2. Parse the JWT to get the GitHub access token
|
||||
// 3. Return the token if valid
|
||||
|
||||
// For now, return empty string to indicate no OAuth token available
|
||||
// This will be implemented when we have proper session management
|
||||
return ""
|
||||
}
|
||||
|
||||
// isNewerVersion compares semantic versions
|
||||
func isNewerVersion(latest, current string) bool {
|
||||
// Remove 'v' prefix if present
|
||||
latest = strings.TrimPrefix(latest, "v")
|
||||
current = strings.TrimPrefix(current, "v")
|
||||
|
||||
latestParts := strings.Split(latest, ".")
|
||||
currentParts := strings.Split(current, ".")
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
var latestNum, currentNum int
|
||||
var err error
|
||||
|
||||
if i < len(latestParts) {
|
||||
latestNum, err = strconv.Atoi(latestParts[i])
|
||||
if err != nil {
|
||||
latestNum = 0
|
||||
}
|
||||
}
|
||||
|
||||
if i < len(currentParts) {
|
||||
currentNum, err = strconv.Atoi(currentParts[i])
|
||||
if err != nil {
|
||||
currentNum = 0
|
||||
}
|
||||
}
|
||||
|
||||
if latestNum > currentNum {
|
||||
return true
|
||||
}
|
||||
if latestNum < currentNum {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isPlatformAsset checks if an asset is appropriate for the current platform
|
||||
func isPlatformAsset(filename string) bool {
|
||||
arch := runtime.GOARCH
|
||||
os := runtime.GOOS
|
||||
|
||||
filename = strings.ToLower(filename)
|
||||
|
||||
// Check for platform-specific patterns
|
||||
switch os {
|
||||
case "windows":
|
||||
return strings.Contains(filename, "windows") || strings.Contains(filename, "win") || strings.HasSuffix(filename, ".exe")
|
||||
case "linux":
|
||||
return strings.Contains(filename, "linux") || strings.Contains(filename, "ubuntu") || strings.Contains(filename, "debian")
|
||||
case "darwin":
|
||||
return strings.Contains(filename, "darwin") || strings.Contains(filename, "macos") || strings.Contains(filename, "mac")
|
||||
}
|
||||
|
||||
// Check architecture
|
||||
if arch == "amd64" {
|
||||
return strings.Contains(filename, "amd64") || strings.Contains(filename, "x86_64")
|
||||
}
|
||||
if arch == "arm64" {
|
||||
return strings.Contains(filename, "arm64") || strings.Contains(filename, "aarch64")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// formatBytes formats bytes into human readable format
|
||||
func formatBytes(bytes int64) string {
|
||||
const unit = 1024
|
||||
if bytes < unit {
|
||||
return fmt.Sprintf("%d B", bytes)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := bytes / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
||||
// extractChecksum attempts to extract SHA256 checksum from release notes
|
||||
func extractChecksum(body string) string {
|
||||
lines := strings.Split(body, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "SHA256:") || strings.HasPrefix(line, "Checksum:") {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
// Also look for pattern like "checksum: sha256:..."
|
||||
if strings.Contains(line, "sha256:") {
|
||||
idx := strings.Index(line, "sha256:")
|
||||
if idx != -1 {
|
||||
checksum := strings.TrimSpace(line[idx+7:])
|
||||
if len(checksum) == 64 { // SHA256 length
|
||||
return checksum
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// performUpdate performs the actual update process
|
||||
@@ -161,18 +444,31 @@ func performUpdate(updateInfo *UpdateInfo) {
|
||||
updateProgress.Error = ""
|
||||
updateMutex.Unlock()
|
||||
|
||||
// Broadcast progress update
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
|
||||
// Simulate download
|
||||
for i := 0; i <= 100; i += 10 {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
log.Printf("Starting update to version %s", updateInfo.Version)
|
||||
|
||||
// Download the update
|
||||
tempFile, err := downloadUpdate(updateInfo)
|
||||
if err != nil {
|
||||
updateMutex.Lock()
|
||||
updateProgress.Progress = float64(i)
|
||||
updateProgress.Downloading = false
|
||||
updateProgress.Error = fmt.Sprintf("Failed to download update: %v", err)
|
||||
updateMutex.Unlock()
|
||||
log.Printf("Update download failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.Remove(tempFile)
|
||||
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
// Verify checksum if available
|
||||
if updateInfo.Checksum != "" {
|
||||
if err := verifyChecksum(tempFile, updateInfo.Checksum); err != nil {
|
||||
updateMutex.Lock()
|
||||
updateProgress.Downloading = false
|
||||
updateProgress.Error = fmt.Sprintf("Checksum verification failed: %v", err)
|
||||
updateMutex.Unlock()
|
||||
log.Printf("Checksum verification failed: %v", err)
|
||||
return
|
||||
}
|
||||
log.Printf("Checksum verification passed")
|
||||
}
|
||||
|
||||
// Start installation
|
||||
@@ -182,36 +478,30 @@ func performUpdate(updateInfo *UpdateInfo) {
|
||||
updateProgress.Progress = 0
|
||||
updateMutex.Unlock()
|
||||
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
|
||||
// Backup user data
|
||||
if err := backupUserData(); err != nil {
|
||||
updateMutex.Lock()
|
||||
updateProgress.Installing = false
|
||||
updateProgress.Error = fmt.Sprintf("Failed to backup user data: %v", err)
|
||||
updateMutex.Unlock()
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
log.Printf("Backup failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Simulate installation
|
||||
for i := 0; i <= 100; i += 20 {
|
||||
time.Sleep(1 * time.Second)
|
||||
// Extract and install the update
|
||||
if err := extractAndInstall(tempFile, updateInfo); err != nil {
|
||||
// Attempt rollback on failure
|
||||
log.Printf("Installation failed, attempting rollback: %v", err)
|
||||
if rollbackErr := rollbackUpdate(); rollbackErr != nil {
|
||||
log.Printf("Rollback also failed: %v", rollbackErr)
|
||||
} else {
|
||||
log.Printf("Rollback completed successfully")
|
||||
}
|
||||
|
||||
updateMutex.Lock()
|
||||
updateProgress.Progress = float64(i)
|
||||
updateMutex.Unlock()
|
||||
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
}
|
||||
|
||||
// Perform the actual update
|
||||
if err := applyUpdate(updateInfo); err != nil {
|
||||
updateMutex.Lock()
|
||||
updateProgress.Installing = false
|
||||
updateProgress.Error = fmt.Sprintf("Failed to apply update: %v", err)
|
||||
updateProgress.Error = fmt.Sprintf("Failed to install update: %v", err)
|
||||
updateMutex.Unlock()
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -222,22 +512,215 @@ func performUpdate(updateInfo *UpdateInfo) {
|
||||
updateProgress.Progress = 100
|
||||
updateMutex.Unlock()
|
||||
|
||||
log.Printf("Update progress: %.1f%% downloading", updateProgress.Progress)
|
||||
log.Printf("Update to version %s completed successfully", updateInfo.Version)
|
||||
|
||||
// Trigger application restart after a delay
|
||||
time.Sleep(2 * time.Second)
|
||||
restartApplication()
|
||||
}
|
||||
|
||||
// downloadUpdate downloads the update file with progress tracking
|
||||
func downloadUpdate(updateInfo *UpdateInfo) (string, error) {
|
||||
if updateInfo.DownloadURL == "" {
|
||||
return "", fmt.Errorf("no download URL available")
|
||||
}
|
||||
|
||||
// Create temporary file
|
||||
tempFile, err := os.CreateTemp("", "trackeep-update-*.zip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer tempFile.Close()
|
||||
|
||||
// Make HTTP request
|
||||
req, err := http.NewRequest("GET", updateInfo.DownloadURL, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Get content length for progress tracking
|
||||
contentLength := resp.ContentLength
|
||||
var downloaded int64
|
||||
|
||||
// Create progress reporter
|
||||
progress := make(chan int64)
|
||||
go func() {
|
||||
for {
|
||||
bytes, ok := <-progress
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
downloaded += bytes
|
||||
if contentLength > 0 {
|
||||
percent := float64(downloaded) / float64(contentLength) * 100
|
||||
updateMutex.Lock()
|
||||
updateProgress.Progress = percent
|
||||
updateMutex.Unlock()
|
||||
log.Printf("Download progress: %.1f%%", percent)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Download with progress tracking
|
||||
writer := &progressWriter{writer: tempFile, progress: progress}
|
||||
_, err = io.Copy(writer, resp.Body)
|
||||
close(progress)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to save download: %w", err)
|
||||
}
|
||||
|
||||
return tempFile.Name(), nil
|
||||
}
|
||||
|
||||
// progressWriter tracks download progress
|
||||
type progressWriter struct {
|
||||
writer io.Writer
|
||||
progress chan<- int64
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.writer.Write(p)
|
||||
if err == nil && n > 0 {
|
||||
pw.progress <- int64(n)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// verifyChecksum verifies the SHA256 checksum of a file
|
||||
func verifyChecksum(filePath, expectedChecksum string) error {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hasher := sha256.New()
|
||||
if _, err := io.Copy(hasher, file); err != nil {
|
||||
return fmt.Errorf("failed to calculate checksum: %w", err)
|
||||
}
|
||||
|
||||
actualChecksum := hex.EncodeToString(hasher.Sum(nil))
|
||||
if !strings.EqualFold(actualChecksum, expectedChecksum) {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractAndInstall extracts the update and installs it
|
||||
func extractAndInstall(filePath string, updateInfo *UpdateInfo) error {
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Get directory of executable
|
||||
installDir := filepath.Dir(executable)
|
||||
|
||||
// Open the zip file
|
||||
reader, err := zip.OpenReader(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open zip file: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Extract files
|
||||
totalFiles := len(reader.File)
|
||||
extractedFiles := 0
|
||||
|
||||
for _, file := range reader.File {
|
||||
// Update progress
|
||||
progress := float64(extractedFiles) / float64(totalFiles) * 100
|
||||
updateMutex.Lock()
|
||||
updateProgress.Progress = progress
|
||||
updateMutex.Unlock()
|
||||
|
||||
// Skip directories for now
|
||||
if file.FileInfo().IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create file path
|
||||
filePath := filepath.Join(installDir, file.Name)
|
||||
|
||||
// Ensure directory exists
|
||||
if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
// Extract file
|
||||
if err := extractFile(file, filePath); err != nil {
|
||||
return fmt.Errorf("failed to extract %s: %w", file.Name, err)
|
||||
}
|
||||
|
||||
extractedFiles++
|
||||
log.Printf("Extracted: %s", file.Name)
|
||||
}
|
||||
|
||||
// Update version in environment
|
||||
os.Setenv("APP_VERSION", updateInfo.Version)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFile extracts a single file from zip
|
||||
func extractFile(file *zip.File, destination string) error {
|
||||
// Open file in zip
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// Create destination file
|
||||
destFile, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy file contents
|
||||
_, err = io.Copy(destFile, rc)
|
||||
return err
|
||||
}
|
||||
|
||||
// backupUserData creates a backup of user data
|
||||
func backupUserData() error {
|
||||
backupDir := filepath.Join(os.TempDir(), "trackeep_backup", time.Now().Format("20060102_150405"))
|
||||
|
||||
// Store backup path globally for potential rollback
|
||||
backupPath = backupDir
|
||||
|
||||
// Create backup directory
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create backup directory: %w", err)
|
||||
}
|
||||
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Backup current executable
|
||||
backupExecPath := filepath.Join(backupDir, filepath.Base(executable))
|
||||
if err := copyFile(executable, backupExecPath); err != nil {
|
||||
return fmt.Errorf("failed to backup executable: %w", err)
|
||||
}
|
||||
|
||||
// Backup database
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
@@ -354,7 +837,74 @@ func getUpdateStatusString(status UpdateStatus) string {
|
||||
return "Not Available"
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
// rollbackUpdate restores the application from backup
|
||||
func rollbackUpdate() error {
|
||||
if backupPath == "" {
|
||||
return fmt.Errorf("no backup path available for rollback")
|
||||
}
|
||||
|
||||
log.Printf("Starting rollback from backup: %s", backupPath)
|
||||
|
||||
// Get current executable path
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
// Restore executable
|
||||
backupExecPath := filepath.Join(backupPath, filepath.Base(executable))
|
||||
if _, err := os.Stat(backupExecPath); err == nil {
|
||||
if err := copyFile(backupExecPath, executable); err != nil {
|
||||
return fmt.Errorf("failed to restore executable: %w", err)
|
||||
}
|
||||
log.Printf("Restored executable from backup")
|
||||
}
|
||||
|
||||
// Restore database
|
||||
backupDBPath := filepath.Join(backupPath, "trackeep.db")
|
||||
dbPath := os.Getenv("DB_PATH")
|
||||
if dbPath == "" {
|
||||
dbPath = "./trackeep.db"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(backupDBPath); err == nil {
|
||||
if err := copyFile(backupDBPath, dbPath); err != nil {
|
||||
return fmt.Errorf("failed to restore database: %w", err)
|
||||
}
|
||||
log.Printf("Restored database from backup")
|
||||
}
|
||||
|
||||
// Restore uploads directory
|
||||
backupUploadsDir := filepath.Join(backupPath, "uploads")
|
||||
uploadsDir := "./uploads"
|
||||
|
||||
if _, err := os.Stat(backupUploadsDir); err == nil {
|
||||
// Remove current uploads directory and restore from backup
|
||||
if err := os.RemoveAll(uploadsDir); err != nil {
|
||||
log.Printf("Warning: failed to remove uploads directory: %v", err)
|
||||
}
|
||||
if err := copyDirectory(backupUploadsDir, uploadsDir); err != nil {
|
||||
return fmt.Errorf("failed to restore uploads: %w", err)
|
||||
}
|
||||
log.Printf("Restored uploads from backup")
|
||||
}
|
||||
|
||||
// Restore configuration files
|
||||
configFiles := []string{".env", "docker-compose.yml"}
|
||||
for _, file := range configFiles {
|
||||
backupFile := filepath.Join(backupPath, file)
|
||||
if _, err := os.Stat(backupFile); err == nil {
|
||||
if err := copyFile(backupFile, file); err != nil {
|
||||
log.Printf("Warning: failed to restore %s: %v", file, err)
|
||||
} else {
|
||||
log.Printf("Restored %s from backup", file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Rollback completed successfully")
|
||||
return nil
|
||||
}
|
||||
func copyFile(src, dst string) error {
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
|
||||
@@ -171,17 +171,13 @@ func GetYouTubeTrending(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GetPredefinedChannelVideos handles GET /api/v1/youtube/predefined-channels
|
||||
func GetPredefinedChannelVideos(c *gin.Context) {
|
||||
// Get query parameters
|
||||
maxResults, _ := strconv.Atoi(c.DefaultQuery("max_results", "5"))
|
||||
|
||||
// Validate max results
|
||||
if maxResults < 1 || maxResults > 20 {
|
||||
maxResults = 10
|
||||
}
|
||||
|
||||
// Get videos from predefined channels
|
||||
response, err := services.GetPredefinedChannelVideos(maxResults)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
@@ -193,3 +189,32 @@ func GetPredefinedChannelVideos(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
func YouTubeSearchTest(c *gin.Context) {
|
||||
var req struct {
|
||||
Query string `json:"query" binding:"required"`
|
||||
MaxResults int `json:"max_results"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.MaxResults <= 0 {
|
||||
req.MaxResults = 5
|
||||
}
|
||||
|
||||
response, err := services.SearchYouTubeVideos(req.Query, req.MaxResults, "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to search YouTube videos",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": response,
|
||||
})
|
||||
}
|
||||
|
||||
+20
-248
@@ -2,15 +2,11 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
@@ -18,29 +14,24 @@ import (
|
||||
"github.com/trackeep/backend/handlers"
|
||||
"github.com/trackeep/backend/middleware"
|
||||
"github.com/trackeep/backend/models"
|
||||
"github.com/trackeep/backend/services"
|
||||
"github.com/trackeep/backend/utils"
|
||||
)
|
||||
|
||||
// IsDemoMode checks if demo mode is enabled
|
||||
func IsDemoMode() bool {
|
||||
return os.Getenv("VITE_DEMO_MODE") == "true"
|
||||
}
|
||||
|
||||
// initializeSecuritySecrets sets up JWT and encryption secrets on startup
|
||||
func initializeSecuritySecrets() error {
|
||||
// Initialize JWT secret
|
||||
jwtSecret, err := utils.GetOrCreateJWTSecret()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize JWT secret: %w", err)
|
||||
return err
|
||||
}
|
||||
os.Setenv("JWT_SECRET", jwtSecret)
|
||||
log.Println("JWT secret initialized successfully")
|
||||
|
||||
// Initialize encryption key
|
||||
encryptionKey, err := utils.GetOrCreateEncryptionKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize encryption key: %w", err)
|
||||
return err
|
||||
}
|
||||
os.Setenv("ENCRYPTION_KEY", encryptionKey)
|
||||
log.Println("Encryption key initialized successfully")
|
||||
@@ -48,13 +39,9 @@ func initializeSecuritySecrets() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var startTime = time.Now()
|
||||
|
||||
func main() {
|
||||
// Set application version
|
||||
os.Setenv("APP_VERSION", "1.0.0")
|
||||
|
||||
// Load environment variables from multiple possible locations
|
||||
envPaths := []string{".env", "../.env", "/app/.env"}
|
||||
envLoaded := false
|
||||
|
||||
@@ -70,8 +57,12 @@ func main() {
|
||||
log.Println("No .env file found, using environment variables only")
|
||||
}
|
||||
|
||||
// Initialize database (skip in demo mode)
|
||||
if !IsDemoMode() {
|
||||
cfg := config.Load()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
log.Printf("Configuration warning: %v", err)
|
||||
}
|
||||
|
||||
if !cfg.App.DemoMode {
|
||||
config.InitDatabase()
|
||||
models.InitDB()
|
||||
models.AutoMigrate()
|
||||
@@ -115,191 +106,12 @@ func main() {
|
||||
// Apply general rate limiting to all endpoints
|
||||
r.Use(middleware.GeneralRateLimit(rateLimiters["general"]))
|
||||
|
||||
// CORS middleware - environment-aware
|
||||
r.Use(func(c *gin.Context) {
|
||||
allowedOrigins := os.Getenv("CORS_ALLOWED_ORIGINS")
|
||||
ginMode := os.Getenv("GIN_MODE")
|
||||
r.Use(middleware.CORSMiddleware())
|
||||
|
||||
// Default origins based on environment
|
||||
if allowedOrigins == "" {
|
||||
if ginMode == "release" {
|
||||
// Production: no default origins - must be explicitly set
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "CORS not configured for production",
|
||||
"message": "Please set CORS_ALLOWED_ORIGINS environment variable",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
} else {
|
||||
// Development: allow localhost origins
|
||||
allowedOrigins = "http://localhost:5173,http://localhost:3000,http://localhost:8080"
|
||||
}
|
||||
}
|
||||
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
allowed := false
|
||||
|
||||
// Handle wildcard origin
|
||||
if allowedOrigins == "*" {
|
||||
allowed = true
|
||||
} else {
|
||||
for _, allowedOrigin := range strings.Split(allowedOrigins, ",") {
|
||||
if strings.TrimSpace(allowedOrigin) == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
if allowedOrigins == "*" {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
}
|
||||
|
||||
// Set other CORS headers
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Max-Age", "86400") // 24 hours
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// Enhanced health check endpoint
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
// Check database connection
|
||||
db := config.GetDB()
|
||||
dbStatus := "connected"
|
||||
var dbPingTime time.Duration = 0
|
||||
|
||||
if db == nil {
|
||||
dbStatus = "disconnected"
|
||||
} else {
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
dbStatus = "error"
|
||||
} else {
|
||||
start := time.Now()
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
dbStatus = "error"
|
||||
} else {
|
||||
dbPingTime = time.Since(start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check session store
|
||||
sessionStatus := "ok"
|
||||
// Since sessionStore is package-private in middleware, we assume it's initialized
|
||||
// if we've reached this point in the code execution
|
||||
|
||||
// Get system stats
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
uptime := time.Since(startTime)
|
||||
|
||||
health := gin.H{
|
||||
"status": "ok",
|
||||
"message": "Trackeep API is running",
|
||||
"version": "1.0.0",
|
||||
"uptime": uptime.String(),
|
||||
"database": gin.H{
|
||||
"status": dbStatus,
|
||||
"ping_time": dbPingTime.String(),
|
||||
},
|
||||
"sessions": gin.H{
|
||||
"status": sessionStatus,
|
||||
},
|
||||
"system": gin.H{
|
||||
"goroutines": runtime.NumGoroutine(),
|
||||
"memory": gin.H{
|
||||
"alloc_mb": memStats.Alloc / 1024 / 1024,
|
||||
"total_alloc_mb": memStats.TotalAlloc / 1024 / 1024,
|
||||
"sys_mb": memStats.Sys / 1024 / 1024,
|
||||
},
|
||||
},
|
||||
"timestamp": gin.H{
|
||||
"human": time.Now().Format(time.RFC3339),
|
||||
"unix": time.Now().Unix(),
|
||||
},
|
||||
}
|
||||
|
||||
// Determine overall health status
|
||||
overallStatus := "healthy"
|
||||
if dbStatus != "connected" {
|
||||
overallStatus = "degraded"
|
||||
health["status"] = "degraded"
|
||||
}
|
||||
if sessionStatus != "ok" {
|
||||
overallStatus = "degraded"
|
||||
health["status"] = "degraded"
|
||||
}
|
||||
|
||||
statusCode := 200
|
||||
if overallStatus == "degraded" {
|
||||
statusCode = 503
|
||||
}
|
||||
|
||||
c.JSON(statusCode, health)
|
||||
})
|
||||
|
||||
// Readiness probe endpoint (for Kubernetes/container orchestration)
|
||||
r.GET("/ready", func(c *gin.Context) {
|
||||
db := config.GetDB()
|
||||
if db == nil {
|
||||
c.JSON(503, gin.H{"status": "not_ready", "reason": "database_not_connected"})
|
||||
return
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
c.JSON(503, gin.H{"status": "not_ready", "reason": "database_ping_failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// Assume session store is initialized if we're running
|
||||
c.JSON(200, gin.H{"status": "ready"})
|
||||
})
|
||||
|
||||
// API configuration endpoint for frontend
|
||||
r.GET("/api/v1/config", func(c *gin.Context) {
|
||||
// Get the current request info to determine base URL
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
host := c.Request.Host
|
||||
if host == "" {
|
||||
// Fallback to environment or localhost
|
||||
host = os.Getenv("HOST")
|
||||
if host == "" {
|
||||
host = "localhost:8080"
|
||||
}
|
||||
}
|
||||
|
||||
apiURL := fmt.Sprintf("%s://%s/api/v1", scheme, host)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"api_url": apiURL,
|
||||
"demo_mode": os.Getenv("VITE_DEMO_MODE") == "true",
|
||||
"version": "1.0.0",
|
||||
})
|
||||
})
|
||||
|
||||
// Liveness probe endpoint (for Kubernetes/container orchestration)
|
||||
r.GET("/live", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "alive", "timestamp": time.Now().Unix()})
|
||||
})
|
||||
r.GET("/health", handlers.HealthCheck)
|
||||
r.GET("/ready", handlers.ReadinessCheck)
|
||||
r.GET("/live", handlers.LivenessCheck)
|
||||
r.GET("/api/v1/config", handlers.GetAPIConfig)
|
||||
|
||||
// Demo status endpoint
|
||||
r.GET("/api/demo/status", handlers.DemoStatus)
|
||||
@@ -363,36 +175,7 @@ func main() {
|
||||
github.GET("/repos", handlers.GetGitHubRepos)
|
||||
}
|
||||
|
||||
// Public YouTube search test endpoint (no auth required for testing)
|
||||
v1.POST("/youtube-search-test", func(c *gin.Context) {
|
||||
var req struct {
|
||||
Query string `json:"query" binding:"required"`
|
||||
MaxResults int `json:"max_results"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.MaxResults <= 0 {
|
||||
req.MaxResults = 5
|
||||
}
|
||||
|
||||
// Search videos using the YouTube service
|
||||
response, err := services.SearchYouTubeVideos(req.Query, req.MaxResults, "")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to search YouTube videos",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": response,
|
||||
})
|
||||
})
|
||||
v1.POST("/youtube-search-test", handlers.YouTubeSearchTest)
|
||||
|
||||
// Protected auth routes (with demo mode protection)
|
||||
authProtected := v1.Group("/auth")
|
||||
@@ -910,41 +693,30 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Start server with graceful shutdown
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
// Create HTTP server with custom configuration
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Addr: ":" + cfg.Server.Port,
|
||||
Handler: r,
|
||||
ReadTimeout: 15 * time.Second,
|
||||
WriteTimeout: 15 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
ReadTimeout: cfg.Server.ReadTimeout,
|
||||
WriteTimeout: cfg.Server.WriteTimeout,
|
||||
IdleTimeout: cfg.Server.IdleTimeout,
|
||||
}
|
||||
|
||||
// Start server in a goroutine
|
||||
go func() {
|
||||
log.Printf("Server starting on port %s", port)
|
||||
log.Printf("Server starting on port %s", cfg.Server.Port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
// Cleanup sessions
|
||||
middleware.CleanupSessionsOnShutdown()
|
||||
log.Println("Sessions cleaned up")
|
||||
|
||||
// Create a deadline for shutdown
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Attempt graceful shutdown
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
allowedOrigins := os.Getenv("CORS_ALLOWED_ORIGINS")
|
||||
ginMode := os.Getenv("GIN_MODE")
|
||||
|
||||
if allowedOrigins == "" {
|
||||
if ginMode == "release" {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "CORS not configured for production",
|
||||
"message": "Please set CORS_ALLOWED_ORIGINS environment variable",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
} else {
|
||||
allowedOrigins = "http://localhost:5173,http://localhost:3000,http://localhost:8080"
|
||||
}
|
||||
}
|
||||
|
||||
origin := c.Request.Header.Get("Origin")
|
||||
allowed := false
|
||||
|
||||
if allowedOrigins == "*" {
|
||||
allowed = true
|
||||
} else {
|
||||
for _, allowedOrigin := range strings.Split(allowedOrigins, ",") {
|
||||
if strings.TrimSpace(allowedOrigin) == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
if allowedOrigins == "*" {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
} else {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
ErrInternal ErrorCode = "INTERNAL_ERROR"
|
||||
ErrBadRequest ErrorCode = "BAD_REQUEST"
|
||||
ErrUnauthorized ErrorCode = "UNAUTHORIZED"
|
||||
ErrForbidden ErrorCode = "FORBIDDEN"
|
||||
ErrNotFound ErrorCode = "NOT_FOUND"
|
||||
ErrConflict ErrorCode = "CONFLICT"
|
||||
ErrValidation ErrorCode = "VALIDATION_ERROR"
|
||||
ErrRateLimit ErrorCode = "RATE_LIMIT_EXCEEDED"
|
||||
ErrServiceUnavailable ErrorCode = "SERVICE_UNAVAILABLE"
|
||||
)
|
||||
|
||||
type AppError struct {
|
||||
Code ErrorCode `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details string `json:"details,omitempty"`
|
||||
HTTPStatus int `json:"-"`
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e.Details != "" {
|
||||
return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.Details)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func NewAppError(code ErrorCode, message string, httpStatus int) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
HTTPStatus: httpStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func NewAppErrorWithDetails(code ErrorCode, message string, httpStatus int, details string) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: details,
|
||||
HTTPStatus: httpStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func WrapError(err error, code ErrorCode, message string, httpStatus int) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Details: err.Error(),
|
||||
HTTPStatus: httpStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func IsAppError(err error) (*AppError, bool) {
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return appErr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func BadRequest(message string) *AppError {
|
||||
return NewAppError(ErrBadRequest, message, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func BadRequestWithDetails(message, details string) *AppError {
|
||||
return NewAppErrorWithDetails(ErrBadRequest, message, http.StatusBadRequest, details)
|
||||
}
|
||||
|
||||
func Unauthorized(message string) *AppError {
|
||||
return NewAppError(ErrUnauthorized, message, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func Forbidden(message string) *AppError {
|
||||
return NewAppError(ErrForbidden, message, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func NotFound(message string) *AppError {
|
||||
return NewAppError(ErrNotFound, message, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func Conflict(message string) *AppError {
|
||||
return NewAppError(ErrConflict, message, http.StatusConflict)
|
||||
}
|
||||
|
||||
func ValidationErr(message string) *AppError {
|
||||
return NewAppError(ErrValidation, message, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func InternalError(message string) *AppError {
|
||||
return NewAppError(ErrInternal, message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func InternalErrorWithDetails(message, details string) *AppError {
|
||||
return NewAppErrorWithDetails(ErrInternal, message, http.StatusInternalServerError, details)
|
||||
}
|
||||
|
||||
func RateLimitExceeded(message string) *AppError {
|
||||
return NewAppError(ErrRateLimit, message, http.StatusTooManyRequests)
|
||||
}
|
||||
|
||||
func ServiceUnavailable(message string) *AppError {
|
||||
return NewAppError(ErrServiceUnavailable, message, http.StatusServiceUnavailable)
|
||||
}
|
||||
Reference in New Issue
Block a user