package api import ( "net/http" "os" "path/filepath" "strings" "git.pengzhan.dev/haopengzhan/rsspipe/internal/auth" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/cors" ) func NewRouter(h *Handler, authService *auth.AuthService, staticDir string) http.Handler { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Use(middleware.Compress(5)) // CORS configuration for local development & PWA r.Use(cors.Handler(cors.Options{ AllowedOrigins: []string{"http://localhost:5173", "http://localhost:3000", "http://localhost:8080", "*"}, AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: true, MaxAge: 300, })) // Public share route (/share/.html) r.Get("/share/{uuid}", h.ViewSharedEntry) // API Routes r.Route("/api", func(r chi.Router) { // Public auth endpoints r.Post("/auth/login", h.Login) r.Post("/auth/logout", h.Logout) // Protected endpoints r.Group(func(r chi.Router) { r.Use(authService.Middleware) r.Get("/auth/me", h.GetMe) r.Put("/auth/password", h.UpdatePassword) r.Get("/stats", h.GetStats) // Categories r.Get("/categories", h.ListCategories) r.Post("/categories", h.CreateCategory) r.Delete("/categories/{id}", h.DeleteCategory) // Feeds r.Get("/feeds", h.ListFeeds) r.Post("/feeds", h.CreateFeed) r.Put("/feeds/{id}", h.UpdateFeed) r.Delete("/feeds/{id}", h.DeleteFeed) r.Post("/feeds/{id}/refresh", h.RefreshFeed) r.Post("/feeds/refresh-all", h.RefreshAllFeeds) // Entries r.Get("/entries", h.ListEntries) r.Get("/entries/{id}", h.GetEntry) r.Put("/entries/{id}/read", h.UpdateEntryRead) r.Put("/entries/{id}/star", h.UpdateEntryStar) r.Post("/entries/{id}/share", h.ToggleShareEntry) r.Post("/entries/{id}/ai-summary", h.GenerateAISummary) r.Post("/entries/mark-all-read", h.MarkAllRead) r.Post("/entries/{id}/scrape", h.ScrapeEntry) r.Get("/entries/{id}/variants", h.ListEntryVariants) r.Get("/entries/{id}/variants/{key}/stream", h.GetVariantBlobStream) // Rules r.Get("/rules", h.ListRules) r.Post("/rules", h.CreateRule) r.Put("/rules/{id}", h.UpdateRule) r.Delete("/rules/{id}", h.DeleteRule) r.Post("/rules/{id}/apply", h.ApplyRule) // AI Settings r.Get("/settings/ai", h.GetAISettings) r.Put("/settings/ai", h.UpdateAISettings) r.Post("/settings/ai/test", h.TestAIConnection) // Search History r.Get("/search/history", h.GetSearchHistory) r.Post("/search/history", h.RecordSearch) r.Delete("/search/history", h.DeleteSearchHistory) // OPML r.Post("/opml/import", h.ImportOPML) r.Get("/opml/export", h.ExportOPML) }) }) // Static Files / Single Page Application fallback if staticDir != "" { if _, err := os.Stat(staticDir); err == nil { fileServer := http.FileServer(http.Dir(staticDir)) r.Get("/*", func(w http.ResponseWriter, req *http.Request) { path := filepath.Join(staticDir, req.URL.Path) if _, err := os.Stat(path); os.IsNotExist(err) && !strings.HasPrefix(req.URL.Path, "/api") && !strings.HasPrefix(req.URL.Path, "/share") { http.ServeFile(w, req, filepath.Join(staticDir, "index.html")) return } fileServer.ServeHTTP(w, req) }) } } return r }