Files
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

131 lines
6.0 KiB
Go

package config
import (
"encoding/hex"
"errors"
"github.com/google/uuid"
"net/netip"
"net/url"
"os"
"regexp"
"strings"
)
var smsNumericID = regexp.MustCompile(`^[0-9]+$`)
type OnboardingConfig struct {
SaaSCaptchaDisabled bool
TencentCaptcha TencentCaptchaConfig
TencentSMSTemplateParams map[string][]string
Enabled, PauseClaims, ActivationEnabled, SMSLoginEnabled, AllowLocalHTTP bool
MasterKey, EntryURL, SMSGatewayURL, SMSGatewayToken, SMSProvider string
TencentSMSSecretID, TencentSMSSecretKey, TencentSMSAppID string
TencentSMSSignName, TencentSMSTemplateID string
TencentSMSTemplates map[string]string
WeChatAppID, WeChatAppSecret string
TrustedProxyCIDRs, SelfSignupTenantIDs []string
}
func loadOnboardingConfig(secrets map[string]string) (OnboardingConfig, error) {
c := OnboardingConfig{MasterKey: secrets["ONBOARDING_MASTER_KEY"], SMSGatewayToken: secrets["ONBOARDING_SMS_GATEWAY_TOKEN"], EntryURL: strings.TrimSpace(os.Getenv("ONBOARDING_ENTRY_URL")), SMSGatewayURL: strings.TrimSpace(os.Getenv("ONBOARDING_SMS_GATEWAY_URL")), SMSProvider: strings.TrimSpace(os.Getenv("ONBOARDING_SMS_PROVIDER")), TencentSMSSecretID: secrets["TENCENT_SMS_SECRET_ID"], TencentSMSSecretKey: secrets["TENCENT_SMS_SECRET_KEY"], TencentSMSAppID: strings.TrimSpace(os.Getenv("TENCENT_SMS_SDK_APP_ID")), TencentSMSSignName: strings.TrimSpace(os.Getenv("TENCENT_SMS_SIGN_NAME")), TencentSMSTemplateID: strings.TrimSpace(os.Getenv("TENCENT_SMS_TEMPLATE_ID"))}
c.WeChatAppID = strings.TrimSpace(os.Getenv("WECHAT_MINI_APP_ID"))
c.WeChatAppSecret = secrets["WECHAT_MINI_APP_SECRET"]
for name, target := range map[string]*bool{"ONBOARDING_SAAS_CAPTCHA_DISABLED": &c.SaaSCaptchaDisabled, "ONBOARDING_ENABLED": &c.Enabled, "ONBOARDING_PAUSE_CLAIMS": &c.PauseClaims, "ONBOARDING_ACTIVATION_ENABLED": &c.ActivationEnabled, "ONBOARDING_SMS_LOGIN_ENABLED": &c.SMSLoginEnabled, "ONBOARDING_ALLOW_LOCAL_HTTP": &c.AllowLocalHTTP} {
v, err := boolEnv(name, false)
if err != nil {
return c, err
}
*target = v
}
split := func(name string) []string {
items := []string{}
for _, v := range strings.Split(os.Getenv(name), ",") {
if strings.TrimSpace(v) != "" {
items = append(items, strings.TrimSpace(v))
}
}
return items
}
c.TrustedProxyCIDRs = split("ONBOARDING_TRUSTED_PROXY_CIDRS")
c.SelfSignupTenantIDs = split("ONBOARDING_SELF_SIGNUP_TENANT_IDS")
return c, nil
}
func (c OnboardingConfig) validate(environment string) error {
if c.WeChatAppSecret != "" {
if len(c.WeChatAppID) != 18 || !strings.HasPrefix(c.WeChatAppID, "wx") || len(c.WeChatAppSecret) < 16 {
return errors.New("WeChat login requires WECHAT_MINI_APP_ID and WECHAT_MINI_APP_SECRET")
}
if _, err := hex.DecodeString(c.WeChatAppID[2:]); err != nil {
return errors.New("invalid WECHAT_MINI_APP_ID")
}
}
if c.AllowLocalHTTP && environment != EnvironmentLocal && environment != EnvironmentTest {
return errors.New("ONBOARDING_ALLOW_LOCAL_HTTP is limited to local/test")
}
if c.MasterKey == "" {
if c.Enabled || c.ActivationEnabled || c.SMSLoginEnabled {
return errors.New("onboarding requires ONBOARDING_MASTER_KEY")
}
return nil
}
key, err := hex.DecodeString(c.MasterKey)
if err != nil || len(key) != 32 {
return errors.New("ONBOARDING_MASTER_KEY must encode 32 bytes as hex")
}
u, err := url.Parse(c.EntryURL)
if err != nil || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || (u.Scheme != "https" && !(c.AllowLocalHTTP && u.Scheme == "http" && (u.Hostname() == "localhost" || u.Hostname() == "127.0.0.1"))) {
return errors.New("ONBOARDING_ENTRY_URL must be a fixed HTTPS URL")
}
for _, cidr := range c.TrustedProxyCIDRs {
p, err := netip.ParsePrefix(cidr)
if err != nil || p.Bits() == 0 {
return errors.New("onboarding trusted proxies must be explicit non-global CIDRs")
}
}
for _, id := range c.SelfSignupTenantIDs {
if _, err := uuid.Parse(id); err != nil {
return errors.New("onboarding signup tenants must be UUIDs")
}
}
if c.SMSLoginEnabled {
switch c.SMSProvider {
case "", "gateway":
u, err := url.Parse(c.SMSGatewayURL)
if err != nil || u.Scheme != "https" || u.Host == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || c.SMSGatewayToken == "" {
return errors.New("onboarding SMS requires HTTPS gateway and token")
}
case "tencent":
if c.TencentSMSSecretID == "" || c.TencentSMSSecretKey == "" || !smsNumericID.MatchString(c.TencentSMSAppID) || c.TencentSMSSignName == "" {
return errors.New("Tencent SMS requires SecretId, SecretKey, SDK AppID and sign name")
}
if c.TencentSMSTemplates != nil {
for _, name := range []string{"login", "register", "change_password", "change_phone"} {
if !smsNumericID.MatchString(c.TencentSMSTemplates[name]) {
return errors.New("Tencent SMS requires numeric login, register, change_password and change_phone template IDs")
}
}
} else if !smsNumericID.MatchString(c.TencentSMSTemplateID) {
return errors.New("Tencent SMS requires a numeric template ID")
}
for name, params := range c.TencentSMSTemplateParams {
if name != "login" && name != "register" && name != "change_password" && name != "change_phone" {
return errors.New("Tencent SMS template_params contains an unknown purpose")
}
seen := map[string]bool{}
for _, value := range params {
if (value != "code" && value != "expires_minutes") || seen[value] {
return errors.New("Tencent SMS template_params must contain code and optionally expires_minutes without duplicates")
}
seen[value] = true
}
if !seen["code"] {
return errors.New("Tencent SMS template_params must include code")
}
}
default:
return errors.New("ONBOARDING_SMS_PROVIDER must be gateway or tencent")
}
}
return nil
}