package main import ( "encoding/json" "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/giphy" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload" "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() // Giphy client (nil if no API key) giphyClient := giphy.NewClient(cfg.Giphy.APIKey) // Upload handler (nil if no MinIO config) uploadHandler, err := upload.NewHandler(cfg) if err != nil { logger.Warn("upload handler not available", "error", err) } // Auth handler authHandler := auth.NewHandler(database.DB, cfg) 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: register, login, logout) r.Route("/auth", func(r chi.Router) { authHandler.RegisterPublicRoutes(r) }) // Protected routes r.Group(func(r chi.Router) { r.Use(middleware.Session(sessionStore, cfg)) r.Use(middleware.RequireAuth) // Auth (protected: me, update profile) authHandler.RegisterProtectedRoutes(r) // 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) }) // Giphy search if giphyClient != nil { r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest) return } gifs, err := giphyClient.Search(query, 20) if err != nil { http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(gifs) }) r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) { gifs, err := giphyClient.GetTrending(20) if err != nil { http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(gifs) }) } // File upload if uploadHandler != nil { r.Post("/upload", uploadHandler.Upload) } }) }) // File serving (public, for viewing uploaded files) if uploadHandler != nil { r.Get("/files/*", uploadHandler.Serve) } // 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) } }