package app import ( "context" "fmt" "net/http" "os" "os/signal" "strconv" "strings" "syscall" "time" "git.linuxforward.com/byom/byom-core/config" "git.linuxforward.com/byom/byom-core/handlers" "git.linuxforward.com/byom/byom-core/hook" "git.linuxforward.com/byom/byom-core/jwtutils" "git.linuxforward.com/byom/byom-core/logger" "git.linuxforward.com/byom/byom-core/middleware" "git.linuxforward.com/byom/byom-core/smtp" "git.linuxforward.com/byom/byom-core/store" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "gorm.io/driver/sqlite" "gorm.io/gorm" ) type App struct { entry *logrus.Entry cnf *config.Config ctx context.Context cancel context.CancelFunc dataStore *store.DataStore emailSvc *smtp.Service jwtSvc *jwtutils.Service hookSvc *hook.HookClient userHandler *handlers.UserHandler workspaceHandler *handlers.WorkspaceHandler profileHandler *handlers.ProfileHandler router *gin.Engine } type ServiceHandler interface { SetupRoutes(r *gin.Engine) Close() error } // NewApp creates a new application instance with the given options 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 router and middleware if err := app.initRouter(); err != nil { cancel() return nil, fmt.Errorf("init router: %w", err) } // Initialize services if err := app.initServices(); err != nil { cancel() return nil, fmt.Errorf("failed to initialize services: %w", err) } return app, nil } // initRouter initializes the router and middleware func (a *App) initRouter() error { rtr := gin.New() // Add core middleware rtr.Use(middleware.Recovery(a.entry.Logger)) rtr.Use(middleware.RequestID()) rtr.Use(middleware.RequestLogger(a.entry.Logger)) rtr.Use(middleware.ErrorHandler()) // Add security middleware rtr.Use(middleware.SecurityHeaders()) rtr.Use(middleware.RequestSanitizer()) // Configure request timeout timeout := 30 * time.Second if a.cnf.Server.RequestTimeout > 0 { timeout = time.Duration(a.cnf.Server.RequestTimeout) * time.Second } rtr.Use(middleware.TimeoutMiddleware(timeout)) // Configure CORS corsConfig := middleware.DefaultCORSConfig() if len(a.cnf.Server.CorsOrigins) > 0 { a.entry.WithField("origins", a.cnf.Server.CorsOrigins).Info("Configuring CORS with specific origins") corsConfig.AllowOrigins = a.cnf.Server.CorsOrigins } else { a.entry.Warn("No CORS origins specified, using development defaults (localhost only)") } rtr.Use(middleware.CORS(corsConfig)) // Add health check endpoint addRoutes(rtr, a.jwtSvc, a.userHandler, a.workspaceHandler, a.profileHandler) a.router = rtr return nil } // initServices initializes all required services func (a *App) initServices() error { // Initialize database dbConn, err := initDBConn(a.cnf.Database) if err != nil { return fmt.Errorf("init database: %w", err) } // Initialize services if not already set through options if a.emailSvc == nil { a.emailSvc = smtp.NewService(a.cnf.Smtp) } a.jwtSvc = jwtutils.NewService(a.cnf.Jwt.JwtSecret) a.hookSvc = hook.NewHookClient(a.cnf.Hook.BaseURL, a.cnf.Hook.Domain, a.cnf.Hook.SecretKey) a.dataStore = store.NewDataStore(dbConn) // Initialize handlers a.userHandler = handlers.NewUserHandler(a.dataStore, a.emailSvc, a.jwtSvc, a.hookSvc) a.workspaceHandler = handlers.NewWorkspaceHandler(a.dataStore) a.profileHandler = handlers.NewProfileHandler(a.dataStore) return nil } func (a *App) Run() error { a.entry.Info("Starting server...") if len(a.cnf.Server.Opts) > 0 { a.router.Use(func(c *gin.Context) { for _, opt := range a.cnf.Server.Opts { parts := strings.Split(opt, ": ") if len(parts) == 2 { c.Header(parts[0], parts[1]) } } c.Next() }) } // Configure server with timeouts srv := &http.Server{ Addr: fmt.Sprintf(":%s", strconv.Itoa(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 %d 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 %d 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 initDBConn(cnf *config.Database) (*gorm.DB, error) { db, err := gorm.Open(sqlite.Open(cnf.Path), &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) Close() error { a.entry.Info("Closing application resources...") if err := a.dataStore.Close(); err != nil { a.entry.WithError(err).Error("Failed to close database connections") return err } if err := a.emailSvc.Close(); err != nil { a.entry.WithError(err).Error("Failed to close email service") } if err := a.hookSvc.Close(); err != nil { a.entry.WithError(err).Error("Failed to close hook service") } return nil }