package main import ( "fmt" "log/slog" "net/http" "os" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/channel" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" ) func main() { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) cfg := config.Load() database, err := db.New(cfg) if err != nil { logger.Error("failed to connect to database", "error", err) os.Exit(1) } defer database.Close() if err := database.RunMigrations(); err != nil { logger.Error("failed to run migrations", "error", err) os.Exit(1) } // Session store for middleware sessionStore := auth.NewSessionStore(database.DB, cfg) // WebSocket hub hub := gateway.NewHub(logger) go hub.Run() r := chi.NewRouter() r.Use(chimw.Logger) r.Use(chimw.Recoverer) r.Use(chimw.RequestID) // Health check r.Get("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }) // WebSocket endpoint r.Get("/ws", func(w http.ResponseWriter, r *http.Request) { gateway.ServeWS(database.DB, hub, logger, w, r) }) // API routes r.Route("/api/v1", func(r chi.Router) { // Auth (public) r.Route("/auth", func(r chi.Router) { auth.NewHandler(database.DB, cfg).RegisterRoutes(r) }) // Protected routes r.Group(func(r chi.Router) { r.Use(middleware.Session(sessionStore, cfg)) r.Use(middleware.RequireAuth) // Servers r.Route("/servers", func(r chi.Router) { server.NewHandler(database.DB).RegisterRoutes(r) // Channels (nested under servers) r.Route("/{serverID}/channels", func(r chi.Router) { channel.NewHandler(database.DB).RegisterRoutes(r) }) }) // Messages (under channels) r.Route("/channels/{channelID}/messages", func(r chi.Router) { message.NewHandler(database.DB, hub).RegisterRoutes(r) }) }) }) // Static file serving for production (SPA) staticDir := "/srv/web" if _, err := os.Stat(staticDir); err == nil { fileServer := http.FileServer(http.Dir(staticDir)) r.NotFound(func(w http.ResponseWriter, r *http.Request) { // If the file exists, serve it; otherwise serve index.html (SPA fallback) path := staticDir + r.URL.Path if _, err := os.Stat(path); os.IsNotExist(err) { http.ServeFile(w, r, staticDir+"/index.html") return } fileServer.ServeHTTP(w, r) }) } addr := fmt.Sprintf(":%s", cfg.Port) logger.Info("starting server", "addr", addr) if err := http.ListenAndServe(addr, r); err != nil { logger.Error("server error", "error", err) os.Exit(1) } }