Head of Product & Engineering 9995027093
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
feat: per-location Cast API key and unified admin config API
Each GHL location can now have its own Cast API key and sender ID stored
in MongoDB. Falls back to global CAST_API_KEY / CAST_SENDER_ID env vars
when not set per-location.

Admin endpoints (all require Authorization: Bearer <INBOUND_API_KEY>):
  GET  /api/admin/locations                        — list all locations
  GET  /api/admin/locations/{locationId}/config    — get location config
  PUT  /api/admin/locations/{locationId}/config    — set sender_id + cast_api_key

Cast API key is masked in GET responses (first 12 chars + "...").
Replaces the /sender-id endpoint deployed in the previous commit.

Also adds FUTURE_DEV.md documenting the migration path to Infisical
for secret management, plus MongoDB security hardening checklist.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-06 14:08:29 +02:00

100 lines
2.8 KiB
Go

package main
import (
"context"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"git.sds.dev/CAST/cast-ghl-plugin/internal/cast"
"git.sds.dev/CAST/cast-ghl-plugin/internal/config"
"git.sds.dev/CAST/cast-ghl-plugin/internal/ghl"
"git.sds.dev/CAST/cast-ghl-plugin/internal/store"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})))
if err := run(); err != nil {
slog.Error("fatal error", "err", err)
os.Exit(1)
}
}
func run() error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("config: %w", err)
}
ctx := context.Background()
s, err := store.NewStore(ctx, cfg.MongoURI)
if err != nil {
return fmt.Errorf("mongodb: %w", err)
}
defer func() { _ = s.Close(ctx) }()
castClient := cast.NewClient(cfg.CastAPIURL, cfg.CastAPIKey, cfg.CastSenderID)
ghlAPI := ghl.NewAPIClient()
oauthHandler := ghl.NewOAuthHandler(cfg.GHLClientID, cfg.GHLClientSecret, cfg.BaseURL, cfg.GHLConversationProviderID, s)
webhookHandler, err := ghl.NewWebhookHandler(cfg.GHLWebhookPublicKey, castClient, ghlAPI, oauthHandler, s)
if err != nil {
return fmt.Errorf("webhook handler: %w", err)
}
adminHandler := ghl.NewAdminHandler(cfg.InboundAPIKey, s)
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(60 * time.Second))
r.Get("/health", healthCheck)
r.Get("/install", oauthHandler.HandleInstall)
r.Get("/oauth-callback", oauthHandler.HandleCallback)
r.Post("/api/ghl/v1/webhook/messages", webhookHandler.HandleWebhook)
r.Post("/api/ghl/v1/webhook/uninstall", webhookHandler.HandleUninstall)
r.Get("/api/admin/locations", adminHandler.HandleListLocations)
r.Get("/api/admin/locations/{locationId}/config", adminHandler.HandleGetLocationConfig)
r.Put("/api/admin/locations/{locationId}/config", adminHandler.HandleSetLocationConfig)
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
slog.Info("shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
slog.Error("shutdown error", "err", err)
}
}()
slog.Info("cast-ghl-provider started", "port", cfg.Port, "base_url", cfg.BaseURL)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
return fmt.Errorf("server: %w", err)
}
return nil
}
func healthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status":"ok","service":"cast-ghl-provider"}`))
}