feat: sync configurable OTP delivery providers
This commit is contained in:
parent
c18f773701
commit
6af61f26ba
28 changed files with 2100 additions and 118 deletions
|
|
@ -4,6 +4,7 @@ package config
|
|||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -112,6 +113,9 @@ type Config struct {
|
|||
DevAuthCode string
|
||||
// AuthCodeTTL 是登录/注册/邮箱验证 code 的有效期。
|
||||
AuthCodeTTL time.Duration
|
||||
// PhoneCodeLength 是使用外部 provider 时生成的短信验证码长度。development
|
||||
// provider 继续使用 DevAuthCode 原样,不受此字段影响。
|
||||
PhoneCodeLength int
|
||||
// AuthCodeMaxAttempts 是同一 phone_code_hash / email verification code 的最大错误次数。
|
||||
// 达到上限后验证码立即失效,用户必须重发。
|
||||
AuthCodeMaxAttempts int
|
||||
|
|
@ -127,7 +131,16 @@ type Config struct {
|
|||
LoginEmailRequireSetup bool
|
||||
// LoginEmailCodeLength 是邮箱验证码长度。
|
||||
LoginEmailCodeLength int
|
||||
// SMTP* 是登录邮箱验证码的出站邮件配置。LoginEmailEnable=true 时必须可用。
|
||||
// PhoneCodeDeliveryProvider 选择普通登录/注册与改号验证码的投递方式:
|
||||
// development 保留固定码与 777000 app-code;webhook 使用随机 SMS code。
|
||||
PhoneCodeDeliveryProvider string
|
||||
// EmailCodeDeliveryProvider 选择登录邮箱与邮箱 setup/change 的投递方式。
|
||||
EmailCodeDeliveryProvider string
|
||||
// OTPWebhook* 定义固定 v1 webhook 协议的端点、HMAC secret 与请求超时。
|
||||
OTPWebhookURL string
|
||||
OTPWebhookSecret string
|
||||
OTPWebhookTimeout time.Duration
|
||||
// SMTP* 是 email provider=smtp 时使用的出站邮件配置。
|
||||
SMTPHost string
|
||||
SMTPPort int
|
||||
SMTPUsername string
|
||||
|
|
@ -456,6 +469,7 @@ func Load() (Config, error) {
|
|||
|
||||
DevAuthCode: envOr("TELESRV_DEV_AUTH_CODE", "12345"),
|
||||
AuthCodeTTL: envDurationOr("TELESRV_AUTH_CODE_TTL", 5*time.Minute),
|
||||
PhoneCodeLength: envIntOr("TELESRV_PHONE_CODE_LENGTH", 5),
|
||||
AuthCodeMaxAttempts: envIntOr("TELESRV_AUTH_CODE_MAX_ATTEMPTS", 5),
|
||||
AuthCodePhoneRateLimit: envIntOr("TELESRV_AUTH_CODE_PHONE_RATE_LIMIT", 5),
|
||||
AuthCodeAuthKeyRateLimit: envIntOr("TELESRV_AUTH_CODE_AUTH_KEY_RATE_LIMIT", 20),
|
||||
|
|
@ -463,6 +477,11 @@ func Load() (Config, error) {
|
|||
LoginEmailEnable: envBoolOr("TELESRV_LOGIN_EMAIL_ENABLE", false),
|
||||
LoginEmailRequireSetup: envBoolOr("TELESRV_LOGIN_EMAIL_REQUIRE_SETUP", false),
|
||||
LoginEmailCodeLength: envIntOr("TELESRV_LOGIN_EMAIL_CODE_LENGTH", 6),
|
||||
PhoneCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_PHONE_CODE_DELIVERY_PROVIDER", "development"))),
|
||||
EmailCodeDeliveryProvider: strings.ToLower(strings.TrimSpace(envOr("TELESRV_EMAIL_CODE_DELIVERY_PROVIDER", "smtp"))),
|
||||
OTPWebhookURL: envOr("TELESRV_OTP_WEBHOOK_URL", ""),
|
||||
OTPWebhookSecret: envOr("TELESRV_OTP_WEBHOOK_SECRET", ""),
|
||||
OTPWebhookTimeout: envDurationOr("TELESRV_OTP_WEBHOOK_TIMEOUT", 5*time.Second),
|
||||
SMTPHost: envOr("TELESRV_SMTP_HOST", ""),
|
||||
SMTPPort: envIntOr("TELESRV_SMTP_PORT", 587),
|
||||
SMTPUsername: envOr("TELESRV_SMTP_USERNAME", ""),
|
||||
|
|
@ -619,6 +638,9 @@ func validateLoginEmailConfig(cfg Config) error {
|
|||
if cfg.AuthCodeMaxAttempts <= 0 {
|
||||
return fmt.Errorf("TELESRV_AUTH_CODE_MAX_ATTEMPTS must be positive")
|
||||
}
|
||||
if cfg.PhoneCodeLength < 4 || cfg.PhoneCodeLength > 10 {
|
||||
return fmt.Errorf("TELESRV_PHONE_CODE_LENGTH must be between 4 and 10")
|
||||
}
|
||||
if cfg.LoginEmailCodeLength < 4 || cfg.LoginEmailCodeLength > 10 {
|
||||
return fmt.Errorf("TELESRV_LOGIN_EMAIL_CODE_LENGTH must be between 4 and 10")
|
||||
}
|
||||
|
|
@ -627,7 +649,28 @@ func validateLoginEmailConfig(cfg Config) error {
|
|||
default:
|
||||
return fmt.Errorf("TELESRV_SMTP_TLS must be starttls, tls, or none")
|
||||
}
|
||||
if !cfg.LoginEmailEnable {
|
||||
switch cfg.PhoneCodeDeliveryProvider {
|
||||
case "development", "webhook":
|
||||
default:
|
||||
return fmt.Errorf("TELESRV_PHONE_CODE_DELIVERY_PROVIDER must be development or webhook")
|
||||
}
|
||||
switch cfg.EmailCodeDeliveryProvider {
|
||||
case "smtp", "webhook":
|
||||
default:
|
||||
return fmt.Errorf("TELESRV_EMAIL_CODE_DELIVERY_PROVIDER must be smtp or webhook")
|
||||
}
|
||||
webhookEnabled := cfg.PhoneCodeDeliveryProvider == "webhook" ||
|
||||
(cfg.LoginEmailEnable && cfg.EmailCodeDeliveryProvider == "webhook")
|
||||
if webhookEnabled {
|
||||
if cfg.OTPWebhookTimeout <= 0 {
|
||||
return fmt.Errorf("TELESRV_OTP_WEBHOOK_TIMEOUT must be positive")
|
||||
}
|
||||
u, err := url.Parse(strings.TrimSpace(cfg.OTPWebhookURL))
|
||||
if err != nil || u.Host == "" || u.User != nil || (u.Scheme != "http" && u.Scheme != "https") {
|
||||
return fmt.Errorf("TELESRV_OTP_WEBHOOK_URL must be an absolute http(s) URL without userinfo")
|
||||
}
|
||||
}
|
||||
if !cfg.LoginEmailEnable || cfg.EmailCodeDeliveryProvider == "webhook" {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(cfg.SMTPHost) == "" {
|
||||
|
|
|
|||
|
|
@ -35,13 +35,13 @@ func TestLoadDefaultsAdvertiseIPToLoopback(t *testing.T) {
|
|||
|
||||
func TestLoadUsesExplicitAdvertiseIP(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_ADVERTISE_IP", "203.0.113.10")
|
||||
t.Setenv("TELESRV_ADVERTISE_IP", "192.0.2.10")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.AdvertiseIP != "203.0.113.10" {
|
||||
if cfg.AdvertiseIP != "192.0.2.10" {
|
||||
t.Fatalf("AdvertiseIP = %q, want explicit env", cfg.AdvertiseIP)
|
||||
}
|
||||
}
|
||||
|
|
@ -199,6 +199,7 @@ func TestLoadLoginEmailDefaultsDisabled(t *testing.T) {
|
|||
t.Fatal("LoginEmailRequireSetup = true, want false")
|
||||
}
|
||||
if cfg.AuthCodeTTL != 5*time.Minute || cfg.AuthCodeMaxAttempts != 5 || cfg.LoginEmailCodeLength != 6 ||
|
||||
cfg.PhoneCodeLength != 5 || cfg.PhoneCodeDeliveryProvider != "development" || cfg.EmailCodeDeliveryProvider != "smtp" ||
|
||||
cfg.AuthCodePhoneRateLimit != 5 || cfg.AuthCodeAuthKeyRateLimit != 20 || cfg.AuthCodeRateWindow != 10*time.Minute {
|
||||
t.Fatalf("auth/login email defaults = ttl=%v attempts=%d length=%d phone_limit=%d key_limit=%d window=%v",
|
||||
cfg.AuthCodeTTL, cfg.AuthCodeMaxAttempts, cfg.LoginEmailCodeLength,
|
||||
|
|
@ -251,6 +252,50 @@ func TestLoadLoginEmailRequiresSMTPWhenEnabled(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadLoginEmailWebhookDoesNotRequireSMTP(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_LOGIN_EMAIL_ENABLE", "true")
|
||||
t.Setenv("TELESRV_EMAIL_CODE_DELIVERY_PROVIDER", "webhook")
|
||||
t.Setenv("TELESRV_OTP_WEBHOOK_URL", "https://otp.example.test/v1/deliveries")
|
||||
t.Setenv("TELESRV_OTP_WEBHOOK_SECRET", "test-secret")
|
||||
t.Setenv("TELESRV_OTP_WEBHOOK_TIMEOUT", "3s")
|
||||
t.Setenv("TELESRV_SMTP_HOST", "")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.EmailCodeDeliveryProvider != "webhook" || cfg.OTPWebhookURL != "https://otp.example.test/v1/deliveries" ||
|
||||
cfg.OTPWebhookSecret != "test-secret" || cfg.OTPWebhookTimeout != 3*time.Second {
|
||||
t.Fatalf("webhook config = %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPhoneWebhookRequiresValidURL(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_PHONE_CODE_DELIVERY_PROVIDER", "webhook")
|
||||
t.Setenv("TELESRV_OTP_WEBHOOK_URL", "relative/path")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("Load succeeded with relative OTP webhook URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPhoneWebhookConfig(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_PHONE_CODE_DELIVERY_PROVIDER", "webhook")
|
||||
t.Setenv("TELESRV_PHONE_CODE_LENGTH", "7")
|
||||
t.Setenv("TELESRV_OTP_WEBHOOK_URL", "http://127.0.0.1:8080/otp")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.PhoneCodeDeliveryProvider != "webhook" || cfg.PhoneCodeLength != 7 {
|
||||
t.Fatalf("phone webhook config = %#v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadKeepsAdminAndRtmpDefaultPortsSeparate(t *testing.T) {
|
||||
disableDefaultConfigFile(t)
|
||||
t.Setenv("TELESRV_ADMIN_UI_ADDR", "")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue