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
This commit is contained in:
@@ -6,10 +6,13 @@ import (
|
||||
"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"
|
||||
@@ -17,13 +20,17 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
database, err := db.Init("/workspace/data/go-crm.db")
|
||||
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()
|
||||
|
||||
waConnector, err := wa.NewWhatsmeowAdapter("/workspace/data/whatsapp.db", "http://localhost:8080", "internal-secret")
|
||||
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
|
||||
@@ -31,6 +38,15 @@ func main() {
|
||||
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()
|
||||
@@ -39,60 +55,51 @@ func main() {
|
||||
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"},
|
||||
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,
|
||||
MaxAge: 86400,
|
||||
}))
|
||||
|
||||
templates.Init()
|
||||
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := handlers.GetAccountID(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/auth/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(`<!DOCTYPE html><html><head><title>Dashboard</title></head><body><h1>Welcome to CRM</h1><nav><a href="/clients">Clients</a> | <a href="/customers">Customers</a> | <a href="/services">Services</a> | <a href="/scheduling">Scheduling</a> | <a href="/payments">Payments</a> | <a href="/questions">Questions</a> | <a href="/answers">Answers</a> | <a href="/auth/account">Account</a> | <form method="POST" action="/auth/logout" style="display:inline"><button type="submit">Logout</button></form></nav></body></html>`))
|
||||
})
|
||||
r.Get("/", app.Dashboard)
|
||||
|
||||
r.Route("/auth", func(r chi.Router) {
|
||||
r.Get("/signup", handlers.SignupPage)
|
||||
r.Post("/signup", handlers.Signup)
|
||||
r.Get("/login", handlers.LoginPage)
|
||||
r.Post("/login", handlers.Login)
|
||||
r.Post("/logout", handlers.Logout)
|
||||
r.Get("/account", handlers.AccountPage)
|
||||
r.Post("/account", handlers.UpdateAccount)
|
||||
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("/", handlers.ListClients)
|
||||
r.Post("/", handlers.CreateClient)
|
||||
r.Get("/{id}", handlers.ViewClient)
|
||||
r.Put("/{id}", handlers.UpdateClient)
|
||||
r.Delete("/{id}", handlers.DeleteClient)
|
||||
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("/", handlers.ListCustomers)
|
||||
r.Post("/", handlers.CreateCustomer)
|
||||
r.Get("/{id}", handlers.ViewCustomer)
|
||||
r.Put("/{id}", handlers.UpdateCustomer)
|
||||
r.Delete("/{id}", handlers.DeleteCustomer)
|
||||
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")
|
||||
w.Write([]byte(`{"status":"ok","origin":"`+r.Header.Get("Origin")+`"}`))
|
||||
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")
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if waConnector == nil {
|
||||
w.Write([]byte(`{"status":"error","error":"WhatsApp connector not initialized"}`))
|
||||
return
|
||||
@@ -101,7 +108,7 @@ func main() {
|
||||
})
|
||||
|
||||
r.Get("/debug/net-test", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
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() + `}`))
|
||||
@@ -112,53 +119,73 @@ func main() {
|
||||
})
|
||||
|
||||
r.Route("/leads", func(r chi.Router) {
|
||||
r.Get("/", handlers.ListLeads)
|
||||
r.Get("/connect", handlers.LeadsConnectPage)
|
||||
r.Get("/qr", handlers.LeadsQR)
|
||||
r.Put("/{id}", handlers.UpdateLead)
|
||||
r.Delete("/{id}", handlers.DeleteLead)
|
||||
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("/", handlers.ListServices)
|
||||
r.Post("/", handlers.CreateService)
|
||||
r.Get("/{id}", handlers.ViewService)
|
||||
r.Put("/{id}", handlers.UpdateService)
|
||||
r.Delete("/{id}", handlers.DeleteService)
|
||||
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("/", handlers.ListSchedules)
|
||||
r.Post("/", handlers.CreateSchedule)
|
||||
r.Get("/{id}", handlers.ViewSchedule)
|
||||
r.Put("/{id}", handlers.UpdateSchedule)
|
||||
r.Delete("/{id}", handlers.DeleteSchedule)
|
||||
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("/", handlers.ListPayments)
|
||||
r.Post("/", handlers.CreatePayment)
|
||||
r.Get("/{id}", handlers.ViewPayment)
|
||||
r.Put("/{id}", handlers.UpdatePayment)
|
||||
r.Delete("/{id}", handlers.DeletePayment)
|
||||
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("/", handlers.ListQuestions)
|
||||
r.Post("/", handlers.CreateQuestion)
|
||||
r.Get("/{id}", handlers.ViewQuestion)
|
||||
r.Put("/{id}", handlers.UpdateQuestion)
|
||||
r.Delete("/{id}", handlers.DeleteQuestion)
|
||||
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("/", handlers.ListAnswers)
|
||||
r.Post("/", handlers.CreateAnswer)
|
||||
r.Get("/{id}", handlers.ViewAnswer)
|
||||
r.Put("/{id}", handlers.UpdateAnswer)
|
||||
r.Delete("/{id}", handlers.DeleteAnswer)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user