package api import ( "io/fs" "log" "net/http" "strings" "yaum/internal/static" ) func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handler { mux := http.NewServeMux() mux.HandleFunc("POST /api/auth/login", auth.Login) mux.HandleFunc("GET /api/auth/verify", auth.Verify) mux.HandleFunc("GET /api/services", h.ListServices) mux.HandleFunc("POST /api/services", h.CreateService) mux.HandleFunc("PUT /api/services/{id}", h.UpdateService) mux.HandleFunc("PATCH /api/services/{id}/toggle", h.ToggleService) mux.HandleFunc("POST /api/services/{id}/test", h.TestService) mux.HandleFunc("DELETE /api/services/{id}", h.DeleteService) mux.HandleFunc("GET /api/services/{id}/history", h.GetHistory) mux.HandleFunc("GET /api/services/{id}/stats", h.GetStats) mux.HandleFunc("GET /api/services/{id}/ssl", h.GetSSLInfo) mux.HandleFunc("GET /api/services/{id}/badge.svg", h.BadgeSVG) mux.HandleFunc("GET /api/active-maintenance", h.GetActiveMaintenance) mux.HandleFunc("GET /api/maintenances", h.ListMaintenances) mux.HandleFunc("POST /api/maintenances", h.CreateMaintenance) mux.HandleFunc("PUT /api/maintenances/{id}", h.UpdateMaintenance) mux.HandleFunc("DELETE /api/maintenances/{id}", h.DeleteMaintenance) mux.HandleFunc("GET /api/admin/server-stats", h.ServerStats) mux.Handle("GET /api/stream", stream) // Static file serving — register only if build FS is available if buildFS := static.BuildFS(); buildFS != nil { staticHandler := spaFileServer(buildFS) mux.HandleFunc("/", staticHandler.ServeHTTP) } else { log.Println("[router] nenhum build frontend encontrado — servindo apenas API") // Still register root to give a friendly message mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api") { http.NotFound(w, r) return } w.WriteHeader(http.StatusOK) w.Write([]byte(`YAUM API — frontend build not available. Start the frontend dev server (npm run dev) or set YAUM_STATIC_DIR.`)) }) } return corsMiddleware(auth.Middleware(mux)) } // spaFileServer returns an http.Handler that serves static files and falls // back to index.html (or 200.html) for SPA routes like /admin, /status/123. // Only GET/HEAD requests are handled; other methods return 405. func spaFileServer(fsys fs.FS) http.Handler { fileServer := http.FileServer(http.FS(fsys)) exists := func(name string) bool { f, err := fsys.Open(name) if err != nil { return false } f.Close() return true } // Determine which SPA fallback file exists fallbackFile := "200.html" fallbackBytes, fallbackErr := fs.ReadFile(fsys, fallbackFile) if fallbackErr != nil { fallbackFile = "index.html" fallbackBytes, fallbackErr = fs.ReadFile(fsys, fallbackFile) } if fallbackErr != nil { log.Printf("[router] nenhum fallback SPA (200.html/index.html) encontrado") } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Only serve GET/HEAD for static files if r.Method != http.MethodGet && r.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // API paths should already be handled by the mux, but just in case if strings.HasPrefix(r.URL.Path, "/api") { http.NotFound(w, r) return } p := strings.TrimPrefix(r.URL.Path, "/") // Root path or non-existent file → serve SPA fallback if p == "" || !exists(p) { if fallbackErr == nil { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write(fallbackBytes) return } http.NotFound(w, r) return } fileServer.ServeHTTP(w, r) }) } func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") if origin != "" && isAllowedOrigin(origin) { w.Header().Set("Access-Control-Allow-Origin", origin) w.Header().Set("Vary", "Origin") } else { w.Header().Set("Access-Control-Allow-Origin", "*") } w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Max-Age", "86400") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) return } next.ServeHTTP(w, r) }) } func isAllowedOrigin(origin string) bool { allowed := []string{ "http://localhost:5173", "http://localhost:4173", "http://127.0.0.1:5173", "http://127.0.0.1:4173", } for _, a := range allowed { if strings.EqualFold(origin, a) { return true } } if strings.HasPrefix(origin, "http://localhost:") { return true } return false }