install.go 5.7 KB

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