mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
hot fix #1
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type DirectoryController struct {
|
||||
DB *gorm.DB
|
||||
request *gin.Context
|
||||
}
|
||||
|
||||
func NewDirectoryController(db *gorm.DB) *DirectoryController {
|
||||
return &DirectoryController{DB: db}
|
||||
}
|
||||
|
||||
type InstanceRegistrationPayload struct {
|
||||
ClubID string `json:"club_id"`
|
||||
ClubName string `json:"club_name"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
LogoURL string `json:"logo_url"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
IsActive bool `json:"is_active"`
|
||||
Version string `json:"version"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
Features []string `json:"features"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
|
||||
// RegisterInstance registers this club instance with the central directory
|
||||
func (dc *DirectoryController) RegisterInstance(c *gin.Context) {
|
||||
// Store request context for helper methods
|
||||
dc.request = c
|
||||
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load settings"})
|
||||
return
|
||||
}
|
||||
|
||||
// Build registration payload
|
||||
payload := InstanceRegistrationPayload{
|
||||
ClubID: strings.TrimSpace(s.ClubID),
|
||||
ClubName: strings.TrimSpace(s.ClubName),
|
||||
APIBaseURL: dc.getPublicURL(),
|
||||
LogoURL: strings.TrimSpace(s.ClubLogoURL),
|
||||
City: strings.TrimSpace(s.ContactCity),
|
||||
Country: strings.TrimSpace(s.ContactCountry),
|
||||
IsActive: true,
|
||||
Version: strings.TrimSpace(os.Getenv("APP_VERSION")),
|
||||
LastSeen: time.Now(),
|
||||
Tags: map[string]string{
|
||||
"instance_host": dc.getHostname(),
|
||||
"environment": config.AppConfig.AppEnv,
|
||||
"instance_id": dc.getInstanceID(),
|
||||
},
|
||||
Features: dc.getEnabledFeatures(),
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if payload.ClubID == "" || payload.ClubName == "" || payload.APIBaseURL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing required fields: club_id, club_name, api_base_url"})
|
||||
return
|
||||
}
|
||||
|
||||
// Send to central directory
|
||||
if err := dc.sendToCentralDirectory("/api/v1/directory/register", payload); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to register with central directory", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "registered",
|
||||
"club_id": payload.ClubID,
|
||||
"timestamp": payload.LastSeen,
|
||||
})
|
||||
}
|
||||
|
||||
// Heartbeat sends a heartbeat to central directory
|
||||
func (dc *DirectoryController) Heartbeat(c *gin.Context) {
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load settings"})
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"club_id": strings.TrimSpace(s.ClubID),
|
||||
"last_seen": time.Now(),
|
||||
"status": "active",
|
||||
"tags": map[string]string{
|
||||
"instance_host": dc.getHostname(),
|
||||
"environment": config.AppConfig.AppEnv,
|
||||
},
|
||||
}
|
||||
|
||||
if err := dc.sendToCentralDirectory("/api/v1/directory/heartbeat", payload); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to send heartbeat", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "timestamp": time.Now()})
|
||||
}
|
||||
|
||||
// GetInstanceInfo returns this instance's information
|
||||
func (dc *DirectoryController) GetInstanceInfo(c *gin.Context) {
|
||||
// Store request context for helper methods
|
||||
dc.request = c
|
||||
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load settings"})
|
||||
return
|
||||
}
|
||||
|
||||
info := InstanceRegistrationPayload{
|
||||
ClubID: strings.TrimSpace(s.ClubID),
|
||||
ClubName: strings.TrimSpace(s.ClubName),
|
||||
APIBaseURL: dc.getPublicURL(),
|
||||
LogoURL: strings.TrimSpace(s.ClubLogoURL),
|
||||
City: strings.TrimSpace(s.ContactCity),
|
||||
Country: strings.TrimSpace(s.ContactCountry),
|
||||
IsActive: true,
|
||||
Version: strings.TrimSpace(os.Getenv("APP_VERSION")),
|
||||
LastSeen: time.Now(),
|
||||
Tags: map[string]string{
|
||||
"instance_host": dc.getHostname(),
|
||||
"environment": config.AppConfig.AppEnv,
|
||||
"instance_id": dc.getInstanceID(),
|
||||
},
|
||||
Features: dc.getEnabledFeatures(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, info)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (dc *DirectoryController) getPublicURL() string {
|
||||
// Try to get the public URL from settings or environment
|
||||
if config.AppConfig != nil && config.AppConfig.FrontendBaseURL != "" {
|
||||
return strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/") + "/api/v1"
|
||||
}
|
||||
|
||||
// Fallback to constructing from request
|
||||
scheme := "https"
|
||||
if dc.request == nil || dc.request.Request.TLS == nil {
|
||||
scheme = "http"
|
||||
}
|
||||
host := "localhost"
|
||||
if dc.request != nil {
|
||||
host = dc.request.Request.Host
|
||||
if idx := strings.Index(host, ":"); idx >= 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
}
|
||||
return scheme + "://" + host + "/api/v1"
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) getHostname() string {
|
||||
host := "localhost"
|
||||
if dc.request != nil {
|
||||
host = dc.request.Request.Host
|
||||
if idx := strings.Index(host, ":"); idx >= 0 {
|
||||
host = host[:idx]
|
||||
}
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) getInstanceID() string {
|
||||
// Try to get instance ID from environment or generate from hostname
|
||||
if id := strings.TrimSpace(os.Getenv("INSTANCE_ID")); id != "" {
|
||||
return id
|
||||
}
|
||||
|
||||
hostname := dc.getHostname()
|
||||
if hostname != "" {
|
||||
return strings.ReplaceAll(hostname, ".", "-")
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) getEnabledFeatures() []string {
|
||||
var features []string
|
||||
|
||||
// Always add basic features
|
||||
features = append(features, "dashboard", "news", "auth")
|
||||
|
||||
// Check which features are enabled based on settings
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err == nil {
|
||||
if s.VideosModuleEnabled {
|
||||
features = append(features, "videos")
|
||||
}
|
||||
if s.MerchModuleEnabled {
|
||||
features = append(features, "merch")
|
||||
}
|
||||
if s.NewsletterEnabled {
|
||||
features = append(features, "newsletter")
|
||||
}
|
||||
if s.ShowMapOnHomepage {
|
||||
features = append(features, "map")
|
||||
}
|
||||
if s.GalleryURL != "" {
|
||||
features = append(features, "gallery")
|
||||
}
|
||||
// Add matches feature (always enabled for now)
|
||||
features = append(features, "matches")
|
||||
// Add blog feature (always enabled for now)
|
||||
features = append(features, "blog")
|
||||
}
|
||||
|
||||
return features
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) sendToCentralDirectory(endpoint string, payload interface{}) error {
|
||||
// Get central directory URL from environment
|
||||
baseURL := strings.TrimSpace(os.Getenv("DIRECTORY_INGEST_URL"))
|
||||
if baseURL == "" {
|
||||
return nil // Silently skip if not configured
|
||||
}
|
||||
|
||||
// Build full URL
|
||||
fullURL := strings.TrimSuffix(baseURL, "/") + endpoint
|
||||
|
||||
// Marshal payload
|
||||
b, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send request with timeout
|
||||
post := func(u string) bool {
|
||||
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Use same token pattern as error system
|
||||
token := strings.TrimSpace(os.Getenv("DIRECTORY_INGEST_TOKEN"))
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("X-Ingest-Token", token)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
return resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
}
|
||||
|
||||
if post(fullURL) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try Docker host fallback for local development
|
||||
if u, err := url.Parse(fullURL); err == nil {
|
||||
h := u.Hostname()
|
||||
if h == "127.0.0.1" || h == "localhost" {
|
||||
u.Host = strings.Replace(u.Host, h, "host.docker.internal", 1)
|
||||
if post(u.String()) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed to send to central directory")
|
||||
}
|
||||
|
||||
// StartDirectoryHeartbeat starts the background heartbeat process
|
||||
func (dc *DirectoryController) StartDirectoryHeartbeat() {
|
||||
// Check if directory registration is enabled
|
||||
if strings.TrimSpace(os.Getenv("DIRECTORY_INGEST_URL")) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(5 * time.Minute) // Heartbeat every 5 minutes
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
// Send heartbeat in background
|
||||
go func() {
|
||||
payload := map[string]interface{}{
|
||||
"club_id": dc.getClubID(),
|
||||
"last_seen": time.Now(),
|
||||
"status": "active",
|
||||
"tags": map[string]string{
|
||||
"instance_host": dc.getHostname(),
|
||||
"environment": config.AppConfig.AppEnv,
|
||||
},
|
||||
}
|
||||
|
||||
dc.sendToCentralDirectory("/api/v1/directory/heartbeat", payload)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
// Also register immediately on startup
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second) // Wait for server to be ready
|
||||
dc.registerOnStartup()
|
||||
}()
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) getClubID() string {
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(s.ClubID)
|
||||
}
|
||||
|
||||
func (dc *DirectoryController) registerOnStartup() {
|
||||
var s models.Settings
|
||||
if err := dc.DB.First(&s).Error; err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
payload := InstanceRegistrationPayload{
|
||||
ClubID: strings.TrimSpace(s.ClubID),
|
||||
ClubName: strings.TrimSpace(s.ClubName),
|
||||
APIBaseURL: dc.getPublicURL(),
|
||||
LogoURL: strings.TrimSpace(s.ClubLogoURL),
|
||||
City: strings.TrimSpace(s.ContactCity),
|
||||
Country: strings.TrimSpace(s.ContactCountry),
|
||||
IsActive: true,
|
||||
Version: strings.TrimSpace(os.Getenv("APP_VERSION")),
|
||||
LastSeen: time.Now(),
|
||||
Tags: map[string]string{
|
||||
"instance_host": dc.getHostname(),
|
||||
"environment": config.AppConfig.AppEnv,
|
||||
"instance_id": dc.getInstanceID(),
|
||||
},
|
||||
Features: dc.getEnabledFeatures(),
|
||||
}
|
||||
|
||||
dc.sendToCentralDirectory("/api/v1/directory/register", payload)
|
||||
}
|
||||
Reference in New Issue
Block a user