setup.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. package setup
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "crypto/tls"
  6. "database/sql"
  7. "encoding/hex"
  8. "fmt"
  9. "os"
  10. "strconv"
  11. "strings"
  12. "time"
  13. "github.com/Wei-Shaw/sub2api/internal/config"
  14. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  15. "github.com/Wei-Shaw/sub2api/internal/repository"
  16. "github.com/Wei-Shaw/sub2api/internal/service"
  17. _ "github.com/lib/pq"
  18. "github.com/redis/go-redis/v9"
  19. "go.uber.org/zap"
  20. "gopkg.in/yaml.v3"
  21. )
  22. // Config paths
  23. const (
  24. ConfigFileName = "config.yaml"
  25. InstallLockFile = ".installed"
  26. defaultUserConcurrency = 5
  27. simpleModeAdminConcurrency = 30
  28. defaultMigrationTimeout = 60 * time.Second
  29. )
  30. func setupDefaultAdminConcurrency() int {
  31. if strings.EqualFold(strings.TrimSpace(os.Getenv("RUN_MODE")), config.RunModeSimple) {
  32. return simpleModeAdminConcurrency
  33. }
  34. return defaultUserConcurrency
  35. }
  36. // GetDataDir returns the data directory for storing config and lock files.
  37. // Priority: DATA_DIR env > /app/data (if exists and writable) > current directory
  38. func GetDataDir() string {
  39. // Check DATA_DIR environment variable first
  40. if dir := os.Getenv("DATA_DIR"); dir != "" {
  41. return dir
  42. }
  43. // Check if /app/data exists and is writable (Docker environment)
  44. dockerDataDir := "/app/data"
  45. if info, err := os.Stat(dockerDataDir); err == nil && info.IsDir() {
  46. // Try to check if writable by creating a temp file
  47. testFile := dockerDataDir + "/.write_test"
  48. if f, err := os.Create(testFile); err == nil {
  49. _ = f.Close()
  50. _ = os.Remove(testFile)
  51. return dockerDataDir
  52. }
  53. }
  54. // Default to current directory
  55. return "."
  56. }
  57. // GetConfigFilePath returns the full path to config.yaml
  58. func GetConfigFilePath() string {
  59. return GetDataDir() + "/" + ConfigFileName
  60. }
  61. // GetInstallLockPath returns the full path to .installed lock file
  62. func GetInstallLockPath() string {
  63. return GetDataDir() + "/" + InstallLockFile
  64. }
  65. // SetupConfig holds the setup configuration
  66. type SetupConfig struct {
  67. Database DatabaseConfig `json:"database" yaml:"database"`
  68. Redis RedisConfig `json:"redis" yaml:"redis"`
  69. Admin AdminConfig `json:"admin" yaml:"-"` // Not stored in config file
  70. Server ServerConfig `json:"server" yaml:"server"`
  71. JWT JWTConfig `json:"jwt" yaml:"jwt"`
  72. Timezone string `json:"timezone" yaml:"timezone"` // e.g. "Asia/Shanghai", "UTC"
  73. MigrationTimeoutSeconds int `json:"migration_timeout_seconds" yaml:"migration_timeout_seconds,omitempty"`
  74. }
  75. type DatabaseConfig struct {
  76. Host string `json:"host" yaml:"host"`
  77. Port int `json:"port" yaml:"port"`
  78. User string `json:"user" yaml:"user"`
  79. Password string `json:"password" yaml:"password"`
  80. DBName string `json:"dbname" yaml:"dbname"`
  81. SSLMode string `json:"sslmode" yaml:"sslmode"`
  82. }
  83. type RedisConfig struct {
  84. Host string `json:"host" yaml:"host"`
  85. Port int `json:"port" yaml:"port"`
  86. Username string `json:"username" yaml:"username"`
  87. Password string `json:"password" yaml:"password"`
  88. DB int `json:"db" yaml:"db"`
  89. EnableTLS bool `json:"enable_tls" yaml:"enable_tls"`
  90. }
  91. type AdminConfig struct {
  92. Email string `json:"email"`
  93. Password string `json:"password"`
  94. }
  95. type ServerConfig struct {
  96. Host string `json:"host" yaml:"host"`
  97. Port int `json:"port" yaml:"port"`
  98. Mode string `json:"mode" yaml:"mode"`
  99. }
  100. type JWTConfig struct {
  101. Secret string `json:"secret" yaml:"secret"`
  102. ExpireHour int `json:"expire_hour" yaml:"expire_hour"`
  103. }
  104. const (
  105. adminBootstrapReasonEmptyDatabase = "empty_database"
  106. adminBootstrapReasonAdminExists = "admin_exists"
  107. adminBootstrapReasonUsersExistWithoutAdmin = "users_exist_without_admin"
  108. )
  109. type adminBootstrapDecision struct {
  110. shouldCreate bool
  111. reason string
  112. }
  113. func decideAdminBootstrap(totalUsers, adminUsers int64) adminBootstrapDecision {
  114. if adminUsers > 0 {
  115. return adminBootstrapDecision{
  116. shouldCreate: false,
  117. reason: adminBootstrapReasonAdminExists,
  118. }
  119. }
  120. if totalUsers > 0 {
  121. return adminBootstrapDecision{
  122. shouldCreate: false,
  123. reason: adminBootstrapReasonUsersExistWithoutAdmin,
  124. }
  125. }
  126. return adminBootstrapDecision{
  127. shouldCreate: true,
  128. reason: adminBootstrapReasonEmptyDatabase,
  129. }
  130. }
  131. func skipSetupEnabled() bool {
  132. switch strings.ToLower(strings.TrimSpace(os.Getenv("SKIP_SETUP"))) {
  133. case "true", "1", "yes":
  134. return true
  135. default:
  136. return false
  137. }
  138. }
  139. // NeedsSetup checks if the system needs initial setup
  140. // Uses multiple checks to prevent attackers from forcing re-setup by deleting config
  141. func NeedsSetup() bool {
  142. if skipSetupEnabled() {
  143. logger.L().Debug("setup.needs_setup_bypassed", zap.String("reason", "skip_setup_enabled"))
  144. return false
  145. }
  146. // Check 1: Config file must not exist
  147. if _, err := os.Stat(GetConfigFilePath()); !os.IsNotExist(err) {
  148. return false // Config exists, no setup needed
  149. }
  150. // Check 2: Installation lock file (harder to bypass)
  151. if _, err := os.Stat(GetInstallLockPath()); !os.IsNotExist(err) {
  152. return false // Lock file exists, already installed
  153. }
  154. return true
  155. }
  156. func buildPostgresDSN(cfg *DatabaseConfig, dbName string) string {
  157. return fmt.Sprintf(
  158. "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
  159. cfg.Host, cfg.Port, cfg.User, cfg.Password, dbName, cfg.SSLMode,
  160. )
  161. }
  162. func buildDatabaseConnectionDSNs(cfg *DatabaseConfig) (bootstrapDSN, targetDSN string) {
  163. return buildPostgresDSN(cfg, "postgres"), buildPostgresDSN(cfg, cfg.DBName)
  164. }
  165. // TestDatabaseConnection tests the database connection and creates database if not exists
  166. func TestDatabaseConnection(cfg *DatabaseConfig) error {
  167. // First, connect to the default 'postgres' database to check/create target database.
  168. // Connecting to cfg.DBName here fails when the target database has not been
  169. // created yet, so the bootstrap connection must use PostgreSQL's maintenance DB.
  170. defaultDSN, targetDSN := buildDatabaseConnectionDSNs(cfg)
  171. db, err := sql.Open("postgres", defaultDSN)
  172. if err != nil {
  173. return fmt.Errorf("failed to connect to PostgreSQL: %w", err)
  174. }
  175. defer func() {
  176. if db == nil {
  177. return
  178. }
  179. if err := db.Close(); err != nil {
  180. logger.LegacyPrintf("setup", "failed to close postgres connection: %v", err)
  181. }
  182. }()
  183. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  184. defer cancel()
  185. if err := db.PingContext(ctx); err != nil {
  186. return fmt.Errorf("ping failed: %w", err)
  187. }
  188. // Check if target database exists
  189. var exists bool
  190. row := db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)", cfg.DBName)
  191. if err := row.Scan(&exists); err != nil {
  192. return fmt.Errorf("failed to check database existence: %w", err)
  193. }
  194. // Create database if not exists
  195. if !exists {
  196. // 注意:数据库名不能参数化,依赖前置输入校验保障安全。
  197. // Note: Database names cannot be parameterized, but we've already validated cfg.DBName
  198. // in the handler using validateDBName() which only allows [a-zA-Z][a-zA-Z0-9_]*
  199. _, err := db.ExecContext(ctx, fmt.Sprintf("CREATE DATABASE %s", cfg.DBName))
  200. if err != nil {
  201. return fmt.Errorf("failed to create database '%s': %w", cfg.DBName, err)
  202. }
  203. logger.LegacyPrintf("setup", "Database '%s' created successfully", cfg.DBName)
  204. }
  205. // Now connect to the target database to verify
  206. if err := db.Close(); err != nil {
  207. logger.LegacyPrintf("setup", "failed to close postgres connection: %v", err)
  208. }
  209. db = nil
  210. targetDB, err := sql.Open("postgres", targetDSN)
  211. if err != nil {
  212. return fmt.Errorf("failed to connect to database '%s': %w", cfg.DBName, err)
  213. }
  214. defer func() {
  215. if err := targetDB.Close(); err != nil {
  216. logger.LegacyPrintf("setup", "failed to close postgres connection: %v", err)
  217. }
  218. }()
  219. ctx2, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
  220. defer cancel2()
  221. if err := targetDB.PingContext(ctx2); err != nil {
  222. return fmt.Errorf("ping target database failed: %w", err)
  223. }
  224. return nil
  225. }
  226. // TestRedisConnection tests the Redis connection
  227. func TestRedisConnection(cfg *RedisConfig) error {
  228. opts := &redis.Options{
  229. Addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port),
  230. Username: cfg.Username,
  231. Password: cfg.Password,
  232. DB: cfg.DB,
  233. }
  234. if cfg.EnableTLS {
  235. opts.TLSConfig = &tls.Config{
  236. MinVersion: tls.VersionTLS12,
  237. ServerName: cfg.Host,
  238. }
  239. }
  240. rdb := redis.NewClient(opts)
  241. defer func() {
  242. if err := rdb.Close(); err != nil {
  243. logger.LegacyPrintf("setup", "failed to close redis client: %v", err)
  244. }
  245. }()
  246. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  247. defer cancel()
  248. if err := rdb.Ping(ctx).Err(); err != nil {
  249. return fmt.Errorf("ping failed: %w", err)
  250. }
  251. return nil
  252. }
  253. // Install performs the installation with the given configuration
  254. func Install(cfg *SetupConfig) error {
  255. // Security check: prevent re-installation if already installed
  256. if !NeedsSetup() {
  257. return fmt.Errorf("system is already installed, re-installation is not allowed")
  258. }
  259. // Generate JWT secret if not provided
  260. if cfg.JWT.Secret == "" {
  261. secret, err := generateSecret(32)
  262. if err != nil {
  263. return fmt.Errorf("failed to generate jwt secret: %w", err)
  264. }
  265. cfg.JWT.Secret = secret
  266. logger.LegacyPrintf("setup", "%s", "Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
  267. }
  268. // Test connections
  269. if err := TestDatabaseConnection(&cfg.Database); err != nil {
  270. return fmt.Errorf("database connection failed: %w", err)
  271. }
  272. if err := TestRedisConnection(&cfg.Redis); err != nil {
  273. return fmt.Errorf("redis connection failed: %w", err)
  274. }
  275. // Initialize database
  276. if err := initializeDatabase(cfg); err != nil {
  277. return fmt.Errorf("database initialization failed: %w", err)
  278. }
  279. // Create admin user (only when database is empty and no admin exists).
  280. if _, _, err := createAdminUser(cfg); err != nil {
  281. return fmt.Errorf("admin user creation failed: %w", err)
  282. }
  283. // Write config file
  284. if err := writeConfigFile(cfg); err != nil {
  285. return fmt.Errorf("config file creation failed: %w", err)
  286. }
  287. // Create installation lock file to prevent re-setup attacks
  288. if err := createInstallLock(); err != nil {
  289. return fmt.Errorf("failed to create install lock: %w", err)
  290. }
  291. return nil
  292. }
  293. // createInstallLock creates a lock file to prevent re-installation attacks
  294. func createInstallLock() error {
  295. content := fmt.Sprintf("installed_at=%s\n", time.Now().UTC().Format(time.RFC3339))
  296. return os.WriteFile(GetInstallLockPath(), []byte(content), 0400) // Read-only for owner
  297. }
  298. func initializeDatabase(cfg *SetupConfig) error {
  299. dsn := fmt.Sprintf(
  300. "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
  301. cfg.Database.Host, cfg.Database.Port, cfg.Database.User,
  302. cfg.Database.Password, cfg.Database.DBName, cfg.Database.SSLMode,
  303. )
  304. db, err := sql.Open("postgres", dsn)
  305. if err != nil {
  306. return err
  307. }
  308. defer func() {
  309. if err := db.Close(); err != nil {
  310. logger.LegacyPrintf("setup", "failed to close postgres connection: %v", err)
  311. }
  312. }()
  313. migrationCtx, cancel := context.WithTimeout(context.Background(), cfg.migrationTimeout())
  314. defer cancel()
  315. return repository.ApplyMigrations(migrationCtx, db)
  316. }
  317. func (cfg *SetupConfig) migrationTimeout() time.Duration {
  318. if cfg != nil && cfg.MigrationTimeoutSeconds > 0 {
  319. return time.Duration(cfg.MigrationTimeoutSeconds) * time.Second
  320. }
  321. return defaultMigrationTimeout
  322. }
  323. func createAdminUser(cfg *SetupConfig) (bool, string, error) {
  324. dsn := fmt.Sprintf(
  325. "host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
  326. cfg.Database.Host, cfg.Database.Port, cfg.Database.User,
  327. cfg.Database.Password, cfg.Database.DBName, cfg.Database.SSLMode,
  328. )
  329. db, err := sql.Open("postgres", dsn)
  330. if err != nil {
  331. return false, "", err
  332. }
  333. defer func() {
  334. if err := db.Close(); err != nil {
  335. logger.LegacyPrintf("setup", "failed to close postgres connection: %v", err)
  336. }
  337. }()
  338. // 使用超时上下文避免安装流程因数据库异常而长时间阻塞。
  339. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  340. defer cancel()
  341. var totalUsers int64
  342. if err := db.QueryRowContext(ctx, "SELECT COUNT(1) FROM users").Scan(&totalUsers); err != nil {
  343. return false, "", err
  344. }
  345. var adminUsers int64
  346. if err := db.QueryRowContext(ctx, "SELECT COUNT(1) FROM users WHERE role IN ($1, $2)", service.RoleAdmin, service.RoleSuperAdmin).Scan(&adminUsers); err != nil {
  347. return false, "", err
  348. }
  349. decision := decideAdminBootstrap(totalUsers, adminUsers)
  350. if !decision.shouldCreate {
  351. return false, decision.reason, nil
  352. }
  353. if strings.TrimSpace(cfg.Admin.Password) == "" {
  354. password, genErr := generateSecret(16)
  355. if genErr != nil {
  356. return false, "", fmt.Errorf("failed to generate admin password: %w", genErr)
  357. }
  358. cfg.Admin.Password = password
  359. fmt.Printf("Generated admin password (one-time): %s\n", cfg.Admin.Password)
  360. fmt.Println("IMPORTANT: Save this password! It will not be shown again.")
  361. }
  362. admin := &service.User{
  363. Email: cfg.Admin.Email,
  364. Role: service.RoleSuperAdmin,
  365. Status: service.StatusActive,
  366. Balance: 0,
  367. Concurrency: setupDefaultAdminConcurrency(),
  368. CreatedAt: time.Now(),
  369. UpdatedAt: time.Now(),
  370. }
  371. if err := admin.SetPassword(cfg.Admin.Password); err != nil {
  372. return false, "", err
  373. }
  374. _, err = db.ExecContext(
  375. ctx,
  376. `INSERT INTO users (email, password_hash, role, balance, concurrency, status, created_at, updated_at)
  377. VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
  378. admin.Email,
  379. admin.PasswordHash,
  380. admin.Role,
  381. admin.Balance,
  382. admin.Concurrency,
  383. admin.Status,
  384. admin.CreatedAt,
  385. admin.UpdatedAt,
  386. )
  387. if err != nil {
  388. return false, "", err
  389. }
  390. return true, decision.reason, nil
  391. }
  392. func writeConfigFile(cfg *SetupConfig) error {
  393. // Ensure timezone has a default value
  394. tz := cfg.Timezone
  395. if tz == "" {
  396. tz = "Asia/Shanghai"
  397. }
  398. // Prepare config for YAML (exclude sensitive data and admin config)
  399. yamlConfig := struct {
  400. Server ServerConfig `yaml:"server"`
  401. Database DatabaseConfig `yaml:"database"`
  402. Redis RedisConfig `yaml:"redis"`
  403. JWT struct {
  404. Secret string `yaml:"secret"`
  405. ExpireHour int `yaml:"expire_hour"`
  406. } `yaml:"jwt"`
  407. Default struct {
  408. UserConcurrency int `yaml:"user_concurrency"`
  409. UserBalance float64 `yaml:"user_balance"`
  410. APIKeyPrefix string `yaml:"api_key_prefix"`
  411. RateMultiplier float64 `yaml:"rate_multiplier"`
  412. } `yaml:"default"`
  413. RateLimit struct {
  414. RequestsPerMinute int `yaml:"requests_per_minute"`
  415. BurstSize int `yaml:"burst_size"`
  416. } `yaml:"rate_limit"`
  417. Timezone string `yaml:"timezone"`
  418. }{
  419. Server: cfg.Server,
  420. Database: cfg.Database,
  421. Redis: cfg.Redis,
  422. JWT: struct {
  423. Secret string `yaml:"secret"`
  424. ExpireHour int `yaml:"expire_hour"`
  425. }{
  426. Secret: cfg.JWT.Secret,
  427. ExpireHour: cfg.JWT.ExpireHour,
  428. },
  429. Default: struct {
  430. UserConcurrency int `yaml:"user_concurrency"`
  431. UserBalance float64 `yaml:"user_balance"`
  432. APIKeyPrefix string `yaml:"api_key_prefix"`
  433. RateMultiplier float64 `yaml:"rate_multiplier"`
  434. }{
  435. UserConcurrency: defaultUserConcurrency,
  436. UserBalance: 0,
  437. APIKeyPrefix: "sk-",
  438. RateMultiplier: 1.0,
  439. },
  440. RateLimit: struct {
  441. RequestsPerMinute int `yaml:"requests_per_minute"`
  442. BurstSize int `yaml:"burst_size"`
  443. }{
  444. RequestsPerMinute: 60,
  445. BurstSize: 10,
  446. },
  447. Timezone: tz,
  448. }
  449. data, err := yaml.Marshal(&yamlConfig)
  450. if err != nil {
  451. return err
  452. }
  453. return os.WriteFile(GetConfigFilePath(), data, 0600)
  454. }
  455. func generateSecret(length int) (string, error) {
  456. bytes := make([]byte, length)
  457. if _, err := rand.Read(bytes); err != nil {
  458. return "", err
  459. }
  460. return hex.EncodeToString(bytes), nil
  461. }
  462. // =============================================================================
  463. // Auto Setup for Docker Deployment
  464. // =============================================================================
  465. // AutoSetupEnabled checks if auto setup is enabled via environment variable
  466. func AutoSetupEnabled() bool {
  467. val := os.Getenv("AUTO_SETUP")
  468. return val == "true" || val == "1" || val == "yes"
  469. }
  470. // getEnvOrDefault gets environment variable or returns default value
  471. func getEnvOrDefault(key, defaultValue string) string {
  472. if val := os.Getenv(key); val != "" {
  473. return val
  474. }
  475. return defaultValue
  476. }
  477. // getEnvIntOrDefault gets environment variable as int or returns default value
  478. func getEnvIntOrDefault(key string, defaultValue int) int {
  479. if val := os.Getenv(key); val != "" {
  480. if i, err := strconv.Atoi(val); err == nil {
  481. return i
  482. }
  483. }
  484. return defaultValue
  485. }
  486. // AutoSetupFromEnv performs automatic setup using environment variables
  487. // This is designed for Docker deployment where all config is passed via env vars
  488. func AutoSetupFromEnv() error {
  489. logger.LegacyPrintf("setup", "%s", "Auto setup enabled, configuring from environment variables...")
  490. logger.LegacyPrintf("setup", "Data directory: %s", GetDataDir())
  491. // Get timezone from TZ or TIMEZONE env var (TZ is standard for Docker)
  492. tz := getEnvOrDefault("TZ", "")
  493. if tz == "" {
  494. tz = getEnvOrDefault("TIMEZONE", "Asia/Shanghai")
  495. }
  496. // Build config from environment variables
  497. cfg := &SetupConfig{
  498. Database: DatabaseConfig{
  499. Host: getEnvOrDefault("DATABASE_HOST", "localhost"),
  500. Port: getEnvIntOrDefault("DATABASE_PORT", 5432),
  501. User: getEnvOrDefault("DATABASE_USER", "postgres"),
  502. Password: getEnvOrDefault("DATABASE_PASSWORD", ""),
  503. DBName: getEnvOrDefault("DATABASE_DBNAME", "sub2api"),
  504. SSLMode: getEnvOrDefault("DATABASE_SSLMODE", "disable"),
  505. },
  506. Redis: RedisConfig{
  507. Host: getEnvOrDefault("REDIS_HOST", "localhost"),
  508. Port: getEnvIntOrDefault("REDIS_PORT", 6379),
  509. Username: getEnvOrDefault("REDIS_USERNAME", ""),
  510. Password: getEnvOrDefault("REDIS_PASSWORD", ""),
  511. DB: getEnvIntOrDefault("REDIS_DB", 0),
  512. EnableTLS: getEnvOrDefault("REDIS_ENABLE_TLS", "false") == "true",
  513. },
  514. Admin: AdminConfig{
  515. Email: getEnvOrDefault("ADMIN_EMAIL", "admin@sub2api.local"),
  516. Password: getEnvOrDefault("ADMIN_PASSWORD", ""),
  517. },
  518. Server: ServerConfig{
  519. Host: getEnvOrDefault("SERVER_HOST", "0.0.0.0"),
  520. Port: getEnvIntOrDefault("SERVER_PORT", 8080),
  521. Mode: getEnvOrDefault("SERVER_MODE", "release"),
  522. },
  523. JWT: JWTConfig{
  524. Secret: getEnvOrDefault("JWT_SECRET", ""),
  525. ExpireHour: getEnvIntOrDefault("JWT_EXPIRE_HOUR", 24),
  526. },
  527. Timezone: tz,
  528. MigrationTimeoutSeconds: getEnvIntOrDefault("SETUP_MIGRATION_TIMEOUT_SECONDS", 0),
  529. }
  530. // Generate JWT secret if not provided
  531. if cfg.JWT.Secret == "" {
  532. secret, err := generateSecret(32)
  533. if err != nil {
  534. return fmt.Errorf("failed to generate jwt secret: %w", err)
  535. }
  536. cfg.JWT.Secret = secret
  537. logger.LegacyPrintf("setup", "%s", "Warning: JWT secret auto-generated. Consider setting a fixed secret for production.")
  538. }
  539. // Test database connection
  540. logger.LegacyPrintf("setup", "%s", "Testing database connection...")
  541. if err := TestDatabaseConnection(&cfg.Database); err != nil {
  542. return fmt.Errorf("database connection failed: %w", err)
  543. }
  544. logger.LegacyPrintf("setup", "%s", "Database connection successful")
  545. // Test Redis connection
  546. logger.LegacyPrintf("setup", "%s", "Testing Redis connection...")
  547. if err := TestRedisConnection(&cfg.Redis); err != nil {
  548. return fmt.Errorf("redis connection failed: %w", err)
  549. }
  550. logger.LegacyPrintf("setup", "%s", "Redis connection successful")
  551. // Initialize database
  552. logger.LegacyPrintf("setup", "%s", "Initializing database...")
  553. if err := initializeDatabase(cfg); err != nil {
  554. return fmt.Errorf("database initialization failed: %w", err)
  555. }
  556. logger.LegacyPrintf("setup", "%s", "Database initialized successfully")
  557. // Create admin user
  558. logger.LegacyPrintf("setup", "%s", "Creating admin user...")
  559. created, reason, err := createAdminUser(cfg)
  560. if err != nil {
  561. return fmt.Errorf("admin user creation failed: %w", err)
  562. }
  563. if created {
  564. logger.LegacyPrintf("setup", "Admin user created: %s", cfg.Admin.Email)
  565. } else {
  566. switch reason {
  567. case adminBootstrapReasonAdminExists:
  568. logger.LegacyPrintf("setup", "%s", "Admin user already exists, skipping admin bootstrap")
  569. case adminBootstrapReasonUsersExistWithoutAdmin:
  570. logger.LegacyPrintf("setup", "%s", "Database already has user data; skipping auto admin bootstrap to avoid password overwrite")
  571. default:
  572. logger.LegacyPrintf("setup", "%s", "Admin bootstrap skipped")
  573. }
  574. }
  575. // Write config file
  576. logger.LegacyPrintf("setup", "%s", "Writing configuration file...")
  577. if err := writeConfigFile(cfg); err != nil {
  578. return fmt.Errorf("config file creation failed: %w", err)
  579. }
  580. logger.LegacyPrintf("setup", "%s", "Configuration file created")
  581. // Create installation lock file
  582. if err := createInstallLock(); err != nil {
  583. return fmt.Errorf("failed to create install lock: %w", err)
  584. }
  585. logger.LegacyPrintf("setup", "%s", "Installation lock created")
  586. logger.LegacyPrintf("setup", "%s", "Auto setup completed successfully!")
  587. return nil
  588. }