2026-09-12 08:57:29 +08:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"errors"
|
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"net/netip"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
2026-10-08 10:06:44 +08:00
|
|
|
"regexp"
|
2026-09-12 08:57:29 +08:00
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-08 10:06:44 +08:00
|
|
|
var smsNumericID = regexp.MustCompile(`^[0-9]+$`)
|
|
|
|
|
|
2026-09-12 08:57:29 +08:00
|
|
|
type OnboardingConfig struct {
|
2026-10-08 10:06:44 +08:00
|
|
|
SaaSCaptchaDisabled bool
|
|
|
|
|
TencentCaptcha TencentCaptchaConfig
|
|
|
|
|
TencentSMSTemplateParams map[string][]string
|
2026-09-12 08:57:29 +08:00
|
|
|
Enabled, PauseClaims, ActivationEnabled, SMSLoginEnabled, AllowLocalHTTP bool
|
2026-10-08 10:06:44 +08:00
|
|
|
MasterKey, EntryURL, SMSGatewayURL, SMSGatewayToken, SMSProvider string
|
|
|
|
|
TencentSMSSecretID, TencentSMSSecretKey, TencentSMSAppID string
|
|
|
|
|
TencentSMSSignName, TencentSMSTemplateID string
|
|
|
|
|
TencentSMSTemplates map[string]string
|
|
|
|
|
WeChatAppID, WeChatAppSecret string
|
2026-09-12 08:57:29 +08:00
|
|
|
TrustedProxyCIDRs, SelfSignupTenantIDs []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func loadOnboardingConfig(secrets map[string]string) (OnboardingConfig, error) {
|
2026-10-08 10:06:44 +08:00
|
|
|
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} {
|
2026-09-12 08:57:29 +08:00
|
|
|
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 {
|
2026-10-08 10:06:44 +08:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-12 08:57:29 +08:00
|
|
|
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 {
|
2026-10-08 10:06:44 +08:00
|
|
|
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")
|
2026-09-12 08:57:29 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|