mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
357 lines
9.6 KiB
Go
357 lines
9.6 KiB
Go
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)
|
|
}
|