Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Reference: P2E §3 — Webhook authentication registry
|
||||
// Each channel provider stores an "identifier" (bot_token, website_token, phone_number_id)
|
||||
// which is used to route and validate incoming webhook requests.
|
||||
|
||||
// WebhookTokenRegistry manages webhook authentication tokens per channel.
|
||||
// Tokens are loaded from DB on startup and refreshed when inboxes are created/updated.
|
||||
type WebhookTokenRegistry struct {
|
||||
mu sync.RWMutex
|
||||
tokens map[string]map[string]WebhookTokenEntry // channel_type → identifier → entry
|
||||
}
|
||||
|
||||
// WebhookTokenEntry stores a channel's webhook authentication data.
|
||||
type WebhookTokenEntry struct {
|
||||
InboxID uint
|
||||
AccountID uint
|
||||
Secret string // HMAC secret for verification (varies by channel)
|
||||
Identifier string // unique identifier for routing (bot_token, website_token, etc.)
|
||||
}
|
||||
|
||||
// NewWebhookTokenRegistry creates an empty token registry.
|
||||
func NewWebhookTokenRegistry() *WebhookTokenRegistry {
|
||||
return &WebhookTokenRegistry{
|
||||
tokens: make(map[string]map[string]WebhookTokenEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a webhook token entry for a channel.
|
||||
func (r *WebhookTokenRegistry) Register(channelType, identifier string, entry WebhookTokenEntry) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.tokens[channelType] == nil {
|
||||
r.tokens[channelType] = make(map[string]WebhookTokenEntry)
|
||||
}
|
||||
r.tokens[channelType][identifier] = entry
|
||||
}
|
||||
|
||||
// Unregister removes a webhook token entry.
|
||||
func (r *WebhookTokenRegistry) Unregister(channelType, identifier string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.tokens[channelType] != nil {
|
||||
delete(r.tokens[channelType], identifier)
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup finds a webhook token entry by channel type and identifier.
|
||||
func (r *WebhookTokenRegistry) Lookup(channelType, identifier string) (*WebhookTokenEntry, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
if r.tokens[channelType] == nil {
|
||||
return nil, false
|
||||
}
|
||||
entry, ok := r.tokens[channelType][identifier]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &entry, true
|
||||
}
|
||||
|
||||
// Validate checks a webhook request against the stored token entry.
|
||||
// The validation strategy depends on the channel type:
|
||||
// - Telegram: X-Telegram-Bot-Api-Secret-Token header
|
||||
// - Web Widget: HMAC token verification
|
||||
// - Facebook/WhatsApp: app secret + signature verification
|
||||
func (r *WebhookTokenRegistry) Validate(channelType, identifier string, req *http.Request) (bool, error) {
|
||||
entry, found := r.Lookup(channelType, identifier)
|
||||
if !found {
|
||||
return false, nil // unknown identifier → reject
|
||||
}
|
||||
|
||||
switch channelType {
|
||||
case "telegram":
|
||||
// Telegram validates via X-Telegram-Bot-Api-Secret-Token header
|
||||
token := req.Header.Get("X-Telegram-Bot-Api-Secret-Token")
|
||||
if entry.Secret == "" {
|
||||
return true, nil // no secret configured → accept all
|
||||
}
|
||||
return token == entry.Secret, nil
|
||||
|
||||
case "web_widget":
|
||||
// Web widget HMAC verification (handled separately in WebSocket auth)
|
||||
return true, nil
|
||||
|
||||
case "facebook":
|
||||
// Facebook webhook validation: app secret + X-Hub-Signature-256
|
||||
// TODO: implement Facebook signature verification (P8)
|
||||
return true, nil
|
||||
|
||||
case "whatsapp":
|
||||
// WhatsApp webhook validation: Bearer token verification
|
||||
// TODO: implement WhatsApp token verification (P8)
|
||||
return true, nil
|
||||
|
||||
default:
|
||||
return true, nil // unknown channel type → accept (will be filtered by registry)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllIdentifiers returns all registered identifiers for a channel type.
|
||||
func (r *WebhookTokenRegistry) GetAllIdentifiers(channelType string) []WebhookTokenEntry {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
entries := []WebhookTokenEntry{}
|
||||
if r.tokens[channelType] == nil {
|
||||
return entries
|
||||
}
|
||||
for _, entry := range r.tokens[channelType] {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
Reference in New Issue
Block a user