* Update user-related models to use plural naming for consistency * Add relationships to ModelPublicUsers in related models * Adjust database migration and schema to reflect changes * Remove deprecated ModelPublicUser
678 lines
23 KiB
Go
678 lines
23 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"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/serverembed"
|
|
"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()
|
|
|
|
// Add CORS middleware to all routes
|
|
router.Use(corsMiddleware)
|
|
|
|
// 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))
|
|
|
|
// Create the embedded dist FS (built Vite output, includes web/public/ contents)
|
|
distFS, err := fs.Sub(serverembed.RootEmbedFS, "dist")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to sub embedded dist FS: %w", err)
|
|
}
|
|
|
|
// Setup WhatsApp API routes on main router (these use their own Auth middleware)
|
|
SetupWhatsAppRoutes(router, wh, distFS)
|
|
|
|
// 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)
|
|
|
|
// Serve React SPA from the embedded filesystem at /ui/
|
|
spa := embeddedSPAHandler{fs: distFS, indexPath: "index.html"}
|
|
router.PathPrefix("/ui/").Handler(http.StripPrefix("/ui", spa))
|
|
router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", 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.ModelPublicUsers{})
|
|
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, distFS fs.FS) {
|
|
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")
|
|
|
|
// Logo files served from the embedded pkg/handlers/static/ (referenced by index.html)
|
|
router.HandleFunc("/static/logo.png", h.ServeStatic)
|
|
router.HandleFunc("/static/logo1024.png", h.ServeStatic)
|
|
|
|
// Everything else under /static/ is served from the built web/public/ directory
|
|
router.PathPrefix("/static/").Handler(http.StripPrefix("/static", http.FileServer(http.FS(distFS))))
|
|
|
|
// 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", "OPTIONS")
|
|
|
|
// Logout endpoint
|
|
router.HandleFunc("/api/v1/auth/logout", func(w http.ResponseWriter, r *http.Request) {
|
|
handleLogout(w, r, secProvider)
|
|
}).Methods("POST", "OPTIONS")
|
|
|
|
// Unified query endpoint for ResolveSpec-style queries
|
|
router.HandleFunc("/api/v1/query", func(w http.ResponseWriter, r *http.Request) {
|
|
handleQuery(w, r, db, secProvider)
|
|
}).Methods("POST", "OPTIONS")
|
|
}
|
|
|
|
// 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"})
|
|
}
|
|
|
|
// QueryRequest represents a unified query request
|
|
type QueryRequest struct {
|
|
Action string `json:"action"` // "list", "get", "create", "update", "delete"
|
|
Table string `json:"table"` // Table name (e.g., "users", "hooks")
|
|
ID string `json:"id,omitempty"` // For get/update/delete
|
|
Data map[string]interface{} `json:"data,omitempty"` // For create/update
|
|
Filters map[string]interface{} `json:"filters,omitempty"` // For list
|
|
Limit int `json:"limit,omitempty"`
|
|
Offset int `json:"offset,omitempty"`
|
|
}
|
|
|
|
// handleQuery handles unified query requests
|
|
func handleQuery(w http.ResponseWriter, r *http.Request, db *bun.DB, secProvider security.SecurityProvider) {
|
|
// Authenticate request
|
|
userCtx, err := secProvider.Authenticate(r)
|
|
if err != nil {
|
|
http.Error(w, "Authentication required", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
var req QueryRequest
|
|
if err := parseJSON(r, &req); err != nil {
|
|
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Execute query based on action
|
|
switch req.Action {
|
|
case "list":
|
|
handleQueryList(w, r, db, req, userCtx)
|
|
case "get":
|
|
handleQueryGet(w, r, db, req, userCtx)
|
|
case "create":
|
|
handleQueryCreate(w, r, db, req, userCtx)
|
|
case "update":
|
|
handleQueryUpdate(w, r, db, req, userCtx)
|
|
case "delete":
|
|
handleQueryDelete(w, r, db, req, userCtx)
|
|
default:
|
|
http.Error(w, "Invalid action", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
// handleQueryList lists records from a table
|
|
func handleQueryList(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
|
|
// Get model registry to find the model
|
|
registry := getModelForTable(req.Table)
|
|
if registry == nil {
|
|
http.Error(w, "Table not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Create slice to hold results
|
|
results := registry()
|
|
|
|
// Build query
|
|
query := db.NewSelect().Model(results)
|
|
|
|
// Apply filters
|
|
for key, value := range req.Filters {
|
|
query = query.Where("? = ?", bun.Ident(key), value)
|
|
}
|
|
|
|
// Apply limit/offset
|
|
if req.Limit > 0 {
|
|
query = query.Limit(req.Limit)
|
|
}
|
|
if req.Offset > 0 {
|
|
query = query.Offset(req.Offset)
|
|
}
|
|
|
|
// Execute query
|
|
if err := query.Scan(r.Context()); err != nil {
|
|
http.Error(w, fmt.Sprintf("Query failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, results)
|
|
}
|
|
|
|
// handleQueryGet retrieves a single record by ID
|
|
func handleQueryGet(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
|
|
model := getModelSingleForTable(req.Table)
|
|
if model == nil {
|
|
http.Error(w, "Table not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
err := db.NewSelect().Model(model).Where("id = ?", req.ID).Scan(r.Context())
|
|
if err != nil {
|
|
http.Error(w, "Record not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, model)
|
|
}
|
|
|
|
// handleQueryCreate creates a new record
|
|
func handleQueryCreate(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
|
|
model := getModelSingleForTable(req.Table)
|
|
if model == nil {
|
|
http.Error(w, "Table not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Initialize data map if needed
|
|
if req.Data == nil {
|
|
req.Data = make(map[string]interface{})
|
|
}
|
|
|
|
// Auto-generate UUID for id field if not provided
|
|
generatedID := ""
|
|
if _, exists := req.Data["id"]; !exists {
|
|
generatedID = uuid.New().String()
|
|
req.Data["id"] = generatedID
|
|
}
|
|
|
|
// Auto-inject user_id for tables that need it
|
|
if userCtx != nil && userCtx.Claims != nil {
|
|
// Get user_id from claims (it's stored as UUID string)
|
|
if userIDClaim, ok := userCtx.Claims["user_id"]; ok {
|
|
if userID, ok := userIDClaim.(string); ok && userID != "" {
|
|
// Add user_id to data if the table requires it and it's not already set
|
|
tablesWithUserID := map[string]bool{
|
|
"hooks": true,
|
|
"whatsapp_accounts": true,
|
|
"api_keys": true,
|
|
"event_logs": true,
|
|
}
|
|
|
|
if tablesWithUserID[req.Table] {
|
|
// Only set user_id if not already provided
|
|
if _, exists := req.Data["user_id"]; !exists {
|
|
req.Data["user_id"] = userID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Auto-generate session_path for WhatsApp accounts if not provided
|
|
if req.Table == "whatsapp_accounts" {
|
|
// Set session_path if not already provided
|
|
if _, exists := req.Data["session_path"]; !exists {
|
|
// Use account_id if provided, otherwise use generated id
|
|
sessionID := ""
|
|
if accountID, ok := req.Data["account_id"].(string); ok && accountID != "" {
|
|
sessionID = accountID
|
|
} else if generatedID != "" {
|
|
sessionID = generatedID
|
|
} else if id, ok := req.Data["id"].(string); ok && id != "" {
|
|
sessionID = id
|
|
}
|
|
if sessionID != "" {
|
|
req.Data["session_path"] = fmt.Sprintf("./sessions/%s", sessionID)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert data map to model using JSON marshaling
|
|
dataJSON, err := json.Marshal(req.Data)
|
|
if err != nil {
|
|
http.Error(w, "Invalid data", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := json.Unmarshal(dataJSON, model); err != nil {
|
|
http.Error(w, "Invalid data format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Ensure ID is set after unmarshaling by using model-specific handling
|
|
if generatedID != "" {
|
|
switch m := model.(type) {
|
|
case *models.ModelPublicWhatsappAccount:
|
|
m.ID.FromString(generatedID)
|
|
case *models.ModelPublicHook:
|
|
m.ID.FromString(generatedID)
|
|
case *models.ModelPublicAPIKey:
|
|
m.ID.FromString(generatedID)
|
|
case *models.ModelPublicEventLog:
|
|
m.ID.FromString(generatedID)
|
|
case *models.ModelPublicUsers:
|
|
m.ID.FromString(generatedID)
|
|
}
|
|
}
|
|
|
|
// Insert into database
|
|
_, err = db.NewInsert().Model(model).Exec(r.Context())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Create failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, model)
|
|
}
|
|
|
|
// handleQueryUpdate updates an existing record
|
|
func handleQueryUpdate(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
|
|
model := getModelSingleForTable(req.Table)
|
|
if model == nil {
|
|
http.Error(w, "Table not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Convert data map to model
|
|
dataJSON, err := json.Marshal(req.Data)
|
|
if err != nil {
|
|
http.Error(w, "Invalid data", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := json.Unmarshal(dataJSON, model); err != nil {
|
|
http.Error(w, "Invalid data format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Update in database
|
|
_, err = db.NewUpdate().Model(model).Where("id = ?", req.ID).Exec(r.Context())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Update failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, model)
|
|
}
|
|
|
|
// handleQueryDelete deletes a record
|
|
func handleQueryDelete(w http.ResponseWriter, r *http.Request, db *bun.DB, req QueryRequest, userCtx *security.UserContext) {
|
|
model := getModelSingleForTable(req.Table)
|
|
if model == nil {
|
|
http.Error(w, "Table not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
_, err := db.NewDelete().Model(model).Where("id = ?", req.ID).Exec(r.Context())
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Delete failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"message": "Deleted successfully"})
|
|
}
|
|
|
|
// getModelForTable returns a function that creates a slice of models for the table
|
|
func getModelForTable(table string) func() interface{} {
|
|
switch table {
|
|
case "users":
|
|
return func() interface{} { return &[]models.ModelPublicUsers{} }
|
|
case "hooks":
|
|
return func() interface{} { return &[]models.ModelPublicHook{} }
|
|
case "whatsapp_accounts":
|
|
return func() interface{} { return &[]models.ModelPublicWhatsappAccount{} }
|
|
case "event_logs":
|
|
return func() interface{} { return &[]models.ModelPublicEventLog{} }
|
|
case "api_keys":
|
|
return func() interface{} { return &[]models.ModelPublicAPIKey{} }
|
|
case "sessions":
|
|
return func() interface{} { return &[]models.ModelPublicSession{} }
|
|
case "message_cache":
|
|
return func() interface{} { return &[]models.ModelPublicMessageCache{} }
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// getModelSingleForTable returns a single model instance for the table
|
|
func getModelSingleForTable(table string) interface{} {
|
|
switch table {
|
|
case "users":
|
|
return &models.ModelPublicUsers{}
|
|
case "hooks":
|
|
return &models.ModelPublicHook{}
|
|
case "whatsapp_accounts":
|
|
return &models.ModelPublicWhatsappAccount{}
|
|
case "event_logs":
|
|
return &models.ModelPublicEventLog{}
|
|
case "api_keys":
|
|
return &models.ModelPublicAPIKey{}
|
|
case "sessions":
|
|
return &models.ModelPublicSession{}
|
|
case "message_cache":
|
|
return &models.ModelPublicMessageCache{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// 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 <token>"
|
|
auth := r.Header.Get("Authorization")
|
|
if len(auth) > 7 && auth[:7] == "Bearer " {
|
|
return auth[7:]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// embeddedSPAHandler serves a React SPA from an embedded fs.FS.
|
|
// Static assets are served directly; all other paths fall back to index.html
|
|
// to support client-side routing.
|
|
type embeddedSPAHandler struct {
|
|
fs fs.FS
|
|
indexPath string
|
|
}
|
|
|
|
func (h embeddedSPAHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
path := r.URL.Path
|
|
// Strip leading slash so fs.FS Open calls work correctly.
|
|
if len(path) > 0 && path[0] == '/' {
|
|
path = path[1:]
|
|
}
|
|
|
|
if path == "" {
|
|
path = h.indexPath
|
|
}
|
|
|
|
// Try to open the requested path in the embedded FS.
|
|
f, err := h.fs.Open(path)
|
|
if err != nil {
|
|
// Not found — serve index.html for client-side routing.
|
|
r2 := r.Clone(r.Context())
|
|
r2.URL.Path = "/" + h.indexPath
|
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
stat, err := f.Stat()
|
|
if err != nil || stat.IsDir() {
|
|
r2 := r.Clone(r.Context())
|
|
r2.URL.Path = "/" + h.indexPath
|
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r2)
|
|
return
|
|
}
|
|
|
|
// Serve the real file.
|
|
http.FileServer(http.FS(h.fs)).ServeHTTP(w, r)
|
|
}
|
|
|
|
// corsMiddleware adds CORS headers to allow frontend requests
|
|
func corsMiddleware(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Allow requests from any origin (adjust in production)
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
|
w.Header().Set("Access-Control-Max-Age", "86400")
|
|
|
|
// Handle preflight requests
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|