package main import ( "context" "net/http" "os" "os/signal" "syscall" "time" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "github.com/rs/zerolog" "github.com/rs/zerolog/hlog" "aamos-admin/config" "aamos-admin/db" "aamos-admin/handlers" "aamos-admin/middleware" ) func main() { logger := zerolog.New(os.Stdout).With().Timestamp().Logger() cfg := config.Load() port := os.Getenv("PORT") if port == "" { port = "9090" } database, err := db.Connect(cfg.DBURL) if err != nil { logger.Fatal().Err(err).Msg("database connect failed") } defer database.Close() // Extend users table for password auth and soft-delete (idempotent). if _, err := database.Exec( `ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT NOT NULL DEFAULT ''`, ); err != nil { logger.Fatal().Err(err).Msg("password_hash migration failed") } usersH := &handlers.UsersHandler{DB: database} if err := usersH.EnsureSchema(); err != nil { logger.Fatal().Err(err).Msg("users deleted_at migration failed") } authH := &handlers.AuthHandler{ DB: database, JWTSecret: []byte(cfg.JWTSecret), } // External data handler (quiXzoom, LandveX, Economy, CRM) externalH := handlers.UnifiedDashboardHandler(database, cfg.AMOSBaseURL) modulesH, err := handlers.NewModulesHandler(database) if err != nil { logger.Fatal().Err(err).Msg("modules seed failed") } settingsH := handlers.NewSettingsHandler(database) if err := settingsH.Init(); err != nil { logger.Fatal().Err(err).Msg("settings init failed") } onboardingH := handlers.NewOnboardingHandler(database) if err := onboardingH.Init(); err != nil { logger.Fatal().Err(err).Msg("onboarding init failed") } hub := handlers.NewHub() go hub.Run() r := chi.NewRouter() r.Use(middleware.CORS) r.Use(hlog.NewHandler(logger)) r.Use(hlog.RequestIDHandler("req_id", "X-Request-ID")) r.Use(middleware.Logger(logger)) r.Use(chimw.Recoverer) // Public r.Handle("/health", handlers.NewHealthHandler()) r.Post("/api/v1/auth/login", authH.Login) r.Post("/api/v1/auth/logout", authH.Logout) // Protected r.Group(func(r chi.Router) { r.Use(middleware.Auth(cfg)) r.Get("/api/v1/auth/me", authH.Me) r.Get("/api/v1/dashboard", handlers.DashboardHandler(database)) r.Get("/api/v1/unified-dashboard", externalH) r.Get("/api/v1/service-health", handlers.ServiceHealthHandler) r.Get("/api/v1/metrics", handlers.MetricsHandler) r.Get("/api/v1/audit", handlers.AuditHandler(database)) // UsersHandler.ServeHTTP dispatches on method and path suffix. r.Handle("/api/v1/users", usersH) r.Handle("/api/v1/users/{id}", usersH) // ModulesHandler methods; Toggle parses the path internally. r.Get("/api/v1/modules", modulesH.List) r.Put("/api/v1/modules/{id}/toggle", modulesH.Toggle) // SettingsHandler.ServeHTTP dispatches on GET/PUT. r.Handle("/api/v1/settings", settingsH) r.Post("/api/v1/onboarding/start", onboardingH.ServeHTTP) // WebSocket — upgrade is rejected before handshake if token is missing. r.Get("/ws/metrics", hub.ServeWS) }) srv := &http.Server{ Addr: ":" + port, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 120 * time.Second, } go func() { logger.Info().Str("addr", srv.Addr).Msg("server starting") if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { logger.Fatal().Err(err).Msg("listen error") } }() quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit logger.Info().Msg("shutting down") ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { logger.Error().Err(err).Msg("shutdown error") } }