package app import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "git.linuxforward.com/byom/byom-gateway/config" "git.linuxforward.com/byom/byom-gateway/handlers" "git.linuxforward.com/byom/byom-gateway/logger" "git.linuxforward.com/byom/byom-gateway/middleware" "git.linuxforward.com/byom/byom-gateway/store" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" ) type App struct { entry *logrus.Entry cnf *config.Config ctx context.Context cancel context.CancelFunc dbHandler *store.DataStore userHandler *handlers.UserHandler router *gin.Engine } func NewApp(cnf *config.Config) (*App, error) { ctx, cancel := context.WithCancel(context.Background()) // Create logger entry := logger.NewLogger("app") // Create app instance app := &App{ entry: entry, cnf: cnf, ctx: ctx, cancel: cancel, } // Initialize services if err := app.initServices(); err != nil { cancel() return nil, fmt.Errorf("failed to initialize services: %w", err) } // Initialize router and middleware if err := app.initRouter(); err != nil { cancel() return nil, fmt.Errorf("init router: %w", err) } return app, nil } func (a *App) initRouter() error { rtr := gin.Default() coreRtr := rtr.Group("/api/v1/gw") // Add core middleware coreRtr.Use(middleware.Recovery(a.entry.Logger)) coreRtr.Use(middleware.RequestID()) coreRtr.Use(middleware.RequestLogger(a.entry.Logger)) coreRtr.Use(middleware.ErrorHandler()) // Add security middleware coreRtr.Use(middleware.SecurityHeaders()) coreRtr.Use(middleware.RequestSanitizer()) // Configure request timeout timeout := 30 * time.Second coreRtr.Use(middleware.TimeoutMiddleware(timeout)) //Configure CORS coreRtr.Use(cors.New(cors.Config{ AllowOrigins: []string{"http://172.27.28.86:5173"}, AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"}, ExposeHeaders: []string{"Content-Length", "Content-Type", "X-Request-ID", "Location"}, AllowCredentials: true, })) // Add health check endpoint coreRtr.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // Add user routes // userGroup := rtr.Group("/user", gin.BasicAuth(gin.Accounts{ // a.cnf.BasicAuth.Username: a.cnf.BasicAuth.Password, // })) userGroup := coreRtr.Group("/user") { userGroup.GET("/host", a.userHandler.GetUserHost) userGroup.POST("/host", a.userHandler.AddUserHost) userGroup.DELETE("/host", a.userHandler.DeleteUserHost) userGroup.PUT("/host", a.userHandler.UpdateUserHost) } a.router = rtr return nil } func (a *App) initServices() error { // Initialize database dbConn, err := initDBConn(&a.cnf.Database) if err != nil { return fmt.Errorf("init database: %w", err) } dbHandler := store.NewDataStore(dbConn) a.dbHandler = dbHandler // Initialize user handler userHandler := handlers.NewUserHandler(dbHandler, a.cnf.Homepage) a.userHandler = userHandler return nil } func initDBConn(cnf *config.DatabaseConfig) (*gorm.DB, error) { db, err := gorm.Open(sqlite.Open(cnf.File), &gorm.Config{}) if err != nil { return nil, fmt.Errorf("database: %w", err) } //Tune the DB sqlDB, err := db.DB() if err != nil { return nil, fmt.Errorf("database: %w", err) } sqlDB.SetMaxOpenConns(cnf.MaxOpenConns) sqlDB.SetMaxIdleConns(cnf.MaxIdleConns) sqlDB.SetConnMaxLifetime(time.Duration(cnf.ConnMaxLifetime) * time.Second) for _, pragma := range cnf.Pragma { tx := db.Exec("PRAGMA " + pragma) if tx.Error != nil { return nil, fmt.Errorf("database: %w", tx.Error) } } return db, nil } func (a *App) Run() error { a.entry.Info("Starting server...") // Configure server with timeouts srv := &http.Server{ Addr: ":" + a.cnf.Server.ListeningPort, Handler: a.router, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second, } // Start server go func() { var err error if a.cnf.Server.TlsConfig.Enabled { a.entry.Infof("Starting server on port %s with TLS", a.cnf.Server.ListeningPort) err = srv.ListenAndServeTLS( a.cnf.Server.TlsConfig.CertFile, a.cnf.Server.TlsConfig.KeyFile, ) } else { a.entry.Infof("Starting server on port %s without TLS", a.cnf.Server.ListeningPort) err = srv.ListenAndServe() } if err != nil && err != http.ErrServerClosed { a.entry.WithError(err).Error("Server error") // Signal shutdown on critical error a.cancel() } }() // Graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) select { case <-quit: a.entry.Info("Shutdown signal received...") case <-a.ctx.Done(): a.entry.Info("Server error occurred, initiating shutdown...") } // Create shutdown context with timeout ctxShutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Shutdown server if err := srv.Shutdown(ctxShutdown); err != nil { a.entry.WithError(err).Error("Server forced to shutdown") return fmt.Errorf("server forced to shutdown: %w", err) } // Close all services if err := a.Close(); err != nil { a.entry.WithError(err).Error("Error during service cleanup") return fmt.Errorf("service cleanup error: %w", err) } a.entry.Info("Server stopped gracefully") return nil } func (a *App) Close() error { if err := a.dbHandler.Close(); err != nil { return err } return nil }