install.go 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "io/ioutil"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "github.com/gofrs/uuid"
  10. "github.com/jmoiron/sqlx"
  11. goyesqlx "github.com/knadh/goyesql/v2/sqlx"
  12. "github.com/knadh/listmonk/models"
  13. "github.com/knadh/stuffbin"
  14. "github.com/lib/pq"
  15. )
  16. // install runs the first time setup of creating and
  17. // migrating the database and creating the super user.
  18. func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
  19. qMap, _ := initQueries(queryFilePath, db, fs, false)
  20. fmt.Println("")
  21. fmt.Println("** first time installation **")
  22. fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
  23. ko.String("db.database"))
  24. fmt.Println("")
  25. if prompt {
  26. var ok string
  27. fmt.Print("continue (y/n)? ")
  28. if _, err := fmt.Scanf("%s", &ok); err != nil {
  29. lo.Fatalf("error reading value from terminal: %v", err)
  30. }
  31. if strings.ToLower(ok) != "y" {
  32. fmt.Println("install cancelled.")
  33. return
  34. }
  35. }
  36. // Migrate the tables.
  37. err := installSchema(lastVer, db, fs)
  38. if err != nil {
  39. lo.Fatalf("Error migrating DB schema: %v", err)
  40. }
  41. // Load the queries.
  42. var q Queries
  43. if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
  44. lo.Fatalf("error loading SQL queries: %v", err)
  45. }
  46. // Sample list.
  47. var (
  48. defList int
  49. optinList int
  50. )
  51. if err := q.CreateList.Get(&defList,
  52. uuid.Must(uuid.NewV4()),
  53. "Default list",
  54. models.ListTypePrivate,
  55. models.ListOptinSingle,
  56. pq.StringArray{"test"},
  57. ); err != nil {
  58. lo.Fatalf("Error creating list: %v", err)
  59. }
  60. if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()),
  61. "Opt-in list",
  62. models.ListTypePublic,
  63. models.ListOptinDouble,
  64. pq.StringArray{"test"},
  65. ); err != nil {
  66. lo.Fatalf("Error creating list: %v", err)
  67. }
  68. // Sample subscriber.
  69. if _, err := q.UpsertSubscriber.Exec(
  70. uuid.Must(uuid.NewV4()),
  71. "john@example.com",
  72. "John Doe",
  73. `{"type": "known", "good": true, "city": "Bengaluru"}`,
  74. pq.Int64Array{int64(defList)},
  75. true); err != nil {
  76. lo.Fatalf("Error creating subscriber: %v", err)
  77. }
  78. if _, err := q.UpsertSubscriber.Exec(
  79. uuid.Must(uuid.NewV4()),
  80. "anon@example.com",
  81. "Anon Doe",
  82. `{"type": "unknown", "good": true, "city": "Bengaluru"}`,
  83. pq.Int64Array{int64(optinList)},
  84. true); err != nil {
  85. lo.Fatalf("Error creating subscriber: %v", err)
  86. }
  87. // Default template.
  88. tplBody, err := fs.Get("/static/email-templates/default.tpl")
  89. if err != nil {
  90. lo.Fatalf("error reading default e-mail template: %v", err)
  91. }
  92. var tplID int
  93. if err := q.CreateTemplate.Get(&tplID,
  94. "Default template",
  95. string(tplBody.ReadBytes()),
  96. ); err != nil {
  97. lo.Fatalf("error creating default template: %v", err)
  98. }
  99. if _, err := q.SetDefaultTemplate.Exec(tplID); err != nil {
  100. lo.Fatalf("error setting default template: %v", err)
  101. }
  102. // Sample campaign.
  103. if _, err := q.CreateCampaign.Exec(uuid.Must(uuid.NewV4()),
  104. models.CampaignTypeRegular,
  105. "Test campaign",
  106. "Welcome to listmonk",
  107. "No Reply <noreply@yoursite.com>",
  108. `<h3>Hi {{ .Subscriber.FirstName }}!</h3>
  109. This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.`,
  110. nil,
  111. "richtext",
  112. nil,
  113. pq.StringArray{"test-campaign"},
  114. emailMsgr,
  115. 1,
  116. pq.Int64Array{1},
  117. ); err != nil {
  118. lo.Fatalf("error creating sample campaign: %v", err)
  119. }
  120. lo.Printf("Setup complete")
  121. lo.Printf(`Run the program and access the dashboard at %s`, ko.MustString("app.address"))
  122. }
  123. // installSchema executes the SQL schema and creates the necessary tables and types.
  124. func installSchema(curVer string, db *sqlx.DB, fs stuffbin.FileSystem) error {
  125. q, err := fs.Read("/schema.sql")
  126. if err != nil {
  127. return err
  128. }
  129. if _, err := db.Exec(string(q)); err != nil {
  130. return err
  131. }
  132. // Insert the current migration version.
  133. return recordMigrationVersion(curVer, db)
  134. }
  135. // recordMigrationVersion inserts the given version (of DB migration) into the
  136. // `migrations` array in the settings table.
  137. func recordMigrationVersion(ver string, db *sqlx.DB) error {
  138. _, err := db.Exec(fmt.Sprintf(`INSERT INTO settings (key, value)
  139. VALUES('migrations', '["%s"]'::JSONB)
  140. ON CONFLICT (key) DO UPDATE SET value = settings.value || EXCLUDED.value`, ver))
  141. return err
  142. }
  143. func newConfigFile() error {
  144. if _, err := os.Stat("config.toml"); !os.IsNotExist(err) {
  145. return errors.New("config.toml exists. Remove it to generate a new one")
  146. }
  147. // Initialize the static file system into which all
  148. // required static assets (.sql, .js files etc.) are loaded.
  149. fs := initFS("", "")
  150. b, err := fs.Read("config.toml.sample")
  151. if err != nil {
  152. return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
  153. }
  154. // Generate a random admin password.
  155. pwd, err := generateRandomString(16)
  156. if err == nil {
  157. b = regexp.MustCompile(`admin_password\s+?=\s+?(.*)`).
  158. ReplaceAll(b, []byte(fmt.Sprintf(`admin_password = "%s"`, pwd)))
  159. }
  160. return ioutil.WriteFile("config.toml", b, 0644)
  161. }
  162. // checkSchema checks if the DB schema is installed.
  163. func checkSchema(db *sqlx.DB) (bool, error) {
  164. if _, err := db.Exec(`SELECT id FROM templates LIMIT 1`); err != nil {
  165. if isTableNotExistErr(err) {
  166. return false, nil
  167. }
  168. return false, err
  169. }
  170. return true, nil
  171. }