123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- 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
- }
|