All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
Allows each GHL sub-account to use a different Cast sender ID instead of
the global CAST_SENDER_ID default.
- store.TokenRecord gains a sender_id field (MongoDB)
- store.UpdateSenderID method to set it per location
- cast.Client.SendSMS accepts a senderID override param (empty = use
client-level default)
- webhook.processOutbound reads the location's sender_id from the token
record and passes it to Cast
- new admin handler: PUT /api/admin/locations/{locationId}/sender-id
protected by Authorization: Bearer <INBOUND_API_KEY>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
79 lines
2.2 KiB
Go
79 lines
2.2 KiB
Go
package ghl
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// LocationConfigStore is the store interface used by AdminHandler.
|
|
type LocationConfigStore interface {
|
|
UpdateSenderID(ctx context.Context, locationID, senderID string) error
|
|
}
|
|
|
|
// AdminHandler exposes internal management endpoints.
|
|
// All routes require a valid Authorization: Bearer <adminKey> header.
|
|
type AdminHandler struct {
|
|
adminKey string
|
|
store LocationConfigStore
|
|
}
|
|
|
|
func NewAdminHandler(adminKey string, store LocationConfigStore) *AdminHandler {
|
|
return &AdminHandler{adminKey: adminKey, store: store}
|
|
}
|
|
|
|
// HandleSetSenderID sets or clears the per-location Cast sender ID.
|
|
//
|
|
// PUT /api/admin/locations/{locationId}/sender-id
|
|
// Authorization: Bearer <INBOUND_API_KEY>
|
|
// {"sender_id":"CAST"}
|
|
func (h *AdminHandler) HandleSetSenderID(w http.ResponseWriter, r *http.Request) {
|
|
if h.adminKey == "" || r.Header.Get("Authorization") != "Bearer "+h.adminKey {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
locationID := chi.URLParam(r, "locationId")
|
|
if locationID == "" {
|
|
http.Error(w, "missing locationId", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, 1024))
|
|
if err != nil {
|
|
http.Error(w, "failed to read body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var payload struct {
|
|
SenderID string `json:"sender_id"`
|
|
}
|
|
if err := json.Unmarshal(body, &payload); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := h.store.UpdateSenderID(ctx, locationID, payload.SenderID); err != nil {
|
|
slog.Error("admin: failed to update sender_id", "location_id", locationID, "err", err)
|
|
if err.Error() == "location not found" {
|
|
http.Error(w, "location not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
slog.Info("admin: sender_id updated", "location_id", locationID, "sender_id", payload.SenderID)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_, _ = w.Write([]byte(`{"ok":true}`))
|
|
}
|