server.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package app
  2. import (
  3. "context"
  4. "fmt"
  5. "net/http"
  6. "os"
  7. "os/signal"
  8. "strconv"
  9. "strings"
  10. "syscall"
  11. "time"
  12. "git.linuxforward.com/byom/byom-core/config"
  13. "git.linuxforward.com/byom/byom-core/handlers"
  14. "git.linuxforward.com/byom/byom-core/hook"
  15. "git.linuxforward.com/byom/byom-core/jwtutils"
  16. "git.linuxforward.com/byom/byom-core/logger"
  17. "git.linuxforward.com/byom/byom-core/middleware"
  18. "git.linuxforward.com/byom/byom-core/smtp"
  19. "git.linuxforward.com/byom/byom-core/store"
  20. "github.com/gin-gonic/gin"
  21. "github.com/sirupsen/logrus"
  22. "gorm.io/driver/sqlite"
  23. "gorm.io/gorm"
  24. )
  25. type App struct {
  26. entry *logrus.Entry
  27. cnf *config.Config
  28. ctx context.Context
  29. cancel context.CancelFunc
  30. dataStore *store.DataStore
  31. emailSvc *smtp.Service
  32. jwtSvc *jwtutils.Service
  33. hookSvc *hook.HookClient
  34. userHandler *handlers.UserHandler
  35. workspaceHandler *handlers.WorkspaceHandler
  36. profileHandler *handlers.ProfileHandler
  37. router *gin.Engine
  38. }
  39. type ServiceHandler interface {
  40. SetupRoutes(r *gin.Engine)
  41. Close() error
  42. }
  43. // NewApp creates a new application instance with the given options
  44. func NewApp(cnf *config.Config) (*App, error) {
  45. ctx, cancel := context.WithCancel(context.Background())
  46. // Create logger
  47. entry := logger.NewLogger("app")
  48. // Create app instance
  49. app := &App{
  50. entry: entry,
  51. cnf: cnf,
  52. ctx: ctx,
  53. cancel: cancel,
  54. }
  55. // Initialize router and middleware
  56. if err := app.initRouter(); err != nil {
  57. cancel()
  58. return nil, fmt.Errorf("init router: %w", err)
  59. }
  60. // Initialize services
  61. if err := app.initServices(); err != nil {
  62. cancel()
  63. return nil, fmt.Errorf("failed to initialize services: %w", err)
  64. }
  65. return app, nil
  66. }
  67. // initRouter initializes the router and middleware
  68. func (a *App) initRouter() error {
  69. rtr := gin.New()
  70. // Add core middleware
  71. rtr.Use(middleware.Recovery(a.entry.Logger))
  72. rtr.Use(middleware.RequestID())
  73. rtr.Use(middleware.RequestLogger(a.entry.Logger))
  74. rtr.Use(middleware.ErrorHandler())
  75. // Add security middleware
  76. rtr.Use(middleware.SecurityHeaders())
  77. rtr.Use(middleware.RequestSanitizer())
  78. // Configure request timeout
  79. timeout := 30 * time.Second
  80. if a.cnf.Server.RequestTimeout > 0 {
  81. timeout = time.Duration(a.cnf.Server.RequestTimeout) * time.Second
  82. }
  83. rtr.Use(middleware.TimeoutMiddleware(timeout))
  84. // Configure CORS
  85. corsConfig := middleware.DefaultCORSConfig()
  86. if len(a.cnf.Server.CorsOrigins) > 0 {
  87. a.entry.WithField("origins", a.cnf.Server.CorsOrigins).Info("Configuring CORS with specific origins")
  88. corsConfig.AllowOrigins = a.cnf.Server.CorsOrigins
  89. } else {
  90. a.entry.Warn("No CORS origins specified, using development defaults (localhost only)")
  91. }
  92. rtr.Use(middleware.CORS(corsConfig))
  93. // Add health check endpoint
  94. addRoutes(rtr, a.jwtSvc, a.userHandler, a.workspaceHandler, a.profileHandler)
  95. a.router = rtr
  96. return nil
  97. }
  98. // initServices initializes all required services
  99. func (a *App) initServices() error {
  100. // Initialize database
  101. dbConn, err := initDBConn(a.cnf.Database)
  102. if err != nil {
  103. return fmt.Errorf("init database: %w", err)
  104. }
  105. // Initialize services if not already set through options
  106. if a.emailSvc == nil {
  107. a.emailSvc = smtp.NewService(a.cnf.Smtp)
  108. }
  109. a.jwtSvc = jwtutils.NewService(a.cnf.Jwt.JwtSecret)
  110. a.hookSvc = hook.NewHookClient(a.cnf.Hook.BaseURL, a.cnf.Hook.Domain, a.cnf.Hook.SecretKey)
  111. a.dataStore = store.NewDataStore(dbConn)
  112. // Initialize handlers
  113. a.userHandler = handlers.NewUserHandler(a.dataStore, a.emailSvc, a.jwtSvc, a.hookSvc)
  114. a.workspaceHandler = handlers.NewWorkspaceHandler(a.dataStore)
  115. a.profileHandler = handlers.NewProfileHandler(a.dataStore)
  116. return nil
  117. }
  118. func (a *App) Run() error {
  119. a.entry.Info("Starting server...")
  120. if len(a.cnf.Server.Opts) > 0 {
  121. a.router.Use(func(c *gin.Context) {
  122. for _, opt := range a.cnf.Server.Opts {
  123. parts := strings.Split(opt, ": ")
  124. if len(parts) == 2 {
  125. c.Header(parts[0], parts[1])
  126. }
  127. }
  128. c.Next()
  129. })
  130. }
  131. // Configure server with timeouts
  132. srv := &http.Server{
  133. Addr: fmt.Sprintf(":%s", strconv.Itoa(a.cnf.Server.ListeningPort)),
  134. Handler: a.router,
  135. ReadTimeout: 15 * time.Second,
  136. WriteTimeout: 15 * time.Second,
  137. IdleTimeout: 60 * time.Second,
  138. }
  139. // Start server
  140. go func() {
  141. var err error
  142. if a.cnf.Server.TlsConfig.Enabled {
  143. a.entry.Infof("Starting server on port %d with TLS", a.cnf.Server.ListeningPort)
  144. err = srv.ListenAndServeTLS(
  145. a.cnf.Server.TlsConfig.CertFile,
  146. a.cnf.Server.TlsConfig.KeyFile,
  147. )
  148. } else {
  149. a.entry.Infof("Starting server on port %d without TLS", a.cnf.Server.ListeningPort)
  150. err = srv.ListenAndServe()
  151. }
  152. if err != nil && err != http.ErrServerClosed {
  153. a.entry.WithError(err).Error("Server error")
  154. // Signal shutdown on critical error
  155. a.cancel()
  156. }
  157. }()
  158. // Graceful shutdown
  159. quit := make(chan os.Signal, 1)
  160. signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
  161. select {
  162. case <-quit:
  163. a.entry.Info("Shutdown signal received...")
  164. case <-a.ctx.Done():
  165. a.entry.Info("Server error occurred, initiating shutdown...")
  166. }
  167. // Create shutdown context with timeout
  168. ctxShutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  169. defer cancel()
  170. // Shutdown server
  171. if err := srv.Shutdown(ctxShutdown); err != nil {
  172. a.entry.WithError(err).Error("Server forced to shutdown")
  173. return fmt.Errorf("server forced to shutdown: %w", err)
  174. }
  175. // Close all services
  176. if err := a.Close(); err != nil {
  177. a.entry.WithError(err).Error("Error during service cleanup")
  178. return fmt.Errorf("service cleanup error: %w", err)
  179. }
  180. a.entry.Info("Server stopped gracefully")
  181. return nil
  182. }
  183. func initDBConn(cnf *config.Database) (*gorm.DB, error) {
  184. db, err := gorm.Open(sqlite.Open(cnf.Path), &gorm.Config{})
  185. if err != nil {
  186. return nil, fmt.Errorf("database: %w", err)
  187. }
  188. //Tune the DB
  189. sqlDB, err := db.DB()
  190. if err != nil {
  191. return nil, fmt.Errorf("database: %w", err)
  192. }
  193. sqlDB.SetMaxOpenConns(cnf.MaxOpenConns)
  194. sqlDB.SetMaxIdleConns(cnf.MaxIdleConns)
  195. sqlDB.SetConnMaxLifetime(time.Duration(cnf.ConnMaxLifetime) * time.Second)
  196. for _, pragma := range cnf.Pragma {
  197. tx := db.Exec("PRAGMA " + pragma)
  198. if tx.Error != nil {
  199. return nil, fmt.Errorf("database: %w", tx.Error)
  200. }
  201. }
  202. return db, nil
  203. }
  204. func (a *App) Close() error {
  205. a.entry.Info("Closing application resources...")
  206. if err := a.dataStore.Close(); err != nil {
  207. a.entry.WithError(err).Error("Failed to close database connections")
  208. return err
  209. }
  210. if err := a.emailSvc.Close(); err != nil {
  211. a.entry.WithError(err).Error("Failed to close email service")
  212. }
  213. if err := a.hookSvc.Close(); err != nil {
  214. a.entry.WithError(err).Error("Failed to close hook service")
  215. }
  216. return nil
  217. }