cf66ed7e26
Tests · Go / test (push) Successful in 7s
Tests · Integration / integration (push) Successful in 12s
Tests · Go / test (pull_request) Successful in 6s
Tests · Integration / integration (pull_request) Successful in 11s
Tests · UI / test (pull_request) Successful in 19s
New platform/telegram connector (own container, bot token only there): - go-telegram/bot long-poll loop: /start deep-links + Mini App launch button. - gRPC API pkg/proto/telegram/v1 (Telegram service): ValidateInitData, Notify (renders a localized message + deep-link button), SendToUser/SendToGameChannel (admin, wired in Stage 10). Generic methods are platform-agnostic (external_id). - Bot API base override for Telegram's test environment; Dockerfile + compose (VPN sidecar, no public ingress); README. Gateway: - initData validation relocated from the gateway into the connector; the gateway calls ValidateInitData over gRPC (GATEWAY_CONNECTOR_ADDR), drops the bot token, and deletes internal/auth. - Out-of-app push: runPushPump routes events whose recipient has no live in-app stream to connector.Notify, gated by /internal/push-target + the in-app-only flag (race-free de-dup); HasSubscribers added to the push hub. Backend: - Migration 00007 accounts.notifications_in_app_only (default true) + jetgen. - ProvisionTelegram seeds a new account's language/display name from the launch fields; IdentityExternalID reverse lookup; /internal/push-target handler. UI: - Telegram Mini App launch: detect initData, apply themeParams, authTelegram, route the deep-link start_param (g/i/f); /telegram/ guard redirects outside Telegram. Vite relative base + telegram-web-app.js. In-app-only profile toggle; share-to-Telegram link for a friend code. Vitest + Playwright coverage. Wire/docs/CI: fbs Profile/UpdateProfileRequest gain notifications_in_app_only (Go + TS); go.work uses ./platform/telegram; go-unit.yaml covers it; PLAN, ARCHITECTURE, FUNCTIONAL (+ru), UI_DESIGN, READMEs updated.
177 lines
6.0 KiB
Go
177 lines
6.0 KiB
Go
// Package config loads and validates the gateway's runtime configuration from
|
|
// the process environment. Every variable is prefixed GATEWAY_.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// Config holds the gateway's runtime configuration.
|
|
type Config struct {
|
|
// HTTPAddr is the public Connect/h2c listener address (host:port).
|
|
HTTPAddr string
|
|
// AdminAddr is the admin reverse-proxy listener address. Admin is enabled only
|
|
// when AdminUser and AdminPassword are also set.
|
|
AdminAddr string
|
|
// LogLevel is the zap log level: "debug", "info", "warn" or "error".
|
|
LogLevel string
|
|
// BackendHTTPURL is the base URL of the backend REST API (gateway -> backend).
|
|
BackendHTTPURL string
|
|
// BackendGRPCAddr is the backend push gRPC address the gateway subscribes to.
|
|
BackendGRPCAddr string
|
|
// BackendTimeout bounds a single backend REST call.
|
|
BackendTimeout time.Duration
|
|
// AdminUser and AdminPassword are the Basic-Auth credentials the gateway
|
|
// checks before proxying admin traffic to the backend. Empty disables admin.
|
|
AdminUser string
|
|
AdminPassword string
|
|
// ConnectorAddr is the gRPC address of the Telegram connector side-service. The
|
|
// gateway calls it to validate Mini App initData and to deliver out-of-app push.
|
|
// Empty disables the telegram auth path and the out-of-app push channel.
|
|
ConnectorAddr string
|
|
// SessionTTL bounds how long a resolved session stays cached; SessionCacheMax
|
|
// caps the number of cached sessions.
|
|
SessionTTL time.Duration
|
|
SessionCacheMax int
|
|
// PushHeartbeatInterval is the idle keep-alive cadence on a client live stream.
|
|
PushHeartbeatInterval time.Duration
|
|
// RateLimit configures the in-memory anti-abuse limiter.
|
|
RateLimit RateLimitConfig
|
|
}
|
|
|
|
// RateLimitConfig holds the token-bucket limits per class. Public and admin are
|
|
// keyed per client IP; the authenticated class is keyed per user id; the email
|
|
// sub-limit guards the costly email-code path per IP.
|
|
type RateLimitConfig struct {
|
|
PublicPerMinute int
|
|
PublicBurst int
|
|
UserPerMinute int
|
|
UserBurst int
|
|
AdminPerMinute int
|
|
AdminBurst int
|
|
EmailPer10Min int
|
|
EmailBurst int
|
|
}
|
|
|
|
// Defaults applied when the corresponding environment variable is unset.
|
|
const (
|
|
defaultHTTPAddr = ":8081"
|
|
defaultAdminAddr = ":8082"
|
|
defaultLogLevel = "info"
|
|
defaultBackendHTTPURL = "http://localhost:8080"
|
|
defaultBackendGRPCAddr = "localhost:9090"
|
|
defaultBackendTimeout = 5 * time.Second
|
|
defaultSessionTTL = 10 * time.Minute
|
|
defaultSessionCacheMax = 50000
|
|
defaultPushHeartbeatInterval = 15 * time.Second
|
|
)
|
|
|
|
// DefaultRateLimit returns the built-in anti-abuse limits.
|
|
func DefaultRateLimit() RateLimitConfig {
|
|
return RateLimitConfig{
|
|
PublicPerMinute: 30, PublicBurst: 10,
|
|
UserPerMinute: 120, UserBurst: 40,
|
|
AdminPerMinute: 60, AdminBurst: 20,
|
|
EmailPer10Min: 5, EmailBurst: 2,
|
|
}
|
|
}
|
|
|
|
// Load reads the configuration from the environment, applies defaults, and
|
|
// validates the result.
|
|
func Load() (Config, error) {
|
|
var err error
|
|
c := Config{
|
|
HTTPAddr: envOr("GATEWAY_HTTP_ADDR", defaultHTTPAddr),
|
|
AdminAddr: envOr("GATEWAY_ADMIN_ADDR", defaultAdminAddr),
|
|
LogLevel: envOr("GATEWAY_LOG_LEVEL", defaultLogLevel),
|
|
BackendHTTPURL: envOr("GATEWAY_BACKEND_HTTP_URL", defaultBackendHTTPURL),
|
|
BackendGRPCAddr: envOr("GATEWAY_BACKEND_GRPC_ADDR", defaultBackendGRPCAddr),
|
|
AdminUser: os.Getenv("GATEWAY_ADMIN_USER"),
|
|
AdminPassword: os.Getenv("GATEWAY_ADMIN_PASSWORD"),
|
|
ConnectorAddr: os.Getenv("GATEWAY_CONNECTOR_ADDR"),
|
|
SessionCacheMax: defaultSessionCacheMax,
|
|
RateLimit: DefaultRateLimit(),
|
|
}
|
|
if c.BackendTimeout, err = envDuration("GATEWAY_BACKEND_TIMEOUT", defaultBackendTimeout); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.SessionTTL, err = envDuration("GATEWAY_SESSION_TTL", defaultSessionTTL); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.SessionCacheMax, err = envInt("GATEWAY_SESSION_CACHE_MAX", defaultSessionCacheMax); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if c.PushHeartbeatInterval, err = envDuration("GATEWAY_PUSH_HEARTBEAT_INTERVAL", defaultPushHeartbeatInterval); err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := c.validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// AdminEnabled reports whether the admin proxy should be served (an address and
|
|
// both Basic-Auth credentials are configured).
|
|
func (c Config) AdminEnabled() bool {
|
|
return c.AdminAddr != "" && c.AdminUser != "" && c.AdminPassword != ""
|
|
}
|
|
|
|
// validate reports whether the configuration values are acceptable.
|
|
func (c Config) validate() error {
|
|
switch c.LogLevel {
|
|
case "debug", "info", "warn", "error":
|
|
default:
|
|
return fmt.Errorf("config: invalid GATEWAY_LOG_LEVEL %q", c.LogLevel)
|
|
}
|
|
if c.HTTPAddr == "" {
|
|
return fmt.Errorf("config: GATEWAY_HTTP_ADDR must not be empty")
|
|
}
|
|
if c.BackendHTTPURL == "" {
|
|
return fmt.Errorf("config: GATEWAY_BACKEND_HTTP_URL must not be empty")
|
|
}
|
|
if c.BackendGRPCAddr == "" {
|
|
return fmt.Errorf("config: GATEWAY_BACKEND_GRPC_ADDR must not be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// envOr returns the value of the environment variable named key, or fallback
|
|
// when the variable is unset or empty.
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
// envInt parses the environment variable named key as an int, returning fallback
|
|
// when it is unset and an error when it is set but malformed.
|
|
func envInt(key string, fallback int) (int, error) {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return fallback, nil
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("config: %s: %w", key, err)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
// envDuration parses the environment variable named key as a Go duration,
|
|
// returning fallback when it is unset and an error when it is set but malformed.
|
|
func envDuration(key string, fallback time.Duration) (time.Duration, error) {
|
|
v := os.Getenv(key)
|
|
if v == "" {
|
|
return fallback, nil
|
|
}
|
|
d, err := time.ParseDuration(v)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("config: %s: %w", key, err)
|
|
}
|
|
return d, nil
|
|
}
|