package main import ( "log" "net/http" "os" "strings" "time" "collabos-auth/internal/db" "collabos-auth/internal/handlers" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) func main() { _ = godotenv.Load() if err := db.InitMongo(); err != nil { log.Fatalf("[FATAL] MongoDB: %v", err) } log.Println("[OK] MongoDB connected") if err := db.InitRedis(); err != nil { log.Printf("[WARN] Redis unavailable (continuing without cache): %v", err) } else if db.Redis != nil { log.Println("[OK] Upstash Redis connected") } gin.SetMode(gin.ReleaseMode) r := gin.New() r.Use(gin.Recovery()) r.Use(corsMiddleware()) // ── Liveness ────────────────────────────────────────────────────────────── r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok", "service": "collabos-auth", "version": "3.0.0"}) }) // ── Auth pipe (WS) ──────────────────────────────────────────────────────── // Single endpoint for ALL login and signup across all providers. // Requires: api_key + nonce in payload. No credentials in URL or headers. r.GET("/ws/auth", handlers.HandleAuthPipe) // ── Data pipe (WS) ──────────────────────────────────────────────────────── // Requires: Authorization: Bearer AND X-API-Token header. // Both verified BEFORE the WS upgrade — no upgrade on bad creds. r.GET("/ws/data", handlers.HandleDataPipe) // ── OTP helper (thin REST proxy to OTP server) ──────────────────────────── // No auth required — just proxies email to OTP server. Rate limiting is // on the OTP server side. No credentials stored here. r.POST("/otp/send", handlers.SendOTPProxy) // ── Profile update ──────────────────────────────────────────────────────── // Requires: Authorization: Bearer AND X-Update-Key: // AND body.uid must match jwt.uid. Three independent checks. r.POST("/update", handlers.HandleUpdate) port := os.Getenv("PORT") if port == "" { port = "7860" } log.Printf("[START] CollabOS Auth v3.0.0 on :%s", port) if err := r.Run(":" + port); err != nil { log.Fatalf("[FATAL] %v", err) } } // corsMiddleware builds the CORS handler from ALLOWED_ORIGINS env. // Default is * for development. Set to comma-separated URLs for production. func corsMiddleware() gin.HandlerFunc { originsRaw := os.Getenv("ALLOWED_ORIGINS") return func(c *gin.Context) { origin := c.Request.Header.Get("Origin") allowed := false if originsRaw == "" || originsRaw == "*" { allowed = true c.Header("Access-Control-Allow-Origin", "*") } else { for _, o := range strings.Split(originsRaw, ",") { if strings.TrimSpace(o) == origin { allowed = true c.Header("Access-Control-Allow-Origin", origin) c.Header("Vary", "Origin") break } } } if !allowed { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "origin not allowed"}) return } c.Header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Token, X-Update-Key") c.Header("Access-Control-Max-Age", "86400") if c.Request.Method == http.MethodOptions { c.AbortWithStatus(http.StatusNoContent) return } // Add security headers on every response c.Header("X-Content-Type-Options", "nosniff") c.Header("X-Frame-Options", "DENY") c.Header("Referrer-Policy", "strict-origin-when-cross-origin") c.Header("Cache-Control", "no-store") // Timestamp every response for debugging c.Header("X-Response-Time", time.Now().UTC().Format(time.RFC3339)) c.Next() } }