2018-10-25 21:51:47 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2019-07-08 20:51:44 +08:00
|
|
|
"os"
|
2020-08-08 15:41:49 +08:00
|
|
|
"regexp"
|
2019-06-26 18:52:47 +08:00
|
|
|
"strings"
|
2018-10-25 21:51:47 +08:00
|
|
|
|
2020-03-07 23:07:48 +08:00
|
|
|
"github.com/gofrs/uuid"
|
2018-10-25 21:51:47 +08:00
|
|
|
"github.com/jmoiron/sqlx"
|
2020-03-08 02:33:22 +08:00
|
|
|
goyesqlx "github.com/knadh/goyesql/v2/sqlx"
|
2018-10-25 21:51:47 +08:00
|
|
|
"github.com/knadh/listmonk/models"
|
2020-03-08 02:33:22 +08:00
|
|
|
"github.com/knadh/stuffbin"
|
2019-06-26 18:52:47 +08:00
|
|
|
"github.com/lib/pq"
|
2018-10-25 21:51:47 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
// install runs the first time setup of creating and
|
|
|
|
// migrating the database and creating the super user.
|
2021-08-22 22:39:39 +08:00
|
|
|
func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempotent bool) {
|
2020-03-08 02:33:22 +08:00
|
|
|
qMap, _ := initQueries(queryFilePath, db, fs, false)
|
|
|
|
|
2019-01-03 19:18:47 +08:00
|
|
|
fmt.Println("")
|
2021-08-22 22:39:39 +08:00
|
|
|
if !idempotent {
|
|
|
|
fmt.Println("** first time installation **")
|
|
|
|
fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
|
|
|
|
ko.String("db.database"))
|
|
|
|
} else {
|
|
|
|
fmt.Println("** first time (idempotent) installation **")
|
|
|
|
}
|
2019-01-03 19:18:47 +08:00
|
|
|
fmt.Println("")
|
2018-10-25 21:51:47 +08:00
|
|
|
|
2019-11-30 19:25:14 +08:00
|
|
|
if prompt {
|
|
|
|
var ok string
|
2021-09-13 02:17:06 +08:00
|
|
|
fmt.Print("continue (y/N)? ")
|
2019-11-30 19:25:14 +08:00
|
|
|
if _, err := fmt.Scanf("%s", &ok); err != nil {
|
2020-08-03 21:32:23 +08:00
|
|
|
lo.Fatalf("error reading value from terminal: %v", err)
|
2019-11-30 19:25:14 +08:00
|
|
|
}
|
|
|
|
if strings.ToLower(ok) != "y" {
|
2020-08-03 21:32:23 +08:00
|
|
|
fmt.Println("install cancelled.")
|
2019-11-30 19:25:14 +08:00
|
|
|
return
|
|
|
|
}
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2021-08-22 22:39:39 +08:00
|
|
|
// If idempotence is on, check if the DB is already setup.
|
|
|
|
if idempotent {
|
|
|
|
if _, err := db.Exec("SELECT count(*) FROM settings"); err != nil {
|
|
|
|
// If "settings" doesn't exist, assume it's a fresh install.
|
|
|
|
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code != "42P01" {
|
|
|
|
lo.Fatalf("error checking existing DB schema: %v", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
lo.Println("skipping install as database appears to be already setup")
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
// Migrate the tables.
|
2021-08-22 22:39:39 +08:00
|
|
|
if err := installSchema(lastVer, db, fs); err != nil {
|
|
|
|
lo.Fatalf("error migrating DB schema: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Load the queries.
|
|
|
|
var q Queries
|
2020-03-08 02:33:22 +08:00
|
|
|
if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
|
|
|
|
lo.Fatalf("error loading SQL queries: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sample list.
|
2020-02-09 14:15:01 +08:00
|
|
|
var (
|
|
|
|
defList int
|
|
|
|
optinList int
|
|
|
|
)
|
|
|
|
if err := q.CreateList.Get(&defList,
|
2020-03-07 23:07:48 +08:00
|
|
|
uuid.Must(uuid.NewV4()),
|
2018-10-25 21:51:47 +08:00
|
|
|
"Default list",
|
2020-02-09 14:15:01 +08:00
|
|
|
models.ListTypePrivate,
|
2019-12-01 20:18:36 +08:00
|
|
|
models.ListOptinSingle,
|
2018-10-25 21:51:47 +08:00
|
|
|
pq.StringArray{"test"},
|
|
|
|
); err != nil {
|
2021-08-22 22:39:39 +08:00
|
|
|
lo.Fatalf("error creating list: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2020-03-07 23:07:48 +08:00
|
|
|
if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()),
|
2020-02-09 14:15:01 +08:00
|
|
|
"Opt-in list",
|
|
|
|
models.ListTypePublic,
|
|
|
|
models.ListOptinDouble,
|
|
|
|
pq.StringArray{"test"},
|
|
|
|
); err != nil {
|
2021-08-22 22:39:39 +08:00
|
|
|
lo.Fatalf("error creating list: %v", err)
|
2020-02-09 14:15:01 +08:00
|
|
|
}
|
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
// Sample subscriber.
|
|
|
|
if _, err := q.UpsertSubscriber.Exec(
|
2020-03-07 23:07:48 +08:00
|
|
|
uuid.Must(uuid.NewV4()),
|
2019-07-04 17:17:33 +08:00
|
|
|
"john@example.com",
|
|
|
|
"John Doe",
|
|
|
|
`{"type": "known", "good": true, "city": "Bengaluru"}`,
|
2020-02-09 14:15:01 +08:00
|
|
|
pq.Int64Array{int64(defList)},
|
2021-06-03 01:08:12 +08:00
|
|
|
models.SubscriptionStatusUnconfirmed,
|
2020-07-06 00:05:17 +08:00
|
|
|
true); err != nil {
|
2020-03-08 02:33:22 +08:00
|
|
|
lo.Fatalf("Error creating subscriber: %v", err)
|
2020-02-09 14:15:01 +08:00
|
|
|
}
|
|
|
|
if _, err := q.UpsertSubscriber.Exec(
|
2020-03-07 23:07:48 +08:00
|
|
|
uuid.Must(uuid.NewV4()),
|
2020-02-09 14:15:01 +08:00
|
|
|
"anon@example.com",
|
|
|
|
"Anon Doe",
|
|
|
|
`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
|
|
|
|
pq.Int64Array{int64(optinList)},
|
2021-06-03 01:08:12 +08:00
|
|
|
models.SubscriptionStatusUnconfirmed,
|
2020-07-06 00:05:17 +08:00
|
|
|
true); err != nil {
|
2021-08-22 22:39:39 +08:00
|
|
|
lo.Fatalf("error creating subscriber: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Default template.
|
2021-01-24 15:19:52 +08:00
|
|
|
tplBody, err := fs.Get("/static/email-templates/default.tpl")
|
2018-10-25 21:51:47 +08:00
|
|
|
if err != nil {
|
2021-01-24 15:19:52 +08:00
|
|
|
lo.Fatalf("error reading default e-mail template: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
var tplID int
|
|
|
|
if err := q.CreateTemplate.Get(&tplID,
|
|
|
|
"Default template",
|
2021-01-24 15:19:52 +08:00
|
|
|
string(tplBody.ReadBytes()),
|
2018-10-25 21:51:47 +08:00
|
|
|
); err != nil {
|
2020-03-08 02:33:22 +08:00
|
|
|
lo.Fatalf("error creating default template: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
if _, err := q.SetDefaultTemplate.Exec(tplID); err != nil {
|
2020-03-08 02:33:22 +08:00
|
|
|
lo.Fatalf("error setting default template: %v", err)
|
2019-07-04 17:17:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sample campaign.
|
2020-03-07 23:07:48 +08:00
|
|
|
if _, err := q.CreateCampaign.Exec(uuid.Must(uuid.NewV4()),
|
2020-02-09 14:15:01 +08:00
|
|
|
models.CampaignTypeRegular,
|
2019-07-04 17:17:33 +08:00
|
|
|
"Test campaign",
|
|
|
|
"Welcome to listmonk",
|
2019-07-16 22:21:41 +08:00
|
|
|
"No Reply <noreply@yoursite.com>",
|
2019-07-04 17:17:33 +08:00
|
|
|
`<h3>Hi {{ .Subscriber.FirstName }}!</h3>
|
2021-09-26 18:33:05 +08:00
|
|
|
<p>This is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.</p>
|
|
|
|
<p>Here is a <a href="https://listmonk.app@TrackLink">tracked link</a>.</p>
|
|
|
|
<p>Use the link icon in the editor toolbar or when writing raw HTML or Markdown,
|
|
|
|
simply suffix @TrackLink to the end of a URL to turn it into a tracking link. Example:</p>
|
|
|
|
<pre><a href="https:/‌/listmonk.app@TrackLink"></a></pre>
|
|
|
|
<p>For help, refer to the <a href="https://listmonk.app/docs">documentation</a>.</p>
|
|
|
|
`,
|
2021-01-30 17:29:21 +08:00
|
|
|
nil,
|
2019-07-04 17:17:33 +08:00
|
|
|
"richtext",
|
2020-08-01 19:15:29 +08:00
|
|
|
nil,
|
2019-07-04 17:17:33 +08:00
|
|
|
pq.StringArray{"test-campaign"},
|
2020-09-20 19:01:24 +08:00
|
|
|
emailMsgr,
|
2019-07-04 17:17:33 +08:00
|
|
|
1,
|
|
|
|
pq.Int64Array{1},
|
|
|
|
); err != nil {
|
2020-03-08 02:33:22 +08:00
|
|
|
lo.Fatalf("error creating sample campaign: %v", err)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2021-08-22 22:39:39 +08:00
|
|
|
lo.Printf("setup complete")
|
|
|
|
lo.Printf(`run the program and access the dashboard at %s`, ko.MustString("app.address"))
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2020-08-03 21:32:23 +08:00
|
|
|
// installSchema executes the SQL schema and creates the necessary tables and types.
|
|
|
|
func installSchema(curVer string, db *sqlx.DB, fs stuffbin.FileSystem) error {
|
2020-03-08 02:33:22 +08:00
|
|
|
q, err := fs.Read("/schema.sql")
|
2018-10-25 21:51:47 +08:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-03 21:32:23 +08:00
|
|
|
if _, err := db.Exec(string(q)); err != nil {
|
2018-10-25 21:51:47 +08:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-08-03 21:32:23 +08:00
|
|
|
// Insert the current migration version.
|
|
|
|
return recordMigrationVersion(curVer, db)
|
|
|
|
}
|
|
|
|
|
|
|
|
// recordMigrationVersion inserts the given version (of DB migration) into the
|
|
|
|
// `migrations` array in the settings table.
|
|
|
|
func recordMigrationVersion(ver string, db *sqlx.DB) error {
|
|
|
|
_, err := db.Exec(fmt.Sprintf(`INSERT INTO settings (key, value)
|
|
|
|
VALUES('migrations', '["%s"]'::JSONB)
|
|
|
|
ON CONFLICT (key) DO UPDATE SET value = settings.value || EXCLUDED.value`, ver))
|
|
|
|
return err
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
2019-07-08 20:51:44 +08:00
|
|
|
|
2021-06-29 22:40:59 +08:00
|
|
|
func newConfigFile(path string) error {
|
|
|
|
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
|
|
|
return fmt.Errorf("%s exists. Remove it to generate a new one.", path)
|
2019-07-08 20:51:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the static file system into which all
|
|
|
|
// required static assets (.sql, .js files etc.) are loaded.
|
2021-07-11 13:03:00 +08:00
|
|
|
fs := initFS(appDir, "", "", "")
|
2019-07-08 20:51:44 +08:00
|
|
|
b, err := fs.Read("config.toml.sample")
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
|
|
|
|
}
|
|
|
|
|
2020-08-08 15:41:49 +08:00
|
|
|
// Generate a random admin password.
|
|
|
|
pwd, err := generateRandomString(16)
|
|
|
|
if err == nil {
|
|
|
|
b = regexp.MustCompile(`admin_password\s+?=\s+?(.*)`).
|
|
|
|
ReplaceAll(b, []byte(fmt.Sprintf(`admin_password = "%s"`, pwd)))
|
|
|
|
}
|
|
|
|
|
2021-06-29 22:40:59 +08:00
|
|
|
return ioutil.WriteFile(path, b, 0644)
|
2019-07-08 20:51:44 +08:00
|
|
|
}
|
2020-11-11 00:51:25 +08:00
|
|
|
|
|
|
|
// checkSchema checks if the DB schema is installed.
|
|
|
|
func checkSchema(db *sqlx.DB) (bool, error) {
|
|
|
|
if _, err := db.Exec(`SELECT id FROM templates LIMIT 1`); err != nil {
|
|
|
|
if isTableNotExistErr(err) {
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
return true, nil
|
|
|
|
}
|