mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
- Add token hash fields to Master and Key models for indexed lookups - Implement SyncService integration in admin and master handlers - Update master key validation with backward-compatible digest lookup - Hash child keys in database and store token digests for Redis sync - Add master metadata sync to Redis for balancer validation - Ensure backward compatibility with legacy rows during migration
167 lines
4.7 KiB
Go
167 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/ez-api/ez-api/internal/api"
|
|
"github.com/ez-api/ez-api/internal/config"
|
|
"github.com/ez-api/ez-api/internal/middleware"
|
|
"github.com/ez-api/ez-api/internal/model"
|
|
"github.com/ez-api/ez-api/internal/service"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/driver/postgres"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func main() {
|
|
// 1. Load Configuration
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
log.Fatalf("Failed to load config: %v", err)
|
|
}
|
|
|
|
// 2. Initialize Redis Client
|
|
rdb := redis.NewClient(&redis.Options{
|
|
Addr: cfg.Redis.Addr,
|
|
Password: cfg.Redis.Password,
|
|
DB: cfg.Redis.DB,
|
|
})
|
|
|
|
// Verify Redis connection
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := rdb.Ping(ctx).Err(); err != nil {
|
|
log.Fatalf("Failed to connect to Redis: %v", err)
|
|
}
|
|
log.Println("Connected to Redis successfully")
|
|
|
|
// 3. Initialize GORM (PostgreSQL)
|
|
db, err := gorm.Open(postgres.Open(cfg.Postgres.DSN), &gorm.Config{})
|
|
if err != nil {
|
|
log.Fatalf("Failed to connect to PostgreSQL: %v", err)
|
|
}
|
|
sqlDB, err := db.DB()
|
|
if err != nil {
|
|
log.Fatalf("Failed to get generic database object: %v", err)
|
|
}
|
|
// Verify DB connection
|
|
if err := sqlDB.Ping(); err != nil {
|
|
log.Fatalf("Failed to ping PostgreSQL: %v", err)
|
|
}
|
|
log.Println("Connected to PostgreSQL successfully")
|
|
|
|
// Auto Migrate
|
|
if err := db.AutoMigrate(&model.Master{}, &model.Key{}, &model.Provider{}, &model.Model{}, &model.LogRecord{}); err != nil {
|
|
log.Fatalf("Failed to auto migrate: %v", err)
|
|
}
|
|
|
|
// 4. Setup Services and Handlers
|
|
syncService := service.NewSyncService(rdb)
|
|
logWriter := service.NewLogWriter(db, cfg.Log.QueueCapacity, cfg.Log.BatchSize, cfg.Log.FlushInterval)
|
|
logCtx, cancelLogs := context.WithCancel(context.Background())
|
|
defer cancelLogs()
|
|
logWriter.Start(logCtx)
|
|
|
|
adminService, err := service.NewAdminService()
|
|
if err != nil {
|
|
log.Fatalf("Failed to create admin service: %v", err)
|
|
}
|
|
masterService := service.NewMasterService(db)
|
|
healthService := service.NewHealthCheckService(db, rdb)
|
|
|
|
handler := api.NewHandler(db, syncService, logWriter)
|
|
adminHandler := api.NewAdminHandler(masterService, syncService)
|
|
masterHandler := api.NewMasterHandler(masterService, syncService)
|
|
|
|
// 4.1 Prime Redis snapshots so DP can start with data
|
|
if err := syncService.SyncAll(db); err != nil {
|
|
log.Printf("Initial sync warning: %v", err)
|
|
}
|
|
|
|
// 5. Setup Gin Router
|
|
r := gin.Default()
|
|
|
|
// CORS Middleware
|
|
r.Use(func(c *gin.Context) {
|
|
c.Writer.Header().Set("Access-Control-Allow-Origin", "*") // TODO: Restrict this in production
|
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
|
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
|
|
|
|
if c.Request.Method == "OPTIONS" {
|
|
c.AbortWithStatus(204)
|
|
return
|
|
}
|
|
|
|
c.Next()
|
|
})
|
|
|
|
// Health Check Endpoint
|
|
r.GET("/health", func(c *gin.Context) {
|
|
status := healthService.Check(c.Request.Context())
|
|
httpStatus := http.StatusOK
|
|
if status.Status == "down" {
|
|
httpStatus = http.StatusServiceUnavailable
|
|
}
|
|
c.JSON(httpStatus, status)
|
|
})
|
|
|
|
// API Routes
|
|
// Admin Routes
|
|
adminGroup := r.Group("/admin")
|
|
adminGroup.Use(middleware.AdminAuthMiddleware(adminService))
|
|
{
|
|
adminGroup.POST("/masters", adminHandler.CreateMaster)
|
|
// Other admin routes for managing providers, models, etc.
|
|
adminGroup.POST("/providers", handler.CreateProvider)
|
|
adminGroup.POST("/models", handler.CreateModel)
|
|
adminGroup.GET("/models", handler.ListModels)
|
|
adminGroup.POST("/sync/snapshot", handler.SyncSnapshot)
|
|
}
|
|
|
|
// Master Routes
|
|
masterGroup := r.Group("/v1")
|
|
masterGroup.Use(middleware.MasterAuthMiddleware(masterService))
|
|
{
|
|
masterGroup.POST("/tokens", masterHandler.IssueChildKey)
|
|
}
|
|
|
|
// Public/General Routes (if any)
|
|
r.POST("/logs", handler.IngestLog)
|
|
|
|
srv := &http.Server{
|
|
Addr: ":" + cfg.Server.Port,
|
|
Handler: r,
|
|
}
|
|
|
|
// 6. Start Server with Graceful Shutdown
|
|
go func() {
|
|
log.Printf("Starting ez-api on port %s", cfg.Server.Port)
|
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
log.Fatalf("Server failed: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
<-quit
|
|
log.Println("Shutting down server...")
|
|
|
|
// Shutdown with timeout
|
|
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if err := srv.Shutdown(ctx); err != nil {
|
|
log.Fatalf("Server forced to shutdown: %v", err)
|
|
}
|
|
|
|
log.Println("Server exited properly")
|
|
}
|