Files

163 lines
5.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"crypto/rand"
"encoding/base64"
"log"
"os"
"strconv"
"strings"
"github.com/joho/godotenv"
)
// Config 应用配置 // Logan.Li
type Config struct {
// 服务器配置
Port string
Mode string // debug, release
// 数据库配置
DBDriver string
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
// JWT 配置
JWTSecret string
// 离线许可证 Ed25519 seed(32 字节 hex;生产必须固定,勿轮换除非同步桌面公钥) // Logan.Li
OfflineLicenseSeed string
// 客户端授权防护配置 // Logan.Li
GracePeriodDays int // 过期宽限天数(仅商业过期;吊销/暂停无宽限)
ActivateDailyLimit int // 单序列号 24 小时内新设备激活上限(0 = 不限制)
OnlineTTLMinutes int // 激活记录在线状态有效期(last_seen 超过即视为离线)
DeactivateRequireToken bool // 解绑是否强制校验设备激活令牌
SyncIntervalMinutes int // 节点双向同步间隔(分钟),宽松节奏默认 10
// CORS 配置
CORSOrigins []string
}
// Load 从环境变量加载配置 // Logan.Li
func Load() *Config {
// 加载 .env:backend/.env 与仓库根 .env 各自尝试(缺文件不中断) // Logan.Li
_ = godotenv.Load(".env")
_ = godotenv.Load("../.env")
_ = godotenv.Load("../../.env")
cfg := &Config{
Port: getEnv("PORT", "9001"),
Mode: getEnv("GIN_MODE", "debug"),
DBDriver: getEnv("DB_DRIVER", "sqlite"),
DBHost: getEnv("DB_HOST", "127.0.0.1"),
DBPort: getEnv("DB_PORT", "3306"),
DBUser: getEnv("DB_USER", "root"),
DBPassword: getEnv("DB_PASSWORD", ""),
DBName: getEnv("DB_NAME", "db.sqlite3"),
JWTSecret: getEnv("JWT_SECRET", ""),
OfflineLicenseSeed: getEnv("OFFLINE_LICENSE_SEED", ""),
GracePeriodDays: getEnvInt("GRACE_PERIOD_DAYS", 15),
ActivateDailyLimit: getEnvInt("ACTIVATE_DAILY_LIMIT", 5),
OnlineTTLMinutes: getEnvInt("ONLINE_TTL_MINUTES", 11520), // 默认 8 天,适配周检节奏
DeactivateRequireToken: getEnvBool("DEACTIVATE_REQUIRE_TOKEN", true),
SyncIntervalMinutes: getEnvInt("SYNC_INTERVAL_MINUTES", 10),
CORSOrigins: defaultCORSOrigins(),
}
if raw := strings.TrimSpace(os.Getenv("CORS_ORIGINS")); raw != "" {
parts := strings.Split(raw, ",")
origins := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
origins = append(origins, p)
}
if len(origins) > 0 {
cfg.CORSOrigins = origins
}
}
// 安全检查:生产环境拒绝空密钥,开发环境使用进程级随机密钥避免固定后门。 // Logan.Li
if cfg.JWTSecret == "" {
if cfg.Mode == "release" {
log.Fatal("[安全] 生产环境必须设置 JWT_SECRET 环境变量")
}
cfg.JWTSecret = generateEphemeralJWTSecret()
log.Println("[安全] JWT_SECRET 未设置,已生成仅当前开发进程有效的随机密钥")
}
return cfg
}
func defaultCORSOrigins() []string {
return []string{
"http://localhost:5181",
"http://127.0.0.1:5181",
"http://localhost:3000",
"http://127.0.0.1:3000",
// OpsXShell Tauri / Vite dev origins (gin-cors only accepts http/https)
"http://localhost:1420",
"http://127.0.0.1:1420",
"http://tauri.localhost",
"https://tauri.localhost",
"tauri://localhost", // macOS Tauri WebView 实际 Origin // Logan.Li
}
}
// IsOriginAllowed 判断浏览器 Origin 是否在显式 CORS 白名单中。 // Logan.Li
func (c *Config) IsOriginAllowed(origin string) bool {
for _, allowed := range c.CORSOrigins {
if strings.EqualFold(strings.TrimSpace(origin), strings.TrimSpace(allowed)) {
return true
}
}
return false
}
// generateEphemeralJWTSecret 生成开发环境临时 JWT 密钥。 // Logan.Li
func generateEphemeralJWTSecret() string {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
log.Fatal("[安全] 生成开发 JWT 密钥失败")
}
return base64.RawURLEncoding.EncodeToString(bytes)
}
// getEnv 读取环境变量,不存在则返回默认值 // Logan.Li
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// getEnvInt 读取整型环境变量,不存在或非法时返回默认值 // Logan.Li
func getEnvInt(key string, defaultValue int) int {
if raw := strings.TrimSpace(os.Getenv(key)); raw != "" {
if v, err := strconv.Atoi(raw); err == nil {
return v
}
log.Printf("[配置] %s 不是合法整数,使用默认值 %d", key, defaultValue)
}
return defaultValue
}
// getEnvBool 读取布尔环境变量(true/false/1/0),不存在或非法时返回默认值 // Logan.Li
func getEnvBool(key string, defaultValue bool) bool {
if raw := strings.TrimSpace(os.Getenv(key)); raw != "" {
switch strings.ToLower(raw) {
case "true", "1", "yes":
return true
case "false", "0", "no":
return false
}
}
return defaultValue
}