package api import ( "io/fs" "log/slog" "net/http" "strings" "time" "github.com/fonu/fonu/internal/auth" ) func NewRouter(deps Deps) http.Handler { logsHandler := NewLogsHandler(deps.Config) r := &Router{ authHandler: NewAuthHandler(deps.Auth, deps.Notify), proxyHandler: NewProxyHandler(deps.Config, deps.Proxy, logsHandler, deps.Traffic), nginxHandler: NewNginxHandler(deps.Proxy), statusHandler: NewStatusHandler(deps.Config, deps.Proxy, deps.DDNS, deps.ACME, deps.StartedAt, deps.Config.NginxPIDFile), ddnsHandler: NewDDNSHandler(deps.DDNS), certHandler: NewCertHandler(deps.ACME), settingsHandler: NewSettingsHandler(deps.Settings, deps.Proxy, deps.Notify), chinaCIDRHandler: NewChinaCIDRHandler(deps.ChinaCIDR), logsHandler: logsHandler, backupHandler: NewBackupHandler(deps.Backup, deps.Settings), statusPageHandler: NewStatusPageHandler(deps.Settings, deps.FRPMulti), metricsHandler: NewMetricsHandler(deps.Metrics), discoveryHandler: NewDiscoveryHandler(deps.Discovery), frpmultiHandler: NewFRPMultiHandler(deps.FRPMulti), updateHandler: NewUpdateHandler(deps.Updater), authSvc: deps.Auth, staticFS: deps.StaticFS, } mux := http.NewServeMux() mux.HandleFunc("GET /api/version", Version(deps.Config)) // 唯一一个匿名只读接口:默认关闭、可设访问码、按 IP 限流 mux.HandleFunc("GET /api/status-page", r.statusPageHandler.Get) mux.HandleFunc("GET /api/auth/status", r.authHandler.Status) mux.HandleFunc("POST /api/auth/login", r.authHandler.Login) mux.HandleFunc("POST /api/auth/logout", r.authHandler.Logout) protect := func(pattern string, handler http.HandlerFunc) { mux.Handle(pattern, SessionMiddleware(deps.Auth)(RequireAuth(deps.Auth)(http.HandlerFunc(handler)))) } protect("POST /api/auth/password", r.authHandler.ChangePassword) protect("GET /api/settings/tokens", r.authHandler.ListTokens) protect("POST /api/settings/tokens", r.authHandler.CreateToken) protect("DELETE /api/settings/tokens/{id}", r.authHandler.DeleteToken) protect("GET /api/status", r.statusHandler.Get) protect("GET /api/proxies", r.proxyHandler.List) protect("GET /api/proxies/traffic", r.proxyHandler.Traffic) protect("POST /api/proxies", r.proxyHandler.Create) protect("PUT /api/proxies/reorder", r.proxyHandler.Reorder) protect("PUT /api/proxies/{id}", r.proxyHandler.Update) protect("DELETE /api/proxies/{id}", r.proxyHandler.Delete) protect("GET /api/proxies/{id}/logs/stream", r.proxyHandler.StreamLogs) protect("GET /api/proxies/{id}/clients", r.proxyHandler.Clients) protect("GET /api/proxies/{id}/nginx", r.nginxHandler.GetRule) protect("PUT /api/proxies/{id}/nginx", r.nginxHandler.PutRule) protect("POST /api/proxies/{id}/nginx/rollback", r.nginxHandler.RollbackRule) protect("GET /api/settings/nginx/global", r.nginxHandler.GetGlobal) protect("PUT /api/settings/nginx/global", r.nginxHandler.PutGlobal) protect("POST /api/settings/nginx/global/rollback", r.nginxHandler.RollbackGlobal) protect("GET /api/settings/nginx/config/versions", r.nginxHandler.GetConfigVersions) protect("GET /api/settings/nginx/config/versions/{name}", r.nginxHandler.GetConfigVersion) protect("POST /api/settings/nginx/config/versions/{name}/rollback", r.nginxHandler.RollbackConfigVersion) protect("GET /api/ddns", r.ddnsHandler.List) protect("POST /api/ddns", r.ddnsHandler.Create) protect("PUT /api/ddns/{id}", r.ddnsHandler.Update) protect("DELETE /api/ddns/{id}", r.ddnsHandler.Delete) protect("POST /api/ddns/test", r.ddnsHandler.Test) protect("POST /api/ddns/update", r.ddnsHandler.UpdateAll) protect("POST /api/ddns/{id}/update", r.ddnsHandler.UpdateOne) protect("GET /api/certificates", r.certHandler.List) protect("GET /api/certificates/ca-options", r.certHandler.Options) protect("POST /api/certificates/apply", r.certHandler.Apply) protect("GET /api/certificates/jobs/{id}/stream", r.certHandler.ApplyJobStream) protect("POST /api/certificates/import", r.certHandler.Import) protect("POST /api/certificates/renew", r.certHandler.Renew) protect("GET /api/certificates/{domain}/download", r.certHandler.Download) protect("DELETE /api/certificates/{domain}", r.certHandler.Delete) notifyHandler := NewNotifyHandler(deps.Notify) protect("GET /api/settings", r.settingsHandler.Get) protect("PUT /api/settings", r.settingsHandler.Put) protect("POST /api/settings/notify/test", notifyHandler.Test) if deps.ChinaCIDR != nil { protect("GET /api/settings/china-cidr", r.chinaCIDRHandler.Status) protect("POST /api/settings/china-cidr/refresh", r.chinaCIDRHandler.Refresh) } protect("GET /api/logs/access", r.logsHandler.Access) protect("GET /api/logs/error", r.logsHandler.Error) protect("GET /api/logs/system", r.logsHandler.System) protect("GET /api/metrics/history", r.metricsHandler.History) protect("GET /api/logs/stream", r.logsHandler.Stream) protect("GET /api/system/update/status", r.updateHandler.Status) protect("POST /api/system/update/apply", r.updateHandler.Apply) protect("GET /api/backup/export", r.backupHandler.Export) protect("POST /api/backup/restore", r.backupHandler.Restore) protect("GET /api/backup/archives", r.backupHandler.ListArchives) protect("POST /api/backup/archives", r.backupHandler.CreateArchive) protect("GET /api/backup/archives/{name}", r.backupHandler.DownloadArchive) protect("POST /api/backup/archives/{name}/restore", r.backupHandler.RestoreArchive) protect("DELETE /api/backup/archives/{name}", r.backupHandler.DeleteArchive) protect("GET /api/discovery/scan", r.discoveryHandler.Scan) if deps.FRPMulti != nil { frpHandler := r.frpmultiHandler protect("GET /api/frpmulti/status", frpHandler.Status) protect("GET /api/frpmulti/runtime", frpHandler.Runtime) protect("PUT /api/frpmulti/runtime/download-proxy", frpHandler.SetDownloadProxy) protect("GET /api/frpmulti/releases", frpHandler.Releases) protect("GET /api/frpmulti/binaries", frpHandler.Binaries) protect("GET /api/frpmulti/download", frpHandler.DownloadStatus) protect("POST /api/frpmulti/releases/{version}/download", frpHandler.DownloadRelease) protect("POST /api/frpmulti/binaries/{version}/activate", frpHandler.ActivateBinary) protect("DELETE /api/frpmulti/binaries/{version}", frpHandler.DeleteBinary) protect("GET /api/frpmulti/servers", frpHandler.ListServers) protect("GET /api/frpmulti/servers/{id}/detail", frpHandler.ServerDetail) protect("GET /api/frpmulti/servers/{id}/diagnostics", frpHandler.ServerDiagnostics) protect("GET /api/frpmulti/servers/{id}/frps-config", frpHandler.FRPSServerConfig) protect("POST /api/frpmulti/servers/{id}/start", frpHandler.StartServer) protect("POST /api/frpmulti/servers/{id}/stop", frpHandler.StopServer) protect("POST /api/frpmulti/servers", frpHandler.CreateServer) protect("PUT /api/frpmulti/servers/{id}", frpHandler.UpdateServer) protect("DELETE /api/frpmulti/servers/{id}", frpHandler.DeleteServer) protect("POST /api/frpmulti/servers/{id}/test", frpHandler.TestServer) protect("POST /api/frpmulti/servers/{id}/agent/probe", frpHandler.AgentProbe) protect("POST /api/frpmulti/servers/{id}/agent/install", frpHandler.AgentInstall) protect("POST /api/frpmulti/servers/{id}/agent/uninstall", frpHandler.AgentUninstall) protect("POST /api/frpmulti/servers/{id}/agent/ssh-diagnose", frpHandler.AgentSSHDiagnose) protect("POST /api/frpmulti/servers/{id}/agent/test", frpHandler.AgentTest) protect("POST /api/frpmulti/servers/{id}/agent/tunnel/restart", frpHandler.AgentTunnelRestart) protect("POST /api/frpmulti/servers/{id}/agent/transport", frpHandler.SetAgentTransport) protect("GET /api/frpmulti/servers/{id}/agent/status", frpHandler.AgentStatus) protect("GET /api/frpmulti/servers/{id}/agent/routes", frpHandler.AgentRoutes) protect("GET /api/frpmulti/servers/{id}/agent/routes/health", frpHandler.AgentRouteHealth) protect("GET /api/frpmulti/routes/summary", frpHandler.AgentRouteHealthSummary) protect("GET /api/frpmulti/routes/uptime", frpHandler.RouteUptime) protect("GET /api/frpmulti/servers/{id}/agent/routes/drift", frpHandler.AgentRouteDrift) protect("POST /api/frpmulti/servers/{id}/agent/routes/redeploy", frpHandler.AgentRedeployRoutes) protect("GET /api/frpmulti/servers/{id}/agent/metrics", frpHandler.AgentMetrics) protect("GET /api/frpmulti/servers/{id}/agent/logs/nginx", frpHandler.AgentNginxLogs) protect("GET /api/frpmulti/servers/{id}/agent/frps-config", frpHandler.AgentFRPSConfig) protect("GET /api/frpmulti/servers/{id}/agent/certs", frpHandler.AgentCerts) protect("POST /api/frpmulti/servers/{id}/agent/nginx/reload", frpHandler.AgentNginxReload) protect("POST /api/frpmulti/servers/{id}/agent/nginx/install", frpHandler.AgentInstallNginx) protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/conf", frpHandler.AgentRouteConf) protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions", frpHandler.AgentRouteVersions) protect("GET /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions/{name}", frpHandler.AgentRouteVersion) protect("POST /api/frpmulti/servers/{id}/agent/routes/{proxyID}/versions/{name}/rollback", frpHandler.AgentRollbackRouteVersion) protect("GET /api/frpmulti/servers/{id}/agent/route-candidates", frpHandler.AgentRouteCandidates) protect("POST /api/frpmulti/servers/{id}/agent/routes/{proxyID}", frpHandler.DeployAgentRoute) protect("DELETE /api/frpmulti/servers/{id}/agent/routes/{proxyID}", frpHandler.RemoveAgentRoute) protect("POST /api/frpmulti/servers/{id}/agent/frps-install", frpHandler.InstallFRPSOnAgent) protect("POST /api/frpmulti/servers/{id}/agent/frps-action", frpHandler.AgentFRPSAction) protect("POST /api/frpmulti/servers/{id}/agent/frps-config", frpHandler.PushFRPSConfigToAgent) protect("GET /api/frpmulti/proxies", frpHandler.ListProxies) protect("GET /api/frpmulti/proxies/traffic", frpHandler.ListProxiesTraffic) protect("POST /api/frpmulti/proxies", frpHandler.CreateProxy) protect("PUT /api/frpmulti/proxies/{id}", frpHandler.UpdateProxy) protect("DELETE /api/frpmulti/proxies/{id}", frpHandler.DeleteProxy) protect("GET /api/frpmulti/proxies/{id}/logs", frpHandler.ProxyLogs) protect("POST /api/frpmulti/start", frpHandler.Start) protect("POST /api/frpmulti/stop", frpHandler.Stop) protect("POST /api/frpmulti/reload", frpHandler.Reload) protect("GET /api/frpmulti/logs", frpHandler.Logs) protect("GET /api/frpmulti/config/export", frpHandler.ExportConfig) } if deps.StaticFS != nil { mux.Handle("/", r.spaHandler()) } logger := deps.Logger if logger == nil { logger = slog.Default() } return loggingMiddleware(logger, SessionMiddleware(deps.Auth)(mux)) } type Router struct { authHandler *AuthHandler proxyHandler *ProxyHandler nginxHandler *NginxHandler statusHandler *StatusHandler ddnsHandler *DDNSHandler certHandler *CertHandler settingsHandler *SettingsHandler chinaCIDRHandler *ChinaCIDRHandler logsHandler *LogsHandler backupHandler *BackupHandler statusPageHandler *StatusPageHandler metricsHandler *MetricsHandler discoveryHandler *DiscoveryHandler frpmultiHandler *FRPMultiHandler updateHandler *UpdateHandler authSvc *auth.Service staticFS fs.FS } func (r *Router) spaHandler() http.Handler { fileServer := http.FileServer(http.FS(r.staticFS)) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { if strings.HasPrefix(req.URL.Path, "/api/") { http.NotFound(w, req) return } path := strings.TrimPrefix(req.URL.Path, "/") if path == "" { path = "index.html" } if _, err := fs.Stat(r.staticFS, path); err != nil { // SPA fallback: unknown routes serve index.html req.URL.Path = "/index.html" } fileServer.ServeHTTP(w, req) }) } type statusRecorder struct { http.ResponseWriter status int } func (w *statusRecorder) WriteHeader(code int) { w.status = code w.ResponseWriter.WriteHeader(code) } func (w *statusRecorder) Flush() { if f, ok := w.ResponseWriter.(http.Flusher); ok { f.Flush() } } func loggingMiddleware(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(rec, r) if strings.HasPrefix(r.URL.Path, "/api/") && !strings.Contains(r.URL.Path, "/logs/stream") && !strings.Contains(r.URL.Path, "/certificates/jobs/") && rec.status >= 400 { level := slog.LevelWarn msg := "api request completed with client error" if rec.status >= 500 { level = slog.LevelError msg = "api request failed" } logger.Log(r.Context(), level, msg, "module", "HTTP", "method", r.Method, "path", r.URL.Path, "status", rec.status, "duration_ms", time.Since(start).Milliseconds(), ) } }) }