Files
workspace/apps/go-crm/main.go
gabspereira 744868caa1 feat(go-crm): full auth, routing, middleware, and supporting infra
- Add auth handlers (signup, login, logout, account management) with bcrypt
- Add client, customer, service, scheduling, payment, question, answer handlers
- Add dashboard, monthly report, and lead pipeline pages
- Add UTF-8 middleware to force charset on HTML responses
- Add config package with env-based overrides for DB path, secrets, endpoints
- Add parser package for WhatsApp message ingestion
- Add clean-arch layers: pkg/domain, pkg/repo, pkg/usecase for leads
- Add cmd/migrate utility for DB migrations
- Add Makefile, README, run-tests.sh, and dev scripts
- Update docker-compose.yml with memory limits
- Update .air.toml to exclude DB files and stop on errors
- Update whatsapp-sync dependencies and add src/index.js entrypoint
- Add whatsme standalone WhatsApp reader app (source only)
- Untrack .opencode-sandbox/data/go-crm.db from git history
- Expand root .gitignore: ngrok, tmp dirs, sandbox DBs, compiled binaries
2026-05-23 16:55:55 -03:00

192 lines
5.6 KiB
Go

package main
import (
"fmt"
"log"
"net/http"
"strconv"
"go-crm/config"
"go-crm/internal/db"
"go-crm/internal/handlers"
"go-crm/internal/templates"
wa "go-crm/internal/whatsapp"
"go-crm/pkg/repo"
"go-crm/pkg/usecase"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func main() {
fmt.Println("main: starting db init")
database, err := db.Init(config.DatabasePath())
fmt.Println("main: db init returned")
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer database.Close()
fmt.Println("main: initializing WhatsApp adapter")
waConnector, err := wa.NewWhatsmeowAdapter(config.WhatsAppStorePath(), config.HTTPServerEndpoint(), config.InternalSecret())
fmt.Println("main: WhatsApp adapter init returned")
if err != nil {
log.Printf("Warning: failed to initialize WhatsApp connector: %v", err)
waConnector = nil
} else {
waConnector.SetClientDB(database)
}
// App is the single dependency-injection container.
// All handler methods receive DB and WAConnector through it,
// eliminating package-level global state.
// Build our clean-arch layers for leads
leadRepo := repo.NewSQLiteLeadRepository(database)
leadSvc := usecase.NewLeadService(leadRepo)
app := handlers.NewApp(database, leadSvc, waConnector)
// Also populate legacy globals so existing tests continue to work.
handlers.SetupHandlers(database, waConnector)
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Accept", "Content-Type", "X-Internal-Secret", "Origin"},
ExposedHeaders: []string{"Content-Length", "Content-Type"},
AllowCredentials: false,
MaxAge: 86400,
}))
templates.Init()
r.Get("/", app.Dashboard)
r.Route("/auth", func(r chi.Router) {
r.Get("/signup", app.SignupPage)
r.Post("/signup", app.Signup)
r.Get("/login", app.LoginPage)
r.Post("/login", app.Login)
r.Post("/logout", app.Logout)
r.Get("/account", app.AccountPage)
r.Post("/account", app.UpdateAccount)
})
r.Route("/clients", func(r chi.Router) {
r.Get("/", app.ListClients)
r.Post("/", app.CreateClient)
r.Get("/{id}", app.ViewClient)
r.Put("/{id}", app.UpdateClient)
r.Delete("/{id}", app.DeleteClient)
})
r.Route("/customers", func(r chi.Router) {
r.Get("/", app.ListCustomers)
r.Post("/", app.CreateCustomer)
r.Get("/{id}", app.ViewCustomer)
r.Put("/{id}", app.UpdateCustomer)
r.Delete("/{id}", app.DeleteCustomer)
})
r.Get("/debug/cors-test", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.Write([]byte(`{"status":"ok","origin":"` + r.Header.Get("Origin") + `"}`))
})
r.Get("/debug/whatsapp", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if waConnector == nil {
w.Write([]byte(`{"status":"error","error":"WhatsApp connector not initialized"}`))
return
}
w.Write([]byte(`{"status":"ok","message":"WhatsApp connector initialized"}`))
})
r.Get("/debug/net-test", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
resp, err := http.Get("https://www.google.com")
if err != nil {
w.Write([]byte(`{"status":"error","error":` + err.Error() + `}`))
return
}
defer resp.Body.Close()
w.Write([]byte(`{"status":"ok","code":` + strconv.Itoa(resp.StatusCode) + `}`))
})
r.Route("/leads", func(r chi.Router) {
r.Get("/", app.ListLeads)
r.Get("/connect", app.LeadsConnectPage)
r.Get("/qr", app.LeadsQR)
r.Get("/verify/{client_id}", app.VerifyLead)
r.Put("/{id}", app.UpdateLead)
r.Delete("/{id}", app.DeleteLead)
// Lead pipeline
r.Post("/ingest", app.IngestLead)
r.Get("/review", app.LeadReviewQueue)
r.Put("/{id}/review", app.ConfirmLeadReview)
r.Get("/all", app.LeadAllList)
r.Delete("/{id}", app.DeleteLeadNew)
// Keyword management
r.Get("/keywords", app.LeadKeywordsPage)
r.Post("/keywords", app.AddServiceKeyword)
r.Delete("/keywords/{id}", app.DeleteServiceKeywordHandler)
// Status management
r.Post("/statuses", app.AddLeadStatusHandler)
r.Delete("/statuses/{id}", app.DeleteLeadStatusHandler)
})
r.Get("/report", app.MonthlyReport)
r.Route("/services", func(r chi.Router) {
r.Get("/", app.ListServices)
r.Post("/", app.CreateService)
r.Get("/{id}", app.ViewService)
r.Put("/{id}", app.UpdateService)
r.Delete("/{id}", app.DeleteService)
})
r.Route("/scheduling", func(r chi.Router) {
r.Get("/", app.ListSchedules)
r.Post("/", app.CreateSchedule)
r.Get("/{id}", app.ViewSchedule)
r.Put("/{id}", app.UpdateSchedule)
r.Delete("/{id}", app.DeleteSchedule)
})
r.Route("/payments", func(r chi.Router) {
r.Get("/", app.ListPayments)
r.Post("/", app.CreatePayment)
r.Get("/{id}", app.ViewPayment)
r.Put("/{id}", app.UpdatePayment)
r.Delete("/{id}", app.DeletePayment)
})
r.Route("/questions", func(r chi.Router) {
r.Get("/", app.ListQuestions)
r.Post("/", app.CreateQuestion)
r.Get("/{id}", app.ViewQuestion)
r.Put("/{id}", app.UpdateQuestion)
r.Delete("/{id}", app.DeleteQuestion)
})
r.Route("/answers", func(r chi.Router) {
r.Get("/", app.ListAnswers)
r.Post("/", app.CreateAnswer)
r.Get("/{id}", app.ViewAnswer)
r.Put("/{id}", app.UpdateAnswer)
r.Delete("/{id}", app.DeleteAnswer)
})
fmt.Println("main: ready to serve")
fmt.Println("CRM server running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", r))
}