feat: add kafka admin integration

This commit is contained in:
richjohnrambo
2026-08-28 00:05:54 +08:00
parent b2e5097708
commit 5cc6eebc1a
48 changed files with 8065 additions and 55 deletions
+260
View File
@@ -0,0 +1,260 @@
package main
import (
"bytes"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
type adminInternalAPIConfig struct {
Enabled bool `json:"enabled"`
BaseURL string `json:"base_url"`
ServiceName string `json:"service_name"`
Secret string `json:"secret"`
TimeoutMS int `json:"timeout_ms"`
}
type adminInternalClient struct {
config adminInternalAPIConfig
http *http.Client
}
type adminInternalTenantDTO struct {
ID types.TenantID `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
TenantDesc *string `json:"tenant_desc,omitempty"`
DBName string `json:"db_name"`
DBDSN string `json:"db_dsn"`
State types.TenantState `json:"state"`
CreatedAt time.Time `json:"created_at"`
CreatedBy int64 `json:"created_by"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy int64 `json:"updated_by"`
}
func initAdminInternalAPI(raw json.RawMessage) error {
store.SetAdminInternalConfigProvider(nil)
if len(raw) == 0 || string(raw) == "null" {
return nil
}
var cfg adminInternalAPIConfig
if err := json.Unmarshal(raw, &cfg); err != nil {
return err
}
cfg.BaseURL = strings.TrimRight(strings.TrimSpace(cfg.BaseURL), "/")
cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
if cfg.ServiceName == "" {
cfg.ServiceName = "im-server"
}
if cfg.TimeoutMS <= 0 {
cfg.TimeoutMS = 3000
}
if !cfg.Enabled {
return nil
}
if cfg.BaseURL == "" || strings.TrimSpace(cfg.Secret) == "" {
return errors.New("admin_internal_api enabled but base_url or secret is empty")
}
store.SetAdminInternalConfigProvider(&adminInternalClient{
config: cfg,
http: &http.Client{Timeout: time.Duration(cfg.TimeoutMS) * time.Millisecond},
})
logs.Info.Printf("Admin internal config client enabled: base_url=%s service=%s", cfg.BaseURL, cfg.ServiceName)
return nil
}
func (c *adminInternalClient) GetTenantByCode(code string) (*types.Tenant, error) {
var tenant adminInternalTenantDTO
if err := c.getJSON("/internal/im-config/tenants/by-code/"+url.PathEscape(strings.TrimSpace(code)), nil, &tenant); err != nil {
return nil, err
}
return tenant.toStoreTenant(), nil
}
func (c *adminInternalClient) GetTenantByID(id types.TenantID) (*types.Tenant, error) {
if !id.IsValid() {
return nil, types.ErrMalformed
}
var tenant adminInternalTenantDTO
if err := c.getJSON("/internal/im-config/tenants/by-id/"+strconv.FormatInt(int64(id), 10), nil, &tenant); err != nil {
return nil, err
}
return tenant.toStoreTenant(), nil
}
func (c *adminInternalClient) ListActiveTenants(limit int) ([]types.Tenant, error) {
query := url.Values{}
query.Set("state", "active")
if limit > 0 {
query.Set("limit", strconv.Itoa(limit))
}
var wrapper struct {
Items []adminInternalTenantDTO `json:"items"`
}
if err := c.getJSON("/internal/im-config/tenants", query, &wrapper); err != nil {
return nil, err
}
tenants := make([]types.Tenant, 0, len(wrapper.Items))
for _, item := range wrapper.Items {
if tenant := item.toStoreTenant(); tenant != nil {
tenants = append(tenants, *tenant)
}
}
return tenants, nil
}
func (c *adminInternalClient) RegistrationSMSEnabled() (bool, error) {
var wrapper struct {
Enabled bool `json:"enabled"`
}
if err := c.getJSON("/internal/im-config/registration/sms", nil, &wrapper); err != nil {
return true, err
}
return wrapper.Enabled, nil
}
func (c *adminInternalClient) ThirdPartyConfig(group string, keys ...string) (map[string]string, error) {
query := url.Values{}
normalized := make([]string, 0, len(keys))
for _, key := range keys {
if key = strings.TrimSpace(key); key != "" {
normalized = append(normalized, key)
}
}
if len(normalized) > 0 {
query.Set("keys", strings.Join(normalized, ","))
}
var wrapper struct {
Values map[string]string `json:"values"`
}
if err := c.getJSON("/internal/im-config/third-party/"+url.PathEscape(strings.TrimSpace(group)), query, &wrapper); err != nil {
return nil, err
}
if wrapper.Values == nil {
wrapper.Values = map[string]string{}
}
return wrapper.Values, nil
}
func (c *adminInternalClient) getJSON(path string, query url.Values, out any) error {
if c == nil || c.http == nil {
return types.ErrUnsupported
}
path = "/" + strings.TrimLeft(path, "/")
u, err := url.Parse(c.config.BaseURL + path)
if err != nil {
return err
}
if len(query) > 0 {
u.RawQuery = query.Encode()
}
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return err
}
if err = c.signRequest(req, nil); err != nil {
return err
}
resp, err := c.http.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("admin internal api status=%d body=%s", resp.StatusCode, string(body))
}
var parsed struct {
Code int `json:"code"`
Data json.RawMessage `json:"data"`
}
if err = json.Unmarshal(body, &parsed); err != nil {
return err
}
if parsed.Code != 0 {
return fmt.Errorf("admin internal api code=%d", parsed.Code)
}
if out == nil || len(parsed.Data) == 0 || string(parsed.Data) == "null" {
return nil
}
return json.Unmarshal(parsed.Data, out)
}
func (c *adminInternalClient) signRequest(req *http.Request, body []byte) error {
nonce, err := randomInternalNonce()
if err != nil {
return err
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
bodyHash := sha256.Sum256(body)
signingText := strings.Join([]string{
req.Method,
req.URL.RequestURI(),
hex.EncodeToString(bodyHash[:]),
timestamp,
nonce,
}, "\n")
req.Header.Set(internalHeaderService, c.config.ServiceName)
req.Header.Set(internalHeaderTimestamp, timestamp)
req.Header.Set(internalHeaderNonce, nonce)
req.Header.Set(internalHeaderSignature, signInternalRequest(c.config.Secret, signingText))
req.Header.Set("Accept", "application/json")
if len(body) > 0 {
req.Body = io.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body))
req.Header.Set("Content-Type", "application/json")
}
return nil
}
func randomInternalNonce() (string, error) {
var raw [16]byte
if _, err := rand.Read(raw[:]); err != nil {
return "", err
}
return hex.EncodeToString(raw[:]), nil
}
func signInternalRequest(secret, signingText string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(signingText))
return hex.EncodeToString(mac.Sum(nil))
}
func (t adminInternalTenantDTO) toStoreTenant() *types.Tenant {
if t.ID.IsZero() && strings.TrimSpace(t.Code) == "" {
return nil
}
return &types.Tenant{
ID: t.ID,
Code: strings.TrimSpace(t.Code),
Name: t.Name,
TenantDesc: t.TenantDesc,
DBName: t.DBName,
DBDSN: t.DBDSN,
State: t.State,
CreatedAt: t.CreatedAt,
CreatedBy: t.CreatedBy,
UpdatedAt: t.UpdatedAt,
UpdatedBy: t.UpdatedBy,
}
}