184 lines
5.7 KiB
Go
184 lines
5.7 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Config struct {
|
|
AppEnv string
|
|
AppAddr string
|
|
MySQLDSN string
|
|
RedisAddr string
|
|
RedisPassword string
|
|
RedisDB int
|
|
JWTSecret string
|
|
PaymentConfigEncryptionKey string
|
|
ExternalUploadSecret string
|
|
ExternalUploadAllowedIPs []string
|
|
BootstrapAdminUsername string
|
|
BootstrapAdminPassword string
|
|
BootstrapAdminNickname string
|
|
Storage StorageConfig
|
|
SMS SMSConfig
|
|
Realname RealnameConfig
|
|
Log LogConfig
|
|
RateLimit RateLimitConfig
|
|
}
|
|
|
|
type StorageConfig struct {
|
|
Endpoint string
|
|
Bucket string
|
|
AccessKeyID string
|
|
SecretAccessKey string
|
|
}
|
|
|
|
type SMSConfig struct {
|
|
Provider string
|
|
AliyunAccessKeyID string
|
|
AliyunAccessKeySecret string
|
|
AliyunEndpoint string
|
|
AliyunSignName string
|
|
AliyunLoginTemplateCode string
|
|
}
|
|
|
|
type RealnameConfig struct {
|
|
Provider string
|
|
CloudMarketURL string
|
|
CloudMarketAppCode string
|
|
}
|
|
|
|
type LogConfig struct {
|
|
Level string
|
|
Dir string
|
|
EnableConsole bool
|
|
EnableFile bool
|
|
}
|
|
|
|
type RateLimitConfig struct {
|
|
Enabled bool
|
|
RequestsPerMinute int
|
|
}
|
|
|
|
func Load() Config {
|
|
return Config{
|
|
AppEnv: getEnv("APP_ENV", "development"),
|
|
AppAddr: getEnv("APP_ADDR", ":8080"),
|
|
MySQLDSN: getEnv("MYSQL_DSN", "hfb:secret@tcp(127.0.0.1:3306)/hfb_sys?charset=utf8mb4&parseTime=True&loc=Local"),
|
|
RedisAddr: getEnv("REDIS_ADDR", "127.0.0.1:6379"),
|
|
RedisPassword: getEnv("REDIS_PASSWORD", ""),
|
|
RedisDB: getEnvInt("REDIS_DB", 0),
|
|
JWTSecret: getEnv("JWT_SECRET", "change-me"),
|
|
PaymentConfigEncryptionKey: getEnv("PAYMENT_CONFIG_ENCRYPTION_KEY", ""),
|
|
ExternalUploadSecret: getEnv("EXTERNAL_UPLOAD_SECRET", ""),
|
|
ExternalUploadAllowedIPs: getEnvList("EXTERNAL_UPLOAD_ALLOWED_IPS"),
|
|
BootstrapAdminUsername: getEnv("ADMIN_BOOTSTRAP_USERNAME", ""),
|
|
BootstrapAdminPassword: getEnv("ADMIN_BOOTSTRAP_PASSWORD", ""),
|
|
BootstrapAdminNickname: getEnv("ADMIN_BOOTSTRAP_NICKNAME", "超级管理员"),
|
|
Storage: StorageConfig{
|
|
Endpoint: getEnv("STORAGE_ENDPOINT", "http://localhost:9000"),
|
|
Bucket: getEnv("STORAGE_BUCKET", "hfb-sys"),
|
|
AccessKeyID: getEnv("STORAGE_ACCESS_KEY_ID", "minioadmin"),
|
|
SecretAccessKey: getEnv("STORAGE_SECRET_ACCESS_KEY", "minioadmin"),
|
|
},
|
|
SMS: SMSConfig{
|
|
Provider: getEnv("SMS_PROVIDER", "mock"),
|
|
AliyunAccessKeyID: getEnv("ALIYUN_ACCESS_KEY_ID", ""),
|
|
AliyunAccessKeySecret: getEnv("ALIYUN_ACCESS_KEY_SECRET", ""),
|
|
AliyunEndpoint: getEnv("ALIYUN_SMS_ENDPOINT", "dysmsapi.aliyuncs.com"),
|
|
AliyunSignName: getEnv("ALIYUN_SMS_SIGN_NAME", ""),
|
|
AliyunLoginTemplateCode: getEnv("ALIYUN_SMS_LOGIN_TEMPLATE_CODE", ""),
|
|
},
|
|
Realname: RealnameConfig{
|
|
Provider: getEnv("REALNAME_PROVIDER", "mock"),
|
|
CloudMarketURL: getEnv("REALNAME_CLOUDMARKET_URL", "https://sinocheck2.market.alicloudapi.com/fortest/ttttt"),
|
|
CloudMarketAppCode: getEnv("REALNAME_CLOUDMARKET_APPCODE", ""),
|
|
},
|
|
Log: LogConfig{
|
|
Level: getEnv("LOG_LEVEL", "info"),
|
|
Dir: getEnv("LOG_DIR", "logs"),
|
|
EnableConsole: getEnvBool("LOG_ENABLE_CONSOLE", true),
|
|
EnableFile: getEnvBool("LOG_ENABLE_FILE", true),
|
|
},
|
|
RateLimit: RateLimitConfig{
|
|
Enabled: getEnvBool("RATE_LIMIT_ENABLED", true),
|
|
RequestsPerMinute: getEnvInt("RATE_LIMIT_REQUESTS_PER_MINUTE", 300),
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c Config) ValidateProductionSecurity() error {
|
|
if strings.ToLower(strings.TrimSpace(c.AppEnv)) != "production" {
|
|
return nil
|
|
}
|
|
if strings.TrimSpace(c.JWTSecret) == "" || isPlaceholder(c.JWTSecret) || len([]byte(c.JWTSecret)) < 32 {
|
|
return errors.New("JWT_SECRET must be a non-default random value of at least 32 bytes in production")
|
|
}
|
|
keyLen := len([]byte(c.PaymentConfigEncryptionKey))
|
|
if isPlaceholder(c.PaymentConfigEncryptionKey) || (keyLen != 16 && keyLen != 24 && keyLen != 32) {
|
|
return errors.New("PAYMENT_CONFIG_ENCRYPTION_KEY must be 16, 24, or 32 bytes in production")
|
|
}
|
|
if strings.TrimSpace(c.ExternalUploadSecret) == "" || isPlaceholder(c.ExternalUploadSecret) {
|
|
return errors.New("EXTERNAL_UPLOAD_SECRET is required in production")
|
|
}
|
|
if c.BootstrapAdminPassword != "" && isPlaceholder(c.BootstrapAdminPassword) {
|
|
return errors.New("ADMIN_BOOTSTRAP_PASSWORD must not use the example placeholder in production")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getEnv(key, fallback string) string {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func getEnvInt(key string, fallback int) int {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvBool(key string, fallback bool) bool {
|
|
value := os.Getenv(key)
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return fallback
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func getEnvList(key string) []string {
|
|
raw := os.Getenv(key)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(raw, ",")
|
|
values := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
value := strings.TrimSpace(part)
|
|
if value != "" {
|
|
values = append(values, value)
|
|
}
|
|
}
|
|
return values
|
|
}
|
|
|
|
func isPlaceholder(value string) bool {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
return normalized == "change-me" || strings.HasPrefix(normalized, "change-") || strings.Contains(normalized, "change-to-")
|
|
}
|