package api import ( "context" "encoding/json" "fmt" "net/http" "time" "git.warky.dev/wdevs/whatshooked/pkg/config" "git.warky.dev/wdevs/whatshooked/pkg/handlers" "git.warky.dev/wdevs/whatshooked/pkg/models" "git.warky.dev/wdevs/whatshooked/pkg/storage" "github.com/bitechdev/ResolveSpec/pkg/common" "github.com/bitechdev/ResolveSpec/pkg/common/adapters/database" "github.com/bitechdev/ResolveSpec/pkg/modelregistry" "github.com/bitechdev/ResolveSpec/pkg/restheadspec" "github.com/bitechdev/ResolveSpec/pkg/security" "github.com/bitechdev/ResolveSpec/pkg/server" "github.com/gorilla/mux" "github.com/uptrace/bun" ) // WhatsHookedInterface defines the interface for accessing WhatsHooked components type WhatsHookedInterface interface { Handlers() *handlers.Handlers } // Server represents the API server type Server struct { serverMgr server.Manager handler *restheadspec.Handler secProvider security.SecurityProvider db *bun.DB config *config.Config wh WhatsHookedInterface } // NewServer creates a new API server with ResolveSpec integration func NewServer(cfg *config.Config, db *bun.DB, wh WhatsHookedInterface) (*Server, error) { // Create model registry and register models registry := modelregistry.NewModelRegistry() registerModelsToRegistry(registry) // Create BUN adapter bunAdapter := database.NewBunAdapter(db) // Create ResolveSpec handler with registry handler := restheadspec.NewHandler(bunAdapter, registry) // Create security provider secProvider := NewSecurityProvider(cfg.Server.JWTSecret, db, cfg) // Create security list and register hooks securityList, err := security.NewSecurityList(secProvider) if err != nil { return nil, fmt.Errorf("failed to create security list: %w", err) } restheadspec.RegisterSecurityHooks(handler, securityList) // Create router router := mux.NewRouter() // Create a subrouter for /api/v1/* routes that need JWT authentication apiV1Router := router.PathPrefix("/api/v1").Subrouter() apiV1Router.Use(security.NewAuthMiddleware(securityList)) apiV1Router.Use(security.SetSecurityMiddleware(securityList)) // Setup WhatsApp API routes on main router (these use their own Auth middleware) SetupWhatsAppRoutes(router, wh) // Setup ResolveSpec routes on the protected /api/v1 subrouter (auto-generated CRUD) restheadspec.SetupMuxRoutes(apiV1Router, handler, nil) // Add custom routes (login, logout, etc.) on main router SetupCustomRoutes(router, secProvider, db) // Add static file serving for React app (must be last - catch-all route) // Serve React app from web/dist directory spa := spaHandler{staticPath: "web/dist", indexPath: "index.html"} router.PathPrefix("/").Handler(spa) // Create server manager serverMgr := server.NewManager() // Add HTTP server _, err = serverMgr.Add(server.Config{ Name: "whatshooked", Host: cfg.Server.Host, Port: cfg.Server.Port, Handler: router, GZIP: true, ShutdownTimeout: 30 * time.Second, DrainTimeout: 25 * time.Second, }) if err != nil { return nil, fmt.Errorf("failed to add server: %w", err) } // Register shutdown callback for database serverMgr.RegisterShutdownCallback(func(ctx context.Context) error { return storage.Close() }) return &Server{ serverMgr: serverMgr, handler: handler, secProvider: secProvider, db: db, config: cfg, wh: wh, }, nil } // Start starts the API server func (s *Server) Start() error { return s.serverMgr.ServeWithGracefulShutdown() } // registerModelsToRegistry registers all BUN models with the model registry func registerModelsToRegistry(registry common.ModelRegistry) { // Register all models with their table names (without schema for SQLite compatibility) registry.RegisterModel("users", &models.ModelPublicUser{}) registry.RegisterModel("api_keys", &models.ModelPublicAPIKey{}) registry.RegisterModel("hooks", &models.ModelPublicHook{}) registry.RegisterModel("whatsapp_accounts", &models.ModelPublicWhatsappAccount{}) registry.RegisterModel("event_logs", &models.ModelPublicEventLog{}) registry.RegisterModel("sessions", &models.ModelPublicSession{}) registry.RegisterModel("message_cache", &models.ModelPublicMessageCache{}) } // SetupWhatsAppRoutes adds all WhatsApp API routes func SetupWhatsAppRoutes(router *mux.Router, wh WhatsHookedInterface) { h := wh.Handlers() // Landing page (no auth required) router.HandleFunc("/", h.ServeIndex).Methods("GET") // Privacy policy and terms of service (no auth required) router.HandleFunc("/privacy-policy", h.ServePrivacyPolicy).Methods("GET") router.HandleFunc("/terms-of-service", h.ServeTermsOfService).Methods("GET") // Static files (no auth required) router.PathPrefix("/static/").HandlerFunc(h.ServeStatic) // Health check (no auth required) router.HandleFunc("/health", h.Health).Methods("GET") // Hook management (with auth) router.HandleFunc("/api/hooks", h.Auth(h.Hooks)) router.HandleFunc("/api/hooks/add", h.Auth(h.AddHook)).Methods("POST") router.HandleFunc("/api/hooks/remove", h.Auth(h.RemoveHook)).Methods("POST") // Account management (with auth) router.HandleFunc("/api/accounts", h.Auth(h.Accounts)) router.HandleFunc("/api/accounts/add", h.Auth(h.AddAccount)).Methods("POST") router.HandleFunc("/api/accounts/update", h.Auth(h.UpdateAccount)).Methods("POST") router.HandleFunc("/api/accounts/remove", h.Auth(h.RemoveAccount)).Methods("POST") router.HandleFunc("/api/accounts/disable", h.Auth(h.DisableAccount)).Methods("POST") router.HandleFunc("/api/accounts/enable", h.Auth(h.EnableAccount)).Methods("POST") // Send messages (with auth) router.HandleFunc("/api/send", h.Auth(h.SendMessage)).Methods("POST") router.HandleFunc("/api/send/image", h.Auth(h.SendImage)).Methods("POST") router.HandleFunc("/api/send/video", h.Auth(h.SendVideo)).Methods("POST") router.HandleFunc("/api/send/document", h.Auth(h.SendDocument)).Methods("POST") router.HandleFunc("/api/send/audio", h.Auth(h.SendAudio)).Methods("POST") router.HandleFunc("/api/send/sticker", h.Auth(h.SendSticker)).Methods("POST") router.HandleFunc("/api/send/location", h.Auth(h.SendLocation)).Methods("POST") router.HandleFunc("/api/send/contacts", h.Auth(h.SendContacts)).Methods("POST") router.HandleFunc("/api/send/interactive", h.Auth(h.SendInteractive)).Methods("POST") router.HandleFunc("/api/send/template", h.Auth(h.SendTemplate)).Methods("POST") router.HandleFunc("/api/send/flow", h.Auth(h.SendFlow)).Methods("POST") router.HandleFunc("/api/send/reaction", h.Auth(h.SendReaction)).Methods("POST") router.HandleFunc("/api/send/catalog", h.Auth(h.SendCatalogMessage)).Methods("POST") router.HandleFunc("/api/send/product", h.Auth(h.SendSingleProduct)).Methods("POST") router.HandleFunc("/api/send/product-list", h.Auth(h.SendProductList)).Methods("POST") // Message operations (with auth) router.HandleFunc("/api/messages/read", h.Auth(h.MarkAsRead)).Methods("POST") // Serve media files (with auth) router.PathPrefix("/api/media/").HandlerFunc(h.ServeMedia) // Serve QR codes (no auth - needed during pairing) router.PathPrefix("/api/qr/").HandlerFunc(h.ServeQRCode) // Business API webhooks (no auth - Meta validates via verify_token) router.PathPrefix("/webhooks/whatsapp/").HandlerFunc(h.BusinessAPIWebhook) // Template management (with auth) router.HandleFunc("/api/templates", h.Auth(h.ListTemplates)) router.HandleFunc("/api/templates/upload", h.Auth(h.UploadTemplate)).Methods("POST") router.HandleFunc("/api/templates/delete", h.Auth(h.DeleteTemplate)).Methods("POST") // Flow management (with auth) router.HandleFunc("/api/flows", h.Auth(h.ListFlows)) router.HandleFunc("/api/flows/create", h.Auth(h.CreateFlow)).Methods("POST") router.HandleFunc("/api/flows/get", h.Auth(h.GetFlow)) router.HandleFunc("/api/flows/upload", h.Auth(h.UploadFlowAsset)).Methods("POST") router.HandleFunc("/api/flows/publish", h.Auth(h.PublishFlow)).Methods("POST") router.HandleFunc("/api/flows/deprecate", h.Auth(h.DeprecateFlow)).Methods("POST") router.HandleFunc("/api/flows/delete", h.Auth(h.DeleteFlow)).Methods("POST") // Phone number management (with auth) router.HandleFunc("/api/phone-numbers", h.Auth(h.ListPhoneNumbers)) router.HandleFunc("/api/phone-numbers/request-code", h.Auth(h.RequestVerificationCode)).Methods("POST") router.HandleFunc("/api/phone-numbers/verify-code", h.Auth(h.VerifyCode)).Methods("POST") // Media management (with auth) router.HandleFunc("/api/media/upload", h.Auth(h.UploadMedia)).Methods("POST") router.HandleFunc("/api/media-delete", h.Auth(h.DeleteMediaFile)).Methods("POST") // Business profile (with auth) router.HandleFunc("/api/business-profile", h.Auth(h.GetBusinessProfile)) router.HandleFunc("/api/business-profile/update", h.Auth(h.UpdateBusinessProfile)).Methods("POST") // Catalog / commerce (with auth) router.HandleFunc("/api/catalogs", h.Auth(h.ListCatalogs)) router.HandleFunc("/api/catalogs/products", h.Auth(h.ListProducts)) // Message cache management (with auth) router.HandleFunc("/api/cache", h.Auth(h.GetCachedEvents)).Methods("GET") router.HandleFunc("/api/cache/stats", h.Auth(h.GetCacheStats)).Methods("GET") router.HandleFunc("/api/cache/replay", h.Auth(h.ReplayCachedEvents)).Methods("POST") router.HandleFunc("/api/cache/event", h.Auth(h.GetCachedEvent)).Methods("GET") router.HandleFunc("/api/cache/event/replay", h.Auth(h.ReplayCachedEvent)).Methods("POST") router.HandleFunc("/api/cache/event/delete", h.Auth(h.DeleteCachedEvent)).Methods("DELETE") router.HandleFunc("/api/cache/clear", h.Auth(h.ClearCache)).Methods("DELETE") } // SetupCustomRoutes adds custom authentication and management routes func SetupCustomRoutes(router *mux.Router, secProvider security.SecurityProvider, db *bun.DB) { // Health check endpoint router.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"healthy"}`)) }).Methods("GET") // Login endpoint router.HandleFunc("/api/v1/auth/login", func(w http.ResponseWriter, r *http.Request) { handleLogin(w, r, secProvider) }).Methods("POST") // Logout endpoint router.HandleFunc("/api/v1/auth/logout", func(w http.ResponseWriter, r *http.Request) { handleLogout(w, r, secProvider) }).Methods("POST") } // handleLogin handles user login func handleLogin(w http.ResponseWriter, r *http.Request, secProvider security.SecurityProvider) { var req security.LoginRequest if err := parseJSON(r, &req); err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } resp, err := secProvider.Login(r.Context(), req) if err != nil { http.Error(w, "Invalid credentials", http.StatusUnauthorized) return } writeJSON(w, http.StatusOK, resp) } // handleLogout handles user logout func handleLogout(w http.ResponseWriter, r *http.Request, secProvider security.SecurityProvider) { token := extractToken(r) if token == "" { http.Error(w, "No token provided", http.StatusBadRequest) return } req := security.LogoutRequest{Token: token} if err := secProvider.Logout(r.Context(), req); err != nil { http.Error(w, "Logout failed", http.StatusInternalServerError) return } writeJSON(w, http.StatusOK, map[string]string{"message": "Logged out successfully"}) } // Helper functions func parseJSON(r *http.Request, v interface{}) error { decoder := json.NewDecoder(r.Body) return decoder.Decode(v) } func writeJSON(w http.ResponseWriter, status int, v interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func extractToken(r *http.Request) string { // Extract from Authorization header: "Bearer " auth := r.Header.Get("Authorization") if len(auth) > 7 && auth[:7] == "Bearer " { return auth[7:] } return "" } // spaHandler implements the http.Handler interface for serving a SPA type spaHandler struct { staticPath string indexPath string } // ServeHTTP inspects the URL path to locate a file within the static dir // If a file is found, it is served. If not, the index.html file is served for client-side routing func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Get the path path := r.URL.Path // Check whether a file exists at the given path info, err := http.Dir(h.staticPath).Open(path) if err != nil { // File does not exist, serve index.html for client-side routing http.ServeFile(w, r, h.staticPath+"/"+h.indexPath) return } defer info.Close() // Check if path is a directory stat, err := info.Stat() if err != nil { http.ServeFile(w, r, h.staticPath+"/"+h.indexPath) return } if stat.IsDir() { // Serve index.html for directories http.ServeFile(w, r, h.staticPath+"/"+h.indexPath) return } // Otherwise, serve the file http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) }