package api import ( "net/http" "vpnem/internal/rules" ) func NewRouter(store *rules.Store) http.Handler { h := NewHandler(store) mux := http.NewServeMux() mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"status":"ok"}`)) }) mux.HandleFunc("/vpnui", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } http.Redirect(w, r, "/vpnui/", http.StatusTemporaryRedirect) }) mux.HandleFunc("/vpnui/", methodHandler(http.MethodGet, h.VPNUI)) mux.HandleFunc("/api/v1/servers", methodHandler(http.MethodGet, h.Servers)) mux.HandleFunc("/api/v2/catalog", methodHandler(http.MethodGet, h.CatalogV2)) mux.HandleFunc("/api/v1/routing-policy", methodHandler(http.MethodGet, h.RoutingPolicy)) mux.HandleFunc("/api/v1/subscribe", methodHandler(http.MethodGet, h.Subscribe)) mux.HandleFunc("/api/v1/ruleset/manifest", methodHandler(http.MethodGet, h.RuleSetManifest)) mux.HandleFunc("/api/v1/version", methodHandler(http.MethodGet, h.Version)) mux.HandleFunc("/api/v1/control/nodes", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: h.ControlNodes(w, r) case http.MethodPost: h.UpsertControlNode(w, r) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } }) mux.HandleFunc("/api/v1/control/preflight", methodHandler(http.MethodPost, h.QuickPreflightControlNode)) mux.HandleFunc("/api/v1/control/quick-provision", methodHandler(http.MethodPost, h.QuickProvisionControlNode)) mux.HandleFunc("/api/v1/control/nodes/", h.ControlNodeAction) mux.HandleFunc("/api/v1/control/catalog/publish", methodHandler(http.MethodPost, h.PublishControlCatalog)) // Static file serving for .srs and .txt rule files rulesFS := http.StripPrefix("/rules/", http.FileServer(http.Dir(store.RulesDir()))) mux.Handle("/rules/", rulesFS) // Static file serving for client releases releasesFS := http.StripPrefix("/releases/", http.FileServer(http.Dir(store.ReleasesDir()))) mux.Handle("/releases/", releasesFS) // Client error log endpoint (obscure URL, no auth needed — just writes to file) mux.HandleFunc("/logs2026vpnem/errors", methodHandler(http.MethodPost, h.ClientLog)) // Web viewer for client logs (admin-protected via env var) mux.HandleFunc("/client-logs", methodHandler(http.MethodGet, h.ClientLogsViewer)) // Client connection report and recommendation (RealIP middleware auto-detects client IP) mux.HandleFunc("/api/v1/connect", RealIP(h.ClientConnect)) mux.HandleFunc("/api/v1/disconnect", RealIP(h.ClientDisconnect)) mux.HandleFunc("/api/v1/recommend", RealIP(h.Recommend)) return mux } func methodHandler(method string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != method { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } next(w, r) } }