refactor(API): ✨ Relspect integration
This commit is contained in:
271
pkg/api/security.go
Normal file
271
pkg/api/security.go
Normal file
@@ -0,0 +1,271 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/uptrace/bun"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// SecurityProvider implements ResolveSpec SecurityProvider interface
|
||||
type SecurityProvider struct {
|
||||
jwtSecret []byte
|
||||
userRepo *storage.UserRepository
|
||||
sessionRepo *storage.SessionRepository
|
||||
config *config.Config // Add config for Phase 1 auth
|
||||
}
|
||||
|
||||
// NewSecurityProvider creates a new security provider
|
||||
func NewSecurityProvider(jwtSecret string, db *bun.DB, cfg *config.Config) security.SecurityProvider {
|
||||
return &SecurityProvider{
|
||||
jwtSecret: []byte(jwtSecret),
|
||||
userRepo: storage.NewUserRepository(db),
|
||||
sessionRepo: storage.NewSessionRepository(db),
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Claims represents JWT claims
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken generates a JWT token
|
||||
func (sp *SecurityProvider) GenerateToken(userID int, username, role string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "whatshooked",
|
||||
Subject: fmt.Sprintf("%d", userID),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(sp.jwtSecret)
|
||||
}
|
||||
|
||||
// ValidateToken validates a JWT token and returns the claims
|
||||
func (sp *SecurityProvider) ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return sp.jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// Login authenticates a user (implements security.Authenticator)
|
||||
func (sp *SecurityProvider) Login(ctx context.Context, req security.LoginRequest) (*security.LoginResponse, error) {
|
||||
// Get user by username
|
||||
user, err := sp.userRepo.GetByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.Active {
|
||||
return nil, fmt.Errorf("user is inactive")
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password.String()), []byte(req.Password)); err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := sp.GenerateToken(int(user.ID.Int64()), req.Username, user.Role.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate token: %w", err)
|
||||
}
|
||||
|
||||
// Build user context
|
||||
userCtx := &security.UserContext{
|
||||
UserID: int(user.ID.Int64()),
|
||||
UserName: req.Username,
|
||||
Email: user.Email.String(),
|
||||
Roles: []string{user.Role.String()},
|
||||
Claims: map[string]any{
|
||||
"role": user.Role.String(),
|
||||
},
|
||||
}
|
||||
|
||||
return &security.LoginResponse{
|
||||
Token: token,
|
||||
User: userCtx,
|
||||
ExpiresIn: int64(24 * time.Hour.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Logout logs out a user (implements security.Authenticator)
|
||||
func (sp *SecurityProvider) Logout(ctx context.Context, req security.LogoutRequest) error {
|
||||
// For JWT, we can implement token blacklisting if needed
|
||||
// For now, just return success (JWT will expire naturally)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate authenticates an HTTP request (implements security.Authenticator)
|
||||
func (sp *SecurityProvider) Authenticate(r *http.Request) (*security.UserContext, error) {
|
||||
// Try JWT authentication first
|
||||
token := extractBearerToken(r)
|
||||
if token != "" {
|
||||
claims, err := sp.ValidateToken(token)
|
||||
if err == nil {
|
||||
// Get user from database
|
||||
user, err := sp.userRepo.GetByID(r.Context(), fmt.Sprintf("%d", claims.UserID))
|
||||
if err == nil && user.Active {
|
||||
return &security.UserContext{
|
||||
UserID: claims.UserID,
|
||||
UserName: claims.Username,
|
||||
Email: user.Email.String(),
|
||||
Roles: []string{user.Role.String()},
|
||||
Claims: map[string]any{
|
||||
"role": user.Role.String(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try Phase 1 authentication (API key, basic auth)
|
||||
if sp.validatePhase1Auth(r) {
|
||||
// Create a generic user context for Phase 1 auth
|
||||
// Use username from config or "api-user" if using API key
|
||||
username := "api-user"
|
||||
if sp.config.Server.Username != "" {
|
||||
username = sp.config.Server.Username
|
||||
}
|
||||
|
||||
// Check if using basic auth to get actual username
|
||||
if basicUser, _, ok := r.BasicAuth(); ok && basicUser != "" {
|
||||
username = basicUser
|
||||
}
|
||||
|
||||
return &security.UserContext{
|
||||
UserID: 0, // No user ID for Phase 1 auth
|
||||
UserName: username,
|
||||
Email: "",
|
||||
Roles: []string{"admin"}, // Phase 1 auth gets admin role
|
||||
Claims: map[string]any{
|
||||
"role": "admin",
|
||||
"legacy": true, // Mark as legacy auth
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("authentication failed")
|
||||
}
|
||||
|
||||
// validatePhase1Auth checks Phase 1 authentication (API key or basic auth)
|
||||
func (sp *SecurityProvider) validatePhase1Auth(r *http.Request) bool {
|
||||
// Check if any Phase 1 authentication is configured
|
||||
hasAuth := sp.config.Server.AuthKey != "" ||
|
||||
sp.config.Server.Username != "" ||
|
||||
sp.config.Server.Password != ""
|
||||
|
||||
if !hasAuth {
|
||||
// No Phase 1 authentication configured
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for API key authentication (x-api-key header)
|
||||
if sp.config.Server.AuthKey != "" {
|
||||
apiKey := r.Header.Get("x-api-key")
|
||||
if apiKey == sp.config.Server.AuthKey {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also check Authorization header for bearer token (API key)
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if len(authHeader) > 7 && authHeader[:7] == "Bearer " {
|
||||
token := authHeader[7:]
|
||||
if token == sp.config.Server.AuthKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for username/password authentication (HTTP Basic Auth)
|
||||
if sp.config.Server.Username != "" && sp.config.Server.Password != "" {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if ok && username == sp.config.Server.Username && password == sp.config.Server.Password {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetColumnSecurity returns column security rules (implements security.ColumnSecurityProvider)
|
||||
func (sp *SecurityProvider) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]security.ColumnSecurity, error) {
|
||||
// Return empty - no column-level security for now
|
||||
return []security.ColumnSecurity{}, nil
|
||||
}
|
||||
|
||||
// GetRowSecurity returns row security rules (implements security.RowSecurityProvider)
|
||||
func (sp *SecurityProvider) GetRowSecurity(ctx context.Context, userID int, schema, table string) (security.RowSecurity, error) {
|
||||
// Get user to check role
|
||||
user, err := sp.userRepo.GetByID(ctx, fmt.Sprintf("%d", userID))
|
||||
if err != nil {
|
||||
return security.RowSecurity{}, err
|
||||
}
|
||||
|
||||
// Admin can access all rows
|
||||
if user.Role.String() == "admin" {
|
||||
return security.RowSecurity{
|
||||
Template: "", // Empty template means no filtering
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Regular users can only access their own data
|
||||
// Apply user_id filter for tables that have user_id column
|
||||
userTables := []string{"api_keys", "hooks", "whatsapp_accounts"}
|
||||
for _, userTable := range userTables {
|
||||
if table == userTable {
|
||||
return security.RowSecurity{
|
||||
Template: fmt.Sprintf("user_id = %d", userID),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// For other tables, no additional filtering
|
||||
return security.RowSecurity{
|
||||
Template: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractBearerToken extracts Bearer token from Authorization header
|
||||
func extractBearerToken(r *http.Request) string {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if len(auth) > 7 && strings.HasPrefix(auth, "Bearer ") {
|
||||
return auth[7:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
343
pkg/api/server.go
Normal file
343
pkg/api/server.go
Normal file
@@ -0,0 +1,343 @@
|
||||
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 <token>"
|
||||
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)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type ServerConfig struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
Password string `json:"password,omitempty"`
|
||||
AuthKey string `json:"auth_key,omitempty"`
|
||||
JWTSecret string `json:"jwt_secret,omitempty"` // Secret for JWT signing
|
||||
TLS TLSConfig `json:"tls,omitempty"`
|
||||
}
|
||||
|
||||
@@ -152,7 +153,10 @@ func Load(path string) (*Config, error) {
|
||||
cfg.Server.Host = "0.0.0.0"
|
||||
}
|
||||
if cfg.Server.Port == 0 {
|
||||
cfg.Server.Port = 8825
|
||||
cfg.Server.Port = 8080
|
||||
}
|
||||
if cfg.Server.JWTSecret == "" {
|
||||
cfg.Server.JWTSecret = "change-me-in-production" // Default for development
|
||||
}
|
||||
if cfg.Media.DataPath == "" {
|
||||
cfg.Media.DataPath = "./data/media"
|
||||
|
||||
70
pkg/models/sql_public_api_keys.go
Normal file
70
pkg/models/sql_public_api_keys.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicAPIKey struct {
|
||||
bun.BaseModel `bun:"table:api_keys,alias:api_keys"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||
ExpiresAt resolvespec_common.SqlTime `bun:"expires_at,type:timestamp,nullzero," json:"expires_at"`
|
||||
Key resolvespec_common.SqlString `bun:"key,type:varchar(255),notnull," json:"key"` // Hashed API key
|
||||
KeyPrefix resolvespec_common.SqlString `bun:"key_prefix,type:varchar(20),nullzero," json:"key_prefix"` // First few characters for display
|
||||
LastUsedAt resolvespec_common.SqlTime `bun:"last_used_at,type:timestamp,nullzero," json:"last_used_at"`
|
||||
Name resolvespec_common.SqlString `bun:"name,type:varchar(255),notnull," json:"name"` // Friendly name for the API key
|
||||
Permissions resolvespec_common.SqlString `bun:"permissions,type:text,nullzero," json:"permissions"` // JSON array of permissions
|
||||
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicAPIKey
|
||||
func (m ModelPublicAPIKey) TableName() string {
|
||||
return "api_keys"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicAPIKey
|
||||
func (m ModelPublicAPIKey) TableNameOnly() string {
|
||||
return "api_keys"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicAPIKey
|
||||
func (m ModelPublicAPIKey) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicAPIKey) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicAPIKey) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicAPIKey) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicAPIKey) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicAPIKey) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicAPIKey) GetPrefix() string {
|
||||
return "AKP"
|
||||
}
|
||||
70
pkg/models/sql_public_event_logs.go
Normal file
70
pkg/models/sql_public_event_logs.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicEventLog struct {
|
||||
bun.BaseModel `bun:"table:event_logs,alias:event_logs"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
Action resolvespec_common.SqlString `bun:"action,type:varchar(50),nullzero," json:"action"` // create
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
Data resolvespec_common.SqlString `bun:"data,type:text,nullzero," json:"data"` // JSON encoded event data
|
||||
EntityID resolvespec_common.SqlString `bun:"entity_id,type:varchar(36),nullzero," json:"entity_id"`
|
||||
EntityType resolvespec_common.SqlString `bun:"entity_type,type:varchar(100),nullzero," json:"entity_type"` // user
|
||||
Error resolvespec_common.SqlString `bun:"error,type:text,nullzero," json:"error"`
|
||||
EventType resolvespec_common.SqlString `bun:"event_type,type:varchar(100),notnull," json:"event_type"`
|
||||
IpAddress resolvespec_common.SqlString `bun:"ip_address,type:varchar(50),nullzero," json:"ip_address"`
|
||||
Success bool `bun:"success,type:boolean,default:true,notnull," json:"success"`
|
||||
UserAgent resolvespec_common.SqlString `bun:"user_agent,type:text,nullzero," json:"user_agent"`
|
||||
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),nullzero," json:"user_id"` // Optional user reference
|
||||
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicEventLog
|
||||
func (m ModelPublicEventLog) TableName() string {
|
||||
return "event_logs"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicEventLog
|
||||
func (m ModelPublicEventLog) TableNameOnly() string {
|
||||
return "event_logs"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicEventLog
|
||||
func (m ModelPublicEventLog) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicEventLog) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicEventLog) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicEventLog) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicEventLog) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicEventLog) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicEventLog) GetPrefix() string {
|
||||
return "ELV"
|
||||
}
|
||||
73
pkg/models/sql_public_hooks.go
Normal file
73
pkg/models/sql_public_hooks.go
Normal file
@@ -0,0 +1,73 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicHook struct {
|
||||
bun.BaseModel `bun:"table:hooks,alias:hooks"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||
Description resolvespec_common.SqlString `bun:"description,type:text,nullzero," json:"description"`
|
||||
Events resolvespec_common.SqlString `bun:"events,type:text,nullzero," json:"events"` // JSON array of event types
|
||||
Headers resolvespec_common.SqlString `bun:"headers,type:text,nullzero," json:"headers"` // JSON encoded headers
|
||||
Method resolvespec_common.SqlString `bun:"method,type:varchar(10),default:POST,notnull," json:"method"` // HTTP method
|
||||
Name resolvespec_common.SqlString `bun:"name,type:varchar(255),notnull," json:"name"`
|
||||
RetryCount resolvespec_common.SqlInt32 `bun:"retry_count,type:int,default:3,notnull," json:"retry_count"`
|
||||
Secret resolvespec_common.SqlString `bun:"secret,type:varchar(255),nullzero," json:"secret"` // HMAC signature secret
|
||||
Timeout resolvespec_common.SqlInt32 `bun:"timeout,type:int,default:30,notnull," json:"timeout"` // Timeout in seconds
|
||||
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||
URL resolvespec_common.SqlString `bun:"url,type:text,notnull," json:"url"`
|
||||
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicHook
|
||||
func (m ModelPublicHook) TableName() string {
|
||||
return "hooks"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicHook
|
||||
func (m ModelPublicHook) TableNameOnly() string {
|
||||
return "hooks"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicHook
|
||||
func (m ModelPublicHook) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicHook) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicHook) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicHook) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicHook) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicHook) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicHook) GetPrefix() string {
|
||||
return "HOO"
|
||||
}
|
||||
66
pkg/models/sql_public_message_cache.go
Normal file
66
pkg/models/sql_public_message_cache.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicMessageCache struct {
|
||||
bun.BaseModel `bun:"table:message_cache,alias:message_cache"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
AccountID resolvespec_common.SqlString `bun:"account_id,type:varchar(36),notnull," json:"account_id"`
|
||||
ChatID resolvespec_common.SqlString `bun:"chat_id,type:varchar(255),notnull," json:"chat_id"`
|
||||
Content resolvespec_common.SqlString `bun:"content,type:text,notnull," json:"content"` // JSON encoded message content
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
FromMe bool `bun:"from_me,type:boolean,notnull," json:"from_me"`
|
||||
MessageID resolvespec_common.SqlString `bun:"message_id,type:varchar(255),notnull," json:"message_id"`
|
||||
MessageType resolvespec_common.SqlString `bun:"message_type,type:varchar(50),notnull," json:"message_type"` // text
|
||||
Timestamp resolvespec_common.SqlTime `bun:"timestamp,type:timestamp,notnull," json:"timestamp"`
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicMessageCache
|
||||
func (m ModelPublicMessageCache) TableName() string {
|
||||
return "message_cache"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicMessageCache
|
||||
func (m ModelPublicMessageCache) TableNameOnly() string {
|
||||
return "message_cache"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicMessageCache
|
||||
func (m ModelPublicMessageCache) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicMessageCache) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicMessageCache) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicMessageCache) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicMessageCache) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicMessageCache) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicMessageCache) GetPrefix() string {
|
||||
return "MCE"
|
||||
}
|
||||
66
pkg/models/sql_public_sessions.go
Normal file
66
pkg/models/sql_public_sessions.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicSession struct {
|
||||
bun.BaseModel `bun:"table:sessions,alias:sessions"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
ExpiresAt resolvespec_common.SqlTime `bun:"expires_at,type:timestamp,notnull," json:"expires_at"`
|
||||
IpAddress resolvespec_common.SqlString `bun:"ip_address,type:varchar(50),nullzero," json:"ip_address"`
|
||||
Token resolvespec_common.SqlString `bun:"token,type:varchar(255),notnull," json:"token"` // Session token hash
|
||||
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||
UserAgent resolvespec_common.SqlString `bun:"user_agent,type:text,nullzero," json:"user_agent"`
|
||||
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicSession
|
||||
func (m ModelPublicSession) TableName() string {
|
||||
return "sessions"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicSession
|
||||
func (m ModelPublicSession) TableNameOnly() string {
|
||||
return "sessions"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicSession
|
||||
func (m ModelPublicSession) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicSession) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicSession) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicSession) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicSession) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicSession) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicSession) GetPrefix() string {
|
||||
return "SES"
|
||||
}
|
||||
72
pkg/models/sql_public_users.go
Normal file
72
pkg/models/sql_public_users.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicUser struct {
|
||||
bun.BaseModel `bun:"table:users,alias:users"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"` // Soft delete
|
||||
Email resolvespec_common.SqlString `bun:"email,type:varchar(255),notnull," json:"email"`
|
||||
FullName resolvespec_common.SqlString `bun:"full_name,type:varchar(255),nullzero," json:"full_name"`
|
||||
Password resolvespec_common.SqlString `bun:"password,type:varchar(255),notnull," json:"password"` // Bcrypt hashed password
|
||||
Role resolvespec_common.SqlString `bun:"role,type:varchar(50),default:user,notnull," json:"role"` // admin
|
||||
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||
Username resolvespec_common.SqlString `bun:"username,type:varchar(255),notnull," json:"username"`
|
||||
RelUserIDPublicAPIKeys []*ModelPublicAPIKey `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicapikeys,omitempty"` // Has many ModelPublicAPIKey
|
||||
RelUserIDPublicHooks []*ModelPublicHook `bun:"rel:has-many,join:id=user_id" json:"reluseridpublichooks,omitempty"` // Has many ModelPublicHook
|
||||
RelUserIDPublicWhatsappAccounts []*ModelPublicWhatsappAccount `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicwhatsappaccounts,omitempty"` // Has many ModelPublicWhatsappAccount
|
||||
RelUserIDPublicEventLogs []*ModelPublicEventLog `bun:"rel:has-many,join:id=user_id" json:"reluseridpubliceventlogs,omitempty"` // Has many ModelPublicEventLog
|
||||
RelUserIDPublicSessions []*ModelPublicSession `bun:"rel:has-many,join:id=user_id" json:"reluseridpublicsessions,omitempty"` // Has many ModelPublicSession
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicUser
|
||||
func (m ModelPublicUser) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicUser
|
||||
func (m ModelPublicUser) TableNameOnly() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicUser
|
||||
func (m ModelPublicUser) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicUser) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicUser) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicUser) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicUser) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicUser) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicUser) GetPrefix() string {
|
||||
return "USE"
|
||||
}
|
||||
71
pkg/models/sql_public_whatsapp_accounts.go
Normal file
71
pkg/models/sql_public_whatsapp_accounts.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Code generated by relspecgo. DO NOT EDIT.
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type ModelPublicWhatsappAccount struct {
|
||||
bun.BaseModel `bun:"table:whatsapp_accounts,alias:whatsapp_accounts"`
|
||||
ID resolvespec_common.SqlString `bun:"id,type:varchar(36),pk," json:"id"` // UUID
|
||||
AccountType resolvespec_common.SqlString `bun:"account_type,type:varchar(50),notnull," json:"account_type"` // whatsmeow or business-api
|
||||
Active bool `bun:"active,type:boolean,default:true,notnull," json:"active"`
|
||||
Config resolvespec_common.SqlString `bun:"config,type:text,nullzero," json:"config"` // JSON encoded additional config
|
||||
CreatedAt resolvespec_common.SqlTime `bun:"created_at,type:timestamp,default:now(),notnull," json:"created_at"`
|
||||
DeletedAt resolvespec_common.SqlTime `bun:"deleted_at,type:timestamp,nullzero," json:"deleted_at"`
|
||||
DisplayName resolvespec_common.SqlString `bun:"display_name,type:varchar(255),nullzero," json:"display_name"`
|
||||
LastConnectedAt resolvespec_common.SqlTime `bun:"last_connected_at,type:timestamp,nullzero," json:"last_connected_at"`
|
||||
PhoneNumber resolvespec_common.SqlString `bun:"phone_number,type:varchar(50),notnull," json:"phone_number"`
|
||||
SessionPath resolvespec_common.SqlString `bun:"session_path,type:text,nullzero," json:"session_path"`
|
||||
Status resolvespec_common.SqlString `bun:"status,type:varchar(50),default:disconnected,notnull," json:"status"` // connected
|
||||
UpdatedAt resolvespec_common.SqlTime `bun:"updated_at,type:timestamp,default:now(),notnull," json:"updated_at"`
|
||||
UserID resolvespec_common.SqlString `bun:"user_id,type:varchar(36),notnull," json:"user_id"`
|
||||
RelUserID *ModelPublicUser `bun:"rel:has-one,join:user_id=id" json:"reluserid,omitempty"` // Has one ModelPublicUser
|
||||
}
|
||||
|
||||
// TableName returns the table name for ModelPublicWhatsappAccount
|
||||
func (m ModelPublicWhatsappAccount) TableName() string {
|
||||
return "whatsapp_accounts"
|
||||
}
|
||||
|
||||
// TableNameOnly returns the table name without schema for ModelPublicWhatsappAccount
|
||||
func (m ModelPublicWhatsappAccount) TableNameOnly() string {
|
||||
return "whatsapp_accounts"
|
||||
}
|
||||
|
||||
// SchemaName returns the schema name for ModelPublicWhatsappAccount
|
||||
func (m ModelPublicWhatsappAccount) SchemaName() string {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// GetID returns the primary key value
|
||||
func (m ModelPublicWhatsappAccount) GetID() int64 {
|
||||
return m.ID.Int64()
|
||||
}
|
||||
|
||||
// GetIDStr returns the primary key as a string
|
||||
func (m ModelPublicWhatsappAccount) GetIDStr() string {
|
||||
return fmt.Sprintf("%d", m.ID)
|
||||
}
|
||||
|
||||
// SetID sets the primary key value
|
||||
func (m ModelPublicWhatsappAccount) SetID(newid int64) {
|
||||
m.UpdateID(newid)
|
||||
}
|
||||
|
||||
// UpdateID updates the primary key value
|
||||
func (m *ModelPublicWhatsappAccount) UpdateID(newid int64) {
|
||||
m.ID.FromString(fmt.Sprintf("%d", newid))
|
||||
}
|
||||
|
||||
// GetIDName returns the name of the primary key column
|
||||
func (m ModelPublicWhatsappAccount) GetIDName() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
// GetPrefix returns the table prefix
|
||||
func (m ModelPublicWhatsappAccount) GetPrefix() string {
|
||||
return "WAH"
|
||||
}
|
||||
246
pkg/storage/db.go
Normal file
246
pkg/storage/db.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||
"github.com/uptrace/bun"
|
||||
"github.com/uptrace/bun/dialect/pgdialect"
|
||||
"github.com/uptrace/bun/dialect/sqlitedialect"
|
||||
"github.com/uptrace/bun/driver/pgdriver"
|
||||
"github.com/uptrace/bun/driver/sqliteshim"
|
||||
"github.com/uptrace/bun/extra/bundebug"
|
||||
)
|
||||
|
||||
// DB is the global database instance
|
||||
var DB *bun.DB
|
||||
var dbType string // Store the database type for later use
|
||||
|
||||
// Initialize sets up the database connection based on configuration
|
||||
func Initialize(cfg *config.DatabaseConfig) error {
|
||||
var sqldb *sql.DB
|
||||
var err error
|
||||
|
||||
dbType = cfg.Type
|
||||
switch cfg.Type {
|
||||
case "postgres", "postgresql":
|
||||
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
cfg.Username, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||
sqldb = sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
|
||||
DB = bun.NewDB(sqldb, pgdialect.New())
|
||||
|
||||
case "sqlite":
|
||||
sqldb, err = sql.Open(sqliteshim.ShimName, cfg.SQLitePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open sqlite database: %w", err)
|
||||
}
|
||||
DB = bun.NewDB(sqldb, sqlitedialect.New())
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
// Add query hook for debugging (optional, can be removed in production)
|
||||
DB.AddQueryHook(bundebug.NewQueryHook(
|
||||
bundebug.WithVerbose(true),
|
||||
bundebug.FromEnv("BUNDEBUG"),
|
||||
))
|
||||
|
||||
// Set connection pool settings
|
||||
sqldb.SetMaxIdleConns(10)
|
||||
sqldb.SetMaxOpenConns(100)
|
||||
sqldb.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
// Test the connection
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := sqldb.PingContext(ctx); err != nil {
|
||||
return fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateTables creates database tables based on BUN models
|
||||
func CreateTables(ctx context.Context) error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
// For SQLite, use raw SQL with compatible syntax
|
||||
if dbType == "sqlite" {
|
||||
return createTablesSQLite(ctx)
|
||||
}
|
||||
|
||||
// For PostgreSQL, use BUN's auto-generation
|
||||
models := []interface{}{
|
||||
(*models.ModelPublicUser)(nil),
|
||||
(*models.ModelPublicAPIKey)(nil),
|
||||
(*models.ModelPublicHook)(nil),
|
||||
(*models.ModelPublicWhatsappAccount)(nil),
|
||||
(*models.ModelPublicEventLog)(nil),
|
||||
(*models.ModelPublicSession)(nil),
|
||||
(*models.ModelPublicMessageCache)(nil),
|
||||
}
|
||||
|
||||
for _, model := range models {
|
||||
_, err := DB.NewCreateTable().Model(model).IfNotExists().Exec(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createTablesSQLite creates tables using SQLite-compatible SQL
|
||||
func createTablesSQLite(ctx context.Context) error {
|
||||
tables := []string{
|
||||
// Users table
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(255),
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
)`,
|
||||
|
||||
// API Keys table
|
||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
key VARCHAR(255) NOT NULL UNIQUE,
|
||||
key_prefix VARCHAR(20),
|
||||
permissions TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
last_used_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// Hooks table
|
||||
`CREATE TABLE IF NOT EXISTS hooks (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
method VARCHAR(10) NOT NULL DEFAULT 'POST',
|
||||
headers TEXT,
|
||||
events TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
retry_count INTEGER NOT NULL DEFAULT 3,
|
||||
timeout_seconds INTEGER NOT NULL DEFAULT 30,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// WhatsApp Accounts table
|
||||
`CREATE TABLE IF NOT EXISTS whatsapp_accounts (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
phone_number VARCHAR(20) NOT NULL UNIQUE,
|
||||
account_type VARCHAR(50) NOT NULL DEFAULT 'whatsmeow',
|
||||
business_api_config TEXT,
|
||||
active BOOLEAN NOT NULL DEFAULT 1,
|
||||
connected BOOLEAN NOT NULL DEFAULT 0,
|
||||
last_connected_at TIMESTAMP,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// Event Logs table
|
||||
`CREATE TABLE IF NOT EXISTS event_logs (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36),
|
||||
account_id VARCHAR(36),
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
event_data TEXT,
|
||||
from_number VARCHAR(20),
|
||||
to_number VARCHAR(20),
|
||||
message_id VARCHAR(255),
|
||||
status VARCHAR(50),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL
|
||||
)`,
|
||||
|
||||
// Sessions table
|
||||
`CREATE TABLE IF NOT EXISTS sessions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
token VARCHAR(500) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// Message Cache table
|
||||
`CREATE TABLE IF NOT EXISTS message_cache (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
account_id VARCHAR(36),
|
||||
event_type VARCHAR(100) NOT NULL,
|
||||
event_data TEXT NOT NULL,
|
||||
message_id VARCHAR(255),
|
||||
from_number VARCHAR(20),
|
||||
to_number VARCHAR(20),
|
||||
processed BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at TIMESTAMP,
|
||||
FOREIGN KEY (account_id) REFERENCES whatsapp_accounts(id) ON DELETE SET NULL
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, sql := range tables {
|
||||
if _, err := DB.ExecContext(ctx, sql); err != nil {
|
||||
return fmt.Errorf("failed to create table: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func Close() error {
|
||||
if DB == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DB.Close()
|
||||
}
|
||||
|
||||
// GetDB returns the database instance
|
||||
func GetDB() *bun.DB {
|
||||
return DB
|
||||
}
|
||||
|
||||
// HealthCheck checks if the database connection is healthy
|
||||
func HealthCheck() error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
return DB.PingContext(ctx)
|
||||
}
|
||||
299
pkg/storage/repository.go
Normal file
299
pkg/storage/repository.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
// Repository provides common CRUD operations
|
||||
type Repository[T any] struct {
|
||||
db *bun.DB
|
||||
}
|
||||
|
||||
// NewRepository creates a new repository instance
|
||||
func NewRepository[T any](db *bun.DB) *Repository[T] {
|
||||
return &Repository[T]{db: db}
|
||||
}
|
||||
|
||||
// Create inserts a new record
|
||||
func (r *Repository[T]) Create(ctx context.Context, entity *T) error {
|
||||
_, err := r.db.NewInsert().Model(entity).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetByID retrieves a record by ID
|
||||
func (r *Repository[T]) GetByID(ctx context.Context, id string) (*T, error) {
|
||||
var entity T
|
||||
err := r.db.NewSelect().Model(&entity).Where("id = ?", id).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &entity, nil
|
||||
}
|
||||
|
||||
// Update updates an existing record
|
||||
func (r *Repository[T]) Update(ctx context.Context, entity *T) error {
|
||||
_, err := r.db.NewUpdate().Model(entity).WherePK().Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete soft deletes a record by ID (if model has DeletedAt field)
|
||||
func (r *Repository[T]) Delete(ctx context.Context, id string) error {
|
||||
var entity T
|
||||
_, err := r.db.NewDelete().Model(&entity).Where("id = ?", id).Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// List retrieves all records with optional filtering
|
||||
func (r *Repository[T]) List(ctx context.Context, filter map[string]interface{}) ([]T, error) {
|
||||
var entities []T
|
||||
query := r.db.NewSelect().Model(&entities)
|
||||
|
||||
for key, value := range filter {
|
||||
query = query.Where("? = ?", bun.Ident(key), value)
|
||||
}
|
||||
|
||||
err := query.Scan(ctx)
|
||||
return entities, err
|
||||
}
|
||||
|
||||
// Count returns the total number of records matching the filter
|
||||
func (r *Repository[T]) Count(ctx context.Context, filter map[string]interface{}) (int, error) {
|
||||
query := r.db.NewSelect().Model((*T)(nil))
|
||||
|
||||
for key, value := range filter {
|
||||
query = query.Where("? = ?", bun.Ident(key), value)
|
||||
}
|
||||
|
||||
count, err := query.Count(ctx)
|
||||
return count, err
|
||||
}
|
||||
|
||||
// UserRepository provides user-specific operations
|
||||
type UserRepository struct {
|
||||
*Repository[models.ModelPublicUser]
|
||||
}
|
||||
|
||||
// NewUserRepository creates a new user repository
|
||||
func NewUserRepository(db *bun.DB) *UserRepository {
|
||||
return &UserRepository{
|
||||
Repository: NewRepository[models.ModelPublicUser](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByUsername retrieves a user by username
|
||||
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*models.ModelPublicUser, error) {
|
||||
var user models.ModelPublicUser
|
||||
err := r.db.NewSelect().Model(&user).Where("username = ?", username).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetByEmail retrieves a user by email
|
||||
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*models.ModelPublicUser, error) {
|
||||
var user models.ModelPublicUser
|
||||
err := r.db.NewSelect().Model(&user).Where("email = ?", email).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// APIKeyRepository provides API key-specific operations
|
||||
type APIKeyRepository struct {
|
||||
*Repository[models.ModelPublicAPIKey]
|
||||
}
|
||||
|
||||
// NewAPIKeyRepository creates a new API key repository
|
||||
func NewAPIKeyRepository(db *bun.DB) *APIKeyRepository {
|
||||
return &APIKeyRepository{
|
||||
Repository: NewRepository[models.ModelPublicAPIKey](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByKey retrieves an API key by its key value
|
||||
func (r *APIKeyRepository) GetByKey(ctx context.Context, key string) (*models.ModelPublicAPIKey, error) {
|
||||
var apiKey models.ModelPublicAPIKey
|
||||
err := r.db.NewSelect().Model(&apiKey).
|
||||
Where("key = ? AND active = ?", key, true).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &apiKey, nil
|
||||
}
|
||||
|
||||
// GetByUserID retrieves all API keys for a user
|
||||
func (r *APIKeyRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicAPIKey, error) {
|
||||
var apiKeys []models.ModelPublicAPIKey
|
||||
err := r.db.NewSelect().Model(&apiKeys).Where("user_id = ?", userID).Scan(ctx)
|
||||
return apiKeys, err
|
||||
}
|
||||
|
||||
// UpdateLastUsed updates the last used timestamp for an API key
|
||||
func (r *APIKeyRepository) UpdateLastUsed(ctx context.Context, id string) error {
|
||||
now := time.Now()
|
||||
_, err := r.db.NewUpdate().Model((*models.ModelPublicAPIKey)(nil)).
|
||||
Set("last_used_at = ?", now).
|
||||
Where("id = ?", id).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// HookRepository provides hook-specific operations
|
||||
type HookRepository struct {
|
||||
*Repository[models.ModelPublicHook]
|
||||
}
|
||||
|
||||
// NewHookRepository creates a new hook repository
|
||||
func NewHookRepository(db *bun.DB) *HookRepository {
|
||||
return &HookRepository{
|
||||
Repository: NewRepository[models.ModelPublicHook](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByUserID retrieves all hooks for a user
|
||||
func (r *HookRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicHook, error) {
|
||||
var hooks []models.ModelPublicHook
|
||||
err := r.db.NewSelect().Model(&hooks).Where("user_id = ?", userID).Scan(ctx)
|
||||
return hooks, err
|
||||
}
|
||||
|
||||
// GetActiveHooks retrieves all active hooks
|
||||
func (r *HookRepository) GetActiveHooks(ctx context.Context) ([]models.ModelPublicHook, error) {
|
||||
var hooks []models.ModelPublicHook
|
||||
err := r.db.NewSelect().Model(&hooks).Where("active = ?", true).Scan(ctx)
|
||||
return hooks, err
|
||||
}
|
||||
|
||||
// WhatsAppAccountRepository provides WhatsApp account-specific operations
|
||||
type WhatsAppAccountRepository struct {
|
||||
*Repository[models.ModelPublicWhatsappAccount]
|
||||
}
|
||||
|
||||
// NewWhatsAppAccountRepository creates a new WhatsApp account repository
|
||||
func NewWhatsAppAccountRepository(db *bun.DB) *WhatsAppAccountRepository {
|
||||
return &WhatsAppAccountRepository{
|
||||
Repository: NewRepository[models.ModelPublicWhatsappAccount](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByUserID retrieves all WhatsApp accounts for a user
|
||||
func (r *WhatsAppAccountRepository) GetByUserID(ctx context.Context, userID string) ([]models.ModelPublicWhatsappAccount, error) {
|
||||
var accounts []models.ModelPublicWhatsappAccount
|
||||
err := r.db.NewSelect().Model(&accounts).Where("user_id = ?", userID).Scan(ctx)
|
||||
return accounts, err
|
||||
}
|
||||
|
||||
// GetByPhoneNumber retrieves an account by phone number
|
||||
func (r *WhatsAppAccountRepository) GetByPhoneNumber(ctx context.Context, phoneNumber string) (*models.ModelPublicWhatsappAccount, error) {
|
||||
var account models.ModelPublicWhatsappAccount
|
||||
err := r.db.NewSelect().Model(&account).Where("phone_number = ?", phoneNumber).Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &account, nil
|
||||
}
|
||||
|
||||
// UpdateStatus updates the status of a WhatsApp account
|
||||
func (r *WhatsAppAccountRepository) UpdateStatus(ctx context.Context, id string, status string) error {
|
||||
query := r.db.NewUpdate().Model((*models.ModelPublicWhatsappAccount)(nil)).
|
||||
Set("status = ?", status).
|
||||
Where("id = ?", id)
|
||||
|
||||
if status == "connected" {
|
||||
now := time.Now()
|
||||
query = query.Set("last_connected_at = ?", now)
|
||||
}
|
||||
|
||||
_, err := query.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// SessionRepository provides session-specific operations
|
||||
type SessionRepository struct {
|
||||
*Repository[models.ModelPublicSession]
|
||||
}
|
||||
|
||||
// NewSessionRepository creates a new session repository
|
||||
func NewSessionRepository(db *bun.DB) *SessionRepository {
|
||||
return &SessionRepository{
|
||||
Repository: NewRepository[models.ModelPublicSession](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByToken retrieves a session by token
|
||||
func (r *SessionRepository) GetByToken(ctx context.Context, token string) (*models.ModelPublicSession, error) {
|
||||
var session models.ModelPublicSession
|
||||
err := r.db.NewSelect().Model(&session).
|
||||
Where("token = ? AND expires_at > ?", token, time.Now()).
|
||||
Scan(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
// DeleteExpired removes all expired sessions
|
||||
func (r *SessionRepository) DeleteExpired(ctx context.Context) error {
|
||||
_, err := r.db.NewDelete().Model((*models.ModelPublicSession)(nil)).
|
||||
Where("expires_at <= ?", time.Now()).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteByUserID removes all sessions for a user
|
||||
func (r *SessionRepository) DeleteByUserID(ctx context.Context, userID string) error {
|
||||
_, err := r.db.NewDelete().Model((*models.ModelPublicSession)(nil)).
|
||||
Where("user_id = ?", userID).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// EventLogRepository provides event log-specific operations
|
||||
type EventLogRepository struct {
|
||||
*Repository[models.ModelPublicEventLog]
|
||||
}
|
||||
|
||||
// NewEventLogRepository creates a new event log repository
|
||||
func NewEventLogRepository(db *bun.DB) *EventLogRepository {
|
||||
return &EventLogRepository{
|
||||
Repository: NewRepository[models.ModelPublicEventLog](db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetByUserID retrieves event logs for a user
|
||||
func (r *EventLogRepository) GetByUserID(ctx context.Context, userID string, limit int) ([]models.ModelPublicEventLog, error) {
|
||||
var logs []models.ModelPublicEventLog
|
||||
err := r.db.NewSelect().Model(&logs).
|
||||
Where("user_id = ?", userID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Scan(ctx)
|
||||
return logs, err
|
||||
}
|
||||
|
||||
// GetByEntityID retrieves event logs for a specific entity
|
||||
func (r *EventLogRepository) GetByEntityID(ctx context.Context, entityType, entityID string, limit int) ([]models.ModelPublicEventLog, error) {
|
||||
var logs []models.ModelPublicEventLog
|
||||
err := r.db.NewSelect().Model(&logs).
|
||||
Where("entity_type = ? AND entity_id = ?", entityType, entityID).
|
||||
Order("created_at DESC").
|
||||
Limit(limit).
|
||||
Scan(ctx)
|
||||
return logs, err
|
||||
}
|
||||
|
||||
// DeleteOlderThan removes event logs older than the specified duration
|
||||
func (r *EventLogRepository) DeleteOlderThan(ctx context.Context, duration time.Duration) error {
|
||||
cutoff := time.Now().Add(-duration)
|
||||
_, err := r.db.NewDelete().Model((*models.ModelPublicEventLog)(nil)).
|
||||
Where("created_at < ?", cutoff).
|
||||
Exec(ctx)
|
||||
return err
|
||||
}
|
||||
55
pkg/storage/seed.go
Normal file
55
pkg/storage/seed.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/models"
|
||||
resolvespec_common "github.com/bitechdev/ResolveSpec/pkg/spectypes"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// SeedData creates initial data for the application
|
||||
func SeedData(ctx context.Context) error {
|
||||
if DB == nil {
|
||||
return fmt.Errorf("database not initialized")
|
||||
}
|
||||
|
||||
// Check if admin user already exists
|
||||
userRepo := NewUserRepository(DB)
|
||||
_, err := userRepo.GetByUsername(ctx, "admin")
|
||||
if err == nil {
|
||||
// Admin user already exists
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create default admin user
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
adminUser := &models.ModelPublicUser{
|
||||
ID: resolvespec_common.NewSqlString(uuid.New().String()),
|
||||
Username: resolvespec_common.NewSqlString("admin"),
|
||||
Email: resolvespec_common.NewSqlString("admin@whatshooked.local"),
|
||||
Password: resolvespec_common.NewSqlString(string(hashedPassword)),
|
||||
FullName: resolvespec_common.NewSqlString("System Administrator"),
|
||||
Role: resolvespec_common.NewSqlString("admin"),
|
||||
Active: true,
|
||||
CreatedAt: resolvespec_common.NewSqlTime(now),
|
||||
UpdatedAt: resolvespec_common.NewSqlTime(now),
|
||||
}
|
||||
|
||||
if err := userRepo.Create(ctx, adminUser); err != nil {
|
||||
return fmt.Errorf("failed to create admin user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("✓ Created default admin user (username: admin, password: admin123)")
|
||||
fmt.Println("⚠ Please change the default password after first login!")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/api"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/cache"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/config"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/eventlogger"
|
||||
@@ -11,7 +12,10 @@ import (
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/handlers"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/hooks"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/logging"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/storage"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/utils"
|
||||
"git.warky.dev/wdevs/whatshooked/pkg/whatsapp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
// WhatsHooked is the main library instance
|
||||
@@ -24,7 +28,7 @@ type WhatsHooked struct {
|
||||
eventLogger *eventlogger.Logger
|
||||
messageCache *cache.MessageCache
|
||||
handlers *handlers.Handlers
|
||||
server *Server // Optional built-in server
|
||||
apiServer *api.Server // ResolveSpec unified server
|
||||
}
|
||||
|
||||
// NewFromFile creates a WhatsHooked instance from a config file
|
||||
@@ -179,16 +183,116 @@ func (wh *WhatsHooked) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartServer starts the built-in HTTP server (convenience method)
|
||||
func (wh *WhatsHooked) StartServer() error {
|
||||
wh.server = NewServer(wh)
|
||||
return wh.server.Start()
|
||||
// StartServer starts the ResolveSpec HTTP server
|
||||
func (wh *WhatsHooked) StartServer(ctx context.Context) error {
|
||||
return wh.StartAPIServer(ctx)
|
||||
}
|
||||
|
||||
// StopServer stops the built-in HTTP server
|
||||
// StopServer stops the ResolveSpec HTTP server
|
||||
func (wh *WhatsHooked) StopServer(ctx context.Context) error {
|
||||
if wh.server != nil {
|
||||
return wh.server.Stop(ctx)
|
||||
return wh.StopAPIServer(ctx)
|
||||
}
|
||||
|
||||
// StartAPIServer starts the unified ResolveSpec server
|
||||
func (wh *WhatsHooked) StartAPIServer(ctx context.Context) error {
|
||||
// Subscribe to hook success events for two-way communication
|
||||
wh.eventBus.Subscribe(events.EventHookSuccess, wh.handleHookResponse)
|
||||
|
||||
// Initialize database
|
||||
logging.Info("Initializing database")
|
||||
if err := storage.Initialize(&wh.config.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
db := storage.GetDB()
|
||||
|
||||
// Create tables
|
||||
logging.Info("Creating database tables")
|
||||
if err := storage.CreateTables(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Seed initial data (creates admin user if not exists)
|
||||
logging.Info("Seeding initial data")
|
||||
if err := storage.SeedData(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create unified server
|
||||
logging.Info("Creating ResolveSpec server", "host", wh.config.Server.Host, "port", wh.config.Server.Port)
|
||||
apiServer, err := api.NewServer(wh.config, db, wh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wh.apiServer = apiServer
|
||||
|
||||
// Start the server in a goroutine (non-blocking)
|
||||
go func() {
|
||||
if err := wh.apiServer.Start(); err != nil {
|
||||
logging.Error("ResolveSpec server error", "error", err)
|
||||
}
|
||||
}()
|
||||
|
||||
logging.Info("ResolveSpec server started successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopAPIServer stops the ResolveSpec server
|
||||
func (wh *WhatsHooked) StopAPIServer(ctx context.Context) error {
|
||||
if wh.apiServer != nil {
|
||||
logging.Info("Stopping ResolveSpec server")
|
||||
// The server manager handles graceful shutdown internally
|
||||
// We just need to close the database
|
||||
return storage.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleHookResponse processes hook success events for two-way communication
|
||||
func (wh *WhatsHooked) handleHookResponse(event events.Event) {
|
||||
// Use event context for sending message
|
||||
ctx := event.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
// Extract response from event data
|
||||
responseData, ok := event.Data["response"]
|
||||
if !ok || responseData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Try to cast to HookResponse
|
||||
resp, ok := responseData.(hooks.HookResponse)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if !resp.SendMessage {
|
||||
return
|
||||
}
|
||||
|
||||
// Determine which account to use - default to first available if not specified
|
||||
targetAccountID := resp.AccountID
|
||||
if targetAccountID == "" && len(wh.config.WhatsApp) > 0 {
|
||||
targetAccountID = wh.config.WhatsApp[0].ID
|
||||
}
|
||||
|
||||
// Format phone number to JID format
|
||||
formattedJID := utils.FormatPhoneToJID(resp.To, wh.config.Server.DefaultCountryCode)
|
||||
|
||||
// Parse JID
|
||||
jid, err := types.ParseJID(formattedJID)
|
||||
if err != nil {
|
||||
logging.Error("Invalid JID in hook response", "jid", formattedJID, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Send message with context
|
||||
if err := wh.whatsappMgr.SendTextMessage(ctx, targetAccountID, jid, resp.Text); err != nil {
|
||||
logging.Error("Failed to send message from hook response", "error", err)
|
||||
} else {
|
||||
logging.Info("Message sent from hook response", "account_id", targetAccountID, "to", resp.To)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user