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/smtp" "git.linuxforward.com/byom/byom-core/store" "github.com/gin-contrib/cors" "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 } func NewApp(cnf *config.Config) (*App, error) { ctx, cancel := context.WithCancel(context.Background()) rtr := gin.New() rtr.Use(gin.Recovery()) rtr.Use(gin.Logger()) config := cors.DefaultConfig() config.AllowOrigins = []string{"*"} config.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"} config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"} config.AllowCredentials = true config.ExposeHeaders = []string{"Content-Length"} rtr.Use(cors.New(config)) //init GORM dbConn, err := initDBConn(cnf.Database) if err != nil { cancel() return nil, fmt.Errorf("init db: %w", err) } //init email service emailSvc := smtp.NewService(cnf.Smtp) //init jwt service jwtSvc := jwtutils.NewService(cnf.Jwt.JwtSecret) //init hook service hookSvc := hook.NewHookClient(cnf.Hook.BaseURL, cnf.Hook.Domain, cnf.Hook.SecretKey) //init core functions store := store.NewDataStore(dbConn) userHandler := handlers.NewUserHandler(store, emailSvc, jwtSvc, hookSvc) workspaceHandler := handlers.NewWorkspaceHandler(store) profileHandler := handlers.NewProfileHandler(store) //add routes addRoutes(rtr, jwtSvc, userHandler, workspaceHandler, profileHandler) app := &App{ entry: logrus.WithField("component", "app"), cnf: cnf, ctx: ctx, cancel: cancel, router: rtr, dataStore: store, emailSvc: emailSvc, jwtSvc: jwtSvc, userHandler: userHandler, workspaceHandler: workspaceHandler, profileHandler: profileHandler, } return app, 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 srv := &http.Server{ Addr: fmt.Sprintf(":%s", strconv.Itoa(a.cnf.Server.ListeningPort)), Handler: a.router, } // 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") } }() // Graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit a.entry.Info("Stopping server...") ctxTimeout, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second) defer cancelFunc() err := srv.Shutdown(ctxTimeout) if err != nil { return fmt.Errorf("shutdown server: %w", err) } // Close all services if err := a.Close(); err != nil { a.entry.WithError(err).Error("Error during service cleanup") } a.entry.Info("Server stopped successfully") 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 }