package database import ( "fmt" "log/slog" "os" "path/filepath" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" "rill/internal/model" "rill/internal/utils" ) const ( adminUsername = "admin" adminEmail = "admin@example.com" adminPasswordLen = 16 adminPasswordFilename = "admin_password.txt" ) // seedAdminUser 创建初始管理员用户并加入 admin 组,密码随机生成且仅在迁移时打印一次。 func seedAdminUser(tx *gorm.DB) error { var count int64 if err := tx.Model(&model.User{}).Where("username = ?", adminUsername).Count(&count).Error; err != nil { return err } if count > 0 { slog.Warn("已存在同名用户,跳过初始管理员创建", "username", adminUsername) return nil } password, err := utils.RandomString(adminPasswordLen) if err != nil { return err } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } user := model.User{ Username: adminUsername, Email: adminEmail, PasswordHash: string(hash), Nickname: "Administrator", Status: 1, } if err := tx.Create(&user).Error; err != nil { return err } if err := tx.Exec( "INSERT INTO user_group_members (user_id, user_group_id) VALUES (?, ?)", user.ID, model.GroupIDAdmin, ).Error; err != nil { return err } passwordFile := adminPasswordPath(tx) if err := writeAdminPasswordFile(passwordFile, password); err != nil { slog.Warn("初始管理员密码文件保存失败", "path", passwordFile, "err", err) } fmt.Printf( "\n初始管理员账号已创建,密码仅显示这一次,请立即保存:\n 用户名: %s\n 密码: %s\n 密码文件: %s(登录后请立即删除该文件)\n\n", adminUsername, password, passwordFile, ) return nil } // adminPasswordPath 密码文件与 SQLite 数据库同目录;其他驱动回落到 ./data。 func adminPasswordPath(db *gorm.DB) string { if db.Dialector.Name() == "sqlite" { if file := sqliteDatabaseFile(db); file != "" && file != ":memory:" { return filepath.Join(filepath.Dir(file), adminPasswordFilename) } } return filepath.Join("data", adminPasswordFilename) } func sqliteDatabaseFile(db *gorm.DB) string { var rows []struct { File string } if err := db.Raw("PRAGMA database_list").Scan(&rows).Error; err != nil || len(rows) == 0 { return "" } return rows[0].File } func writeAdminPasswordFile(path, password string) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } content := fmt.Sprintf( "初始管理员账号(密码仅明文保存在此,登录后请立即删除本文件)\n\n用户名: %s\n密码: %s\n", adminUsername, password, ) return os.WriteFile(path, []byte(content), 0o600) }