package main import ( "authorization/db" "authorization/docs" "authorization/handlers" "authorization/helper" "authorization/models" "authorization/redisclient" "database/sql" "fmt" "log" "net" "authorization/routes" "net/http" "os" "time" "github.com/getsentry/sentry-go" "github.com/gorilla/mux" "github.com/joho/godotenv" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/cors" ) // @swagger: "2.0" // @title UESS Authentication Microservice // @version 1.0 // @description This is the API for Authentication Microservice for UESS. It doesn't support OAS 3.0 and is only for documentation purposes. The library used doesn't support @server annotation. // @contact.name Darrel Israel // @contact.email d.israel.psa@gmail.com // @BasePath / // @securityDefinitions.apikey BearerToken // @in header // @name Authorization var ( dbOpenConnections = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "db_open_connections", Help: "Number of open database connections", }) dbInUseConnections = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "db_in_use_connections", Help: "Number of in-use database connections", }) dbIdleConnections = prometheus.NewGauge(prometheus.GaugeOpts{ Name: "db_idle_connections", Help: "Number of idle database connections", }) ) var ( httpRequestsTotal = prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "http_requests_total", Help: "Total number of HTTP requests", }, []string{"path", "method"}, ) httpRequestDuration = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_duration_seconds", Help: "Duration of HTTP requests in seconds", Buckets: prometheus.DefBuckets, }, []string{"path", "method"}, ) httpRequestSize = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_request_size_bytes", Help: "Size of HTTP requests in bytes", Buckets: prometheus.ExponentialBuckets(100, 10, 8), }, []string{"path", "method"}, ) httpResponseSize = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "http_response_size_bytes", Help: "Size of HTTP responses in bytes", Buckets: prometheus.ExponentialBuckets(100, 10, 8), }, []string{"path", "method"}, ) ) func init() { prometheus.MustRegister(httpRequestsTotal) prometheus.MustRegister(httpRequestDuration) prometheus.MustRegister(httpRequestSize) prometheus.MustRegister(httpResponseSize) prometheus.MustRegister(dbOpenConnections) prometheus.MustRegister(dbInUseConnections) prometheus.MustRegister(dbIdleConnections) } func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() metricsPath := os.Getenv("METRICS_PATH") if r.URL.Path != metricsPath { helper.LogInfo(fmt.Sprintf("INFO: Started %s %s", r.Method, r.URL.Path)) } httpRequestsTotal.WithLabelValues(r.URL.Path, r.Method).Inc() requestSize := float64(r.ContentLength) if requestSize < 0 { requestSize = 0 } httpRequestSize.WithLabelValues(r.URL.Path, r.Method).Observe(requestSize) rw := &models.ResponseWriter{ResponseWriter: w} next.ServeHTTP(rw, r) duration := time.Since(start).Seconds() httpRequestDuration.WithLabelValues(r.URL.Path, r.Method).Observe(duration) httpResponseSize.WithLabelValues(r.URL.Path, r.Method).Observe(float64(rw.Size)) // Log completion for non-metrics endpoints if r.URL.Path != metricsPath { helper.LogInfo(fmt.Sprintf("INFO: Completed %s %s in %.3f seconds", r.Method, r.URL.Path, duration)) } }) } func collectDBMetrics(database *sql.DB) { for { stats := database.Stats() dbOpenConnections.Set(float64(stats.OpenConnections)) dbInUseConnections.Set(float64(stats.InUse)) dbIdleConnections.Set(float64(stats.Idle)) time.Sleep(10 * time.Second) // Adjust the interval as needed } } func allowOnlyGrafana(next http.Handler, allowedIP string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { http.Error(w, "Forbidden", http.StatusForbidden) return } if remoteIP == allowedIP { next.ServeHTTP(w, r) return } http.Error(w, "Forbidden", http.StatusForbidden) }) } func main() { // Load environment variables from .env file first // Get current working directory for debugging cwd, _ := os.Getwd() metricsPath := os.Getenv("METRICS_PATH") if metricsPath == "" { log.Print("METRICS_PATH not set, defaulting to /metrics") metricsPath = "/metrics" } log.Printf("Current working directory: %s", cwd) err := godotenv.Load() if err != nil { log.Printf("ERROR: Failed to load .env file from default location: %v", err) // Try with explicit path err = godotenv.Load(".env") if err != nil { log.Fatalf("FATAL: Could not load .env file: %v. Tried paths: default and ./.env", err) } log.Println(".env file loaded successfully from ./.env") } else { log.Println(".env file loaded successfully") } // Verify GO_ENV is loaded goEnv := os.Getenv("GO_ENV") log.Printf("GO_ENV value after loading .env: '%s'", goEnv) if goEnv == "" { log.Fatal("GO_ENV is not set in main. Please set the GO_ENV environment variable.") } // Validate JWT_KEY is set jwtKey := os.Getenv("JWT_KEY") if jwtKey == "" { log.Fatal("JWT_KEY is not set. Please set the JWT_KEY environment variable.") } log.Println("JWT_KEY loaded successfully") DSN := os.Getenv("DSN") if DSN == "" { log.Fatal("Sentry DSN is not set. Please set the DSN environment variable.") } err = sentry.Init(sentry.ClientOptions{ Dsn: os.Getenv("DSN"), TracesSampleRate: 1.0, Environment: goEnv, }) if err != nil { log.Fatalf("sentry.Init: %s", err) } defer sentry.Flush(2 * time.Second) docs.SwaggerInfo.Host = "localhost:8080" docs.SwaggerInfo.Schemes = []string{"http"} helper.LogInfo("INFO: Initializing database connection...") var database *sql.DB attempts := 0 backoffTimes := []time.Duration{2 * time.Second, 5 * time.Second, 10 * time.Second} for { database, err = db.InitDB() if err == nil { break } helper.LogError(fmt.Errorf("ERROR: error initializing database: %v", err), "database initialization error") if attempts >= len(backoffTimes) { log.Fatalf("FATAL: Could not initialize database after %d attempts: %v", attempts, err) } time.Sleep(backoffTimes[attempts]) attempts++ } go collectDBMetrics(database) // Initialize authorization service handlers.InitAuthService() helper.LogInfo("INFO: Authorization service initialized with caching") router := mux.NewRouter() routes.SetupRoutes(router, database) helper.LogInfo("INFO: Database initialized successfully.") allowedIP := os.Getenv("ALLOWED_IP") helper.LogInfo("INFO: Setting up routes...") router.Handle(metricsPath, allowOnlyGrafana(promhttp.Handler(), allowedIP)) router.Use(loggingMiddleware) c := cors.New(cors.Options{ AllowedOrigins: []string{"http://localhost:4173", "http://localhost:5173"}, // Your frontend URL AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, AllowedHeaders: []string{"*"}, // Allow all headers temporarily AllowCredentials: true, // Critical for withCredentials requests MaxAge: 86400, // Cache preflight results }) handler := c.Handler(router) redisclient.Init() helper.LogInfo("INFO: Connected to Redis successfully!") helper.LogInfo("WARNING: Ensure Redis is secured to prevent unauthorized access. Use a strong password and bind Redis to localhost or a secure network.") helper.LogInfo("INFO: Authorization Microservice is running on http://localhost:8080") server := &http.Server{ Addr: ":8080", Handler: handler, ReadTimeout: 15 * time.Second, WriteTimeout: 300 * time.Second, IdleTimeout: 60 * time.Second, } log.Fatal(server.ListenAndServe()) }