diff --git a/handlers.go b/handlers.go index 12d52bb..0b4f3b2 100644 --- a/handlers.go +++ b/handlers.go @@ -43,9 +43,6 @@ func registerHandlers(e *echo.Echo) { e.GET("/", handleIndexPage) e.GET("/api/config.js", handleGetConfigScript) e.GET("/api/dashboard/stats", handleGetDashboardStats) - e.GET("/api/users", handleGetUsers) - e.POST("/api/users", handleCreateUser) - e.DELETE("/api/users/:id", handleDeleteUser) e.GET("/api/subscribers/:id", handleGetSubscriber) e.POST("/api/subscribers", handleCreateSubscriber) diff --git a/install.go b/install.go index 7b9e9d7..8cd1dda 100644 --- a/install.go +++ b/install.go @@ -1,90 +1,39 @@ package main import ( - "bytes" "fmt" "io/ioutil" - "regexp" - "syscall" - - "github.com/lib/pq" - uuid "github.com/satori/go.uuid" + "strings" "github.com/jmoiron/sqlx" "github.com/knadh/goyesql" "github.com/knadh/listmonk/models" + "github.com/lib/pq" + uuid "github.com/satori/go.uuid" "github.com/spf13/viper" - "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/ssh/terminal" ) // install runs the first time setup of creating and // migrating the database and creating the super user. func install(app *App, qMap goyesql.Queries) { - var ( - email, pw, pw2 []byte - err error - - // Pseudo e-mail validation using Regexp, well ... - emRegex, _ = regexp.Compile("(.+?)@(.+?)") - ) - fmt.Println("") - fmt.Println("** First time installation. **") - fmt.Println("** IMPORTANT: This will wipe existing listmonk tables and types. **") + fmt.Println("** First time installation **") + fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **", + viper.GetString("db.database")) fmt.Println("") - for len(email) == 0 { - fmt.Print("Enter the superadmin login e-mail: ") - if _, err = fmt.Scanf("%s", &email); err != nil { - logger.Fatalf("Error reading e-mail from the terminal: %v", err) - } - - if !emRegex.Match(email) { - logger.Println("Please enter a valid e-mail") - email = []byte{} - } + var ok string + fmt.Print("Continue (y/n)? ") + if _, err := fmt.Scanf("%s", &ok); err != nil { + logger.Fatalf("Error reading value from terminal: %v", err) } - - for len(pw) < 8 { - fmt.Print("Enter the superadmin password (min 8 chars): ") - if pw, err = terminal.ReadPassword(int(syscall.Stdin)); err != nil { - logger.Fatalf("Error reading password from the terminal: %v", err) - } - - fmt.Println("") - if len(pw) < 8 { - logger.Println("Password should be min 8 characters") - pw = []byte{} - } - } - - for len(pw2) < 8 { - fmt.Print("Repeat the superadmin password: ") - if pw2, err = terminal.ReadPassword(int(syscall.Stdin)); err != nil { - logger.Fatalf("Error reading password from the terminal: %v", err) - } - - fmt.Println("") - if len(pw2) < 8 { - logger.Println("Password should be min 8 characters") - pw2 = []byte{} - } - } - - // Validate. - if !bytes.Equal(pw, pw2) { - logger.Fatalf("Passwords don't match") - } - - // Hash the password. - hash, err := bcrypt.GenerateFromPassword(pw, bcrypt.DefaultCost) - if err != nil { - logger.Fatalf("Error hashing password: %v", err) + if strings.ToLower(ok) != "y" { + fmt.Println("Installation cancelled.") + return } // Migrate the tables. - err = installMigrate(app.DB, app) + err := installMigrate(app.DB, app) if err != nil { logger.Fatalf("Error migrating DB schema: %v", err) } @@ -95,17 +44,6 @@ func install(app *App, qMap goyesql.Queries) { logger.Fatalf("error loading SQL queries: %v", err) } - // Create the superadmin user. - if _, err := q.CreateUser.Exec( - string(email), - models.UserTypeSuperadmin, // name - string(hash), - models.UserTypeSuperadmin, - models.UserStatusEnabled, - ); err != nil { - logger.Fatalf("Error creating superadmin user: %v", err) - } - // Sample list. var listID int if err := q.CreateList.Get(&listID, @@ -118,11 +56,10 @@ func install(app *App, qMap goyesql.Queries) { } // Sample subscriber. - name := bytes.Split(email, []byte("@")) if _, err := q.UpsertSubscriber.Exec( uuid.NewV4(), - email, - bytes.Title(name[0]), + "test@test.com", + "Test Subscriber", `{"type": "known", "good": true}`, pq.Int64Array{int64(listID)}, ); err != nil { @@ -147,8 +84,7 @@ func install(app *App, qMap goyesql.Queries) { } logger.Printf("Setup complete") - logger.Printf(`Run the program and login with the username "superadmin" and your password at %s`, - viper.GetString("server.address")) + logger.Printf(`Run the program view it at %s`, viper.GetString("app.address")) } diff --git a/queries.go b/queries.go index b14a508..397ed41 100644 --- a/queries.go +++ b/queries.go @@ -57,11 +57,6 @@ type Queries struct { RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"` DeleteCampaign *sqlx.Stmt `query:"delete-campaign"` - CreateUser *sqlx.Stmt `query:"create-user"` - GetUsers *sqlx.Stmt `query:"get-users"` - UpdateUser *sqlx.Stmt `query:"update-user"` - DeleteUser *sqlx.Stmt `query:"delete-user"` - InsertMedia *sqlx.Stmt `query:"insert-media"` GetMedia *sqlx.Stmt `query:"get-media"` DeleteMedia *sqlx.Stmt `query:"delete-media"` diff --git a/schema.sql b/schema.sql index 40fd905..ddd0736 100644 --- a/schema.sql +++ b/schema.sql @@ -1,26 +1,9 @@ -DROP TYPE IF EXISTS user_type CASCADE; CREATE TYPE user_type AS ENUM ('superadmin', 'user'); -DROP TYPE IF EXISTS user_status CASCADE; CREATE TYPE user_status AS ENUM ('enabled', 'disabled'); DROP TYPE IF EXISTS list_type CASCADE; CREATE TYPE list_type AS ENUM ('public', 'private', 'temporary'); DROP TYPE IF EXISTS subscriber_status CASCADE; CREATE TYPE subscriber_status AS ENUM ('enabled', 'disabled', 'blacklisted'); DROP TYPE IF EXISTS subscription_status CASCADE; CREATE TYPE subscription_status AS ENUM ('unconfirmed', 'confirmed', 'unsubscribed'); DROP TYPE IF EXISTS campaign_status CASCADE; CREATE TYPE campaign_status AS ENUM ('draft', 'running', 'scheduled', 'paused', 'cancelled', 'finished'); DROP TYPE IF EXISTS content_type CASCADE; CREATE TYPE content_type AS ENUM ('richtext', 'html', 'plain'); --- users -DROP TABLE IF EXISTS users CASCADE; -CREATE TABLE users ( - id SERIAL PRIMARY KEY, - email TEXT NOT NULL UNIQUE, - name TEXT NOT NULL, - password TEXT NOT NULL, - type user_type NOT NULL, - status user_status NOT NULL, - - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() -); -DROP INDEX IF EXISTS idx_users_email; CREATE INDEX idx_users_email ON users(LOWER(email)); - -- subscribers DROP TABLE IF EXISTS subscribers CASCADE; CREATE TABLE subscribers ( diff --git a/users.go b/users.go deleted file mode 100644 index 8978e1a..0000000 --- a/users.go +++ /dev/null @@ -1,138 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "net/http" - "strconv" - - "github.com/asaskevich/govalidator" - "github.com/knadh/listmonk/models" - "github.com/labstack/echo" -) - -// handleGetUsers handles retrieval of users. -func handleGetUsers(c echo.Context) error { - var ( - app = c.Get("app").(*App) - out []models.User - - id, _ = strconv.Atoi(c.Param("id")) - single = false - ) - - // Fetch one list. - if id > 0 { - single = true - } - - err := app.Queries.GetUsers.Select(&out, id) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, - fmt.Sprintf("Error fetching users: %s", pqErrMsg(err))) - } else if single && len(out) == 0 { - return echo.NewHTTPError(http.StatusBadRequest, "User not found.") - } else if len(out) == 0 { - return c.JSON(http.StatusOK, okResp{[]struct{}{}}) - } - - if single { - return c.JSON(http.StatusOK, okResp{out[0]}) - } - - return c.JSON(http.StatusOK, okResp{out}) -} - -// handleCreateUser handles user creation. -func handleCreateUser(c echo.Context) error { - var ( - app = c.Get("app").(*App) - o = models.User{} - ) - - if err := c.Bind(&o); err != nil { - return err - } - - if !govalidator.IsEmail(o.Email) { - return errors.New("invalid `email`") - } - if !govalidator.IsByteLength(o.Name, 1, stdInputMaxLen) { - return errors.New("invalid length for `name`") - } - - // Insert and read ID. - var newID int - if err := app.Queries.CreateUser.Get(&newID, - o.Email, - o.Name, - o.Password, - o.Type, - o.Status); err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, - fmt.Sprintf("Error creating user: %v", pqErrMsg(err))) - } - - // Hand over to the GET handler to return the last insertion. - c.SetParamNames("id") - c.SetParamValues(fmt.Sprintf("%d", newID)) - return c.JSON(http.StatusOK, handleGetLists(c)) -} - -// handleUpdateUser handles user modification. -func handleUpdateUser(c echo.Context) error { - var ( - app = c.Get("app").(*App) - id, _ = strconv.Atoi(c.Param("id")) - ) - - if id < 1 { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") - } else if id == 1 { - return echo.NewHTTPError(http.StatusBadRequest, - "The primordial super admin cannot be deleted.") - } - - var o models.User - if err := c.Bind(&o); err != nil { - return err - } - - if !govalidator.IsEmail(o.Email) { - return errors.New("invalid `email`") - } - if !govalidator.IsByteLength(o.Name, 1, stdInputMaxLen) { - return errors.New("invalid length for `name`") - } - - // TODO: PASSWORD HASHING. - res, err := app.Queries.UpdateUser.Exec(o.ID, - o.Email, - o.Name, - o.Password, - o.Type, - o.Status) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, - fmt.Sprintf("Error updating user: %s", pqErrMsg(err))) - } - - if n, _ := res.RowsAffected(); n == 0 { - return echo.NewHTTPError(http.StatusBadRequest, "User not found.") - } - - return handleGetUsers(c) -} - -// handleDeleteUser handles user deletion. -func handleDeleteUser(c echo.Context) error { - var ( - id, _ = strconv.Atoi(c.Param("id")) - ) - - if id < 1 { - return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") - } - - return c.JSON(http.StatusOK, okResp{true}) -}