db.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Package database provides database initialization, migration, and management utilities
  2. // for the 3x-ui panel using GORM with SQLite.
  3. package database
  4. import (
  5. "bytes"
  6. "io"
  7. "io/fs"
  8. "log"
  9. "os"
  10. "path"
  11. "slices"
  12. "github.com/mhsanaei/3x-ui/v2/config"
  13. "github.com/mhsanaei/3x-ui/v2/database/model"
  14. "github.com/mhsanaei/3x-ui/v2/util/crypto"
  15. "github.com/mhsanaei/3x-ui/v2/xray"
  16. "gorm.io/driver/sqlite"
  17. "gorm.io/gorm"
  18. "gorm.io/gorm/logger"
  19. )
  20. var db *gorm.DB
  21. const (
  22. defaultUsername = "admin"
  23. defaultPassword = "admin"
  24. )
  25. func initModels() error {
  26. models := []any{
  27. &model.User{},
  28. &model.Inbound{},
  29. &model.OutboundTraffics{},
  30. &model.Setting{},
  31. &model.InboundClientIps{},
  32. &xray.ClientTraffic{},
  33. &model.HistoryOfSeeders{},
  34. }
  35. for _, model := range models {
  36. if err := db.AutoMigrate(model); err != nil {
  37. log.Printf("Error auto migrating model: %v", err)
  38. return err
  39. }
  40. }
  41. return nil
  42. }
  43. // initUser creates a default admin user if the users table is empty.
  44. func initUser() error {
  45. empty, err := isTableEmpty("users")
  46. if err != nil {
  47. log.Printf("Error checking if users table is empty: %v", err)
  48. return err
  49. }
  50. if empty {
  51. hashedPassword, err := crypto.HashPasswordAsBcrypt(defaultPassword)
  52. if err != nil {
  53. log.Printf("Error hashing default password: %v", err)
  54. return err
  55. }
  56. user := &model.User{
  57. Username: defaultUsername,
  58. Password: hashedPassword,
  59. }
  60. return db.Create(user).Error
  61. }
  62. return nil
  63. }
  64. // runSeeders migrates user passwords to bcrypt and records seeder execution to prevent re-running.
  65. func runSeeders(isUsersEmpty bool) error {
  66. empty, err := isTableEmpty("history_of_seeders")
  67. if err != nil {
  68. log.Printf("Error checking if users table is empty: %v", err)
  69. return err
  70. }
  71. if empty && isUsersEmpty {
  72. hashSeeder := &model.HistoryOfSeeders{
  73. SeederName: "UserPasswordHash",
  74. }
  75. return db.Create(hashSeeder).Error
  76. } else {
  77. var seedersHistory []string
  78. db.Model(&model.HistoryOfSeeders{}).Pluck("seeder_name", &seedersHistory)
  79. if !slices.Contains(seedersHistory, "UserPasswordHash") && !isUsersEmpty {
  80. var users []model.User
  81. db.Find(&users)
  82. for _, user := range users {
  83. hashedPassword, err := crypto.HashPasswordAsBcrypt(user.Password)
  84. if err != nil {
  85. log.Printf("Error hashing password for user '%s': %v", user.Username, err)
  86. return err
  87. }
  88. db.Model(&user).Update("password", hashedPassword)
  89. }
  90. hashSeeder := &model.HistoryOfSeeders{
  91. SeederName: "UserPasswordHash",
  92. }
  93. return db.Create(hashSeeder).Error
  94. }
  95. }
  96. return nil
  97. }
  98. // isTableEmpty returns true if the named table contains zero rows.
  99. func isTableEmpty(tableName string) (bool, error) {
  100. var count int64
  101. err := db.Table(tableName).Count(&count).Error
  102. return count == 0, err
  103. }
  104. // InitDB sets up the database connection, migrates models, and runs seeders.
  105. func InitDB(dbPath string) error {
  106. dir := path.Dir(dbPath)
  107. err := os.MkdirAll(dir, fs.ModePerm)
  108. if err != nil {
  109. return err
  110. }
  111. var gormLogger logger.Interface
  112. if config.IsDebug() {
  113. gormLogger = logger.Default
  114. } else {
  115. gormLogger = logger.Discard
  116. }
  117. c := &gorm.Config{
  118. Logger: gormLogger,
  119. }
  120. db, err = gorm.Open(sqlite.Open(dbPath), c)
  121. if err != nil {
  122. return err
  123. }
  124. if err := initModels(); err != nil {
  125. return err
  126. }
  127. isUsersEmpty, err := isTableEmpty("users")
  128. if err != nil {
  129. return err
  130. }
  131. if err := initUser(); err != nil {
  132. return err
  133. }
  134. return runSeeders(isUsersEmpty)
  135. }
  136. // CloseDB closes the database connection if it exists.
  137. func CloseDB() error {
  138. if db != nil {
  139. sqlDB, err := db.DB()
  140. if err != nil {
  141. return err
  142. }
  143. return sqlDB.Close()
  144. }
  145. return nil
  146. }
  147. // GetDB returns the global GORM database instance.
  148. func GetDB() *gorm.DB {
  149. return db
  150. }
  151. // IsNotFound checks if the given error is a GORM record not found error.
  152. func IsNotFound(err error) bool {
  153. return err == gorm.ErrRecordNotFound
  154. }
  155. // IsSQLiteDB checks if the given file is a valid SQLite database by reading its signature.
  156. func IsSQLiteDB(file io.ReaderAt) (bool, error) {
  157. signature := []byte("SQLite format 3\x00")
  158. buf := make([]byte, len(signature))
  159. _, err := file.ReadAt(buf, 0)
  160. if err != nil {
  161. return false, err
  162. }
  163. return bytes.Equal(buf, signature), nil
  164. }
  165. // Checkpoint performs a WAL checkpoint on the SQLite database to ensure data consistency.
  166. func Checkpoint() error {
  167. // Update WAL
  168. err := db.Exec("PRAGMA wal_checkpoint;").Error
  169. if err != nil {
  170. return err
  171. }
  172. return nil
  173. }