271 lines
7.8 KiB
Go
271 lines
7.8 KiB
Go
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())
|
|
}
|