Remove stub user features and handlers.
This commit is contained in:
parent
c952b7a2e8
commit
fe9102120c
|
@ -43,9 +43,6 @@ func registerHandlers(e *echo.Echo) {
|
||||||
e.GET("/", handleIndexPage)
|
e.GET("/", handleIndexPage)
|
||||||
e.GET("/api/config.js", handleGetConfigScript)
|
e.GET("/api/config.js", handleGetConfigScript)
|
||||||
e.GET("/api/dashboard/stats", handleGetDashboardStats)
|
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.GET("/api/subscribers/:id", handleGetSubscriber)
|
||||||
e.POST("/api/subscribers", handleCreateSubscriber)
|
e.POST("/api/subscribers", handleCreateSubscriber)
|
||||||
|
|
98
install.go
98
install.go
|
@ -1,90 +1,39 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"regexp"
|
"strings"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/lib/pq"
|
|
||||||
uuid "github.com/satori/go.uuid"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/knadh/goyesql"
|
"github.com/knadh/goyesql"
|
||||||
"github.com/knadh/listmonk/models"
|
"github.com/knadh/listmonk/models"
|
||||||
|
"github.com/lib/pq"
|
||||||
|
uuid "github.com/satori/go.uuid"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
"golang.org/x/crypto/ssh/terminal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// install runs the first time setup of creating and
|
// install runs the first time setup of creating and
|
||||||
// migrating the database and creating the super user.
|
// migrating the database and creating the super user.
|
||||||
func install(app *App, qMap goyesql.Queries) {
|
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("")
|
||||||
fmt.Println("** First time installation. **")
|
fmt.Println("** First time installation **")
|
||||||
fmt.Println("** IMPORTANT: This will wipe existing listmonk tables and types. **")
|
fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
|
||||||
|
viper.GetString("db.database"))
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
|
||||||
for len(email) == 0 {
|
var ok string
|
||||||
fmt.Print("Enter the superadmin login e-mail: ")
|
fmt.Print("Continue (y/n)? ")
|
||||||
if _, err = fmt.Scanf("%s", &email); err != nil {
|
if _, err := fmt.Scanf("%s", &ok); err != nil {
|
||||||
logger.Fatalf("Error reading e-mail from the terminal: %v", err)
|
logger.Fatalf("Error reading value from terminal: %v", err)
|
||||||
}
|
}
|
||||||
|
if strings.ToLower(ok) != "y" {
|
||||||
if !emRegex.Match(email) {
|
fmt.Println("Installation cancelled.")
|
||||||
logger.Println("Please enter a valid e-mail")
|
return
|
||||||
email = []byte{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate the tables.
|
// Migrate the tables.
|
||||||
err = installMigrate(app.DB, app)
|
err := installMigrate(app.DB, app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Error migrating DB schema: %v", err)
|
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)
|
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.
|
// Sample list.
|
||||||
var listID int
|
var listID int
|
||||||
if err := q.CreateList.Get(&listID,
|
if err := q.CreateList.Get(&listID,
|
||||||
|
@ -118,11 +56,10 @@ func install(app *App, qMap goyesql.Queries) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sample subscriber.
|
// Sample subscriber.
|
||||||
name := bytes.Split(email, []byte("@"))
|
|
||||||
if _, err := q.UpsertSubscriber.Exec(
|
if _, err := q.UpsertSubscriber.Exec(
|
||||||
uuid.NewV4(),
|
uuid.NewV4(),
|
||||||
email,
|
"test@test.com",
|
||||||
bytes.Title(name[0]),
|
"Test Subscriber",
|
||||||
`{"type": "known", "good": true}`,
|
`{"type": "known", "good": true}`,
|
||||||
pq.Int64Array{int64(listID)},
|
pq.Int64Array{int64(listID)},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
|
@ -147,8 +84,7 @@ func install(app *App, qMap goyesql.Queries) {
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("Setup complete")
|
logger.Printf("Setup complete")
|
||||||
logger.Printf(`Run the program and login with the username "superadmin" and your password at %s`,
|
logger.Printf(`Run the program view it at %s`, viper.GetString("app.address"))
|
||||||
viper.GetString("server.address"))
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -57,11 +57,6 @@ type Queries struct {
|
||||||
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
RegisterCampaignView *sqlx.Stmt `query:"register-campaign-view"`
|
||||||
DeleteCampaign *sqlx.Stmt `query:"delete-campaign"`
|
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"`
|
InsertMedia *sqlx.Stmt `query:"insert-media"`
|
||||||
GetMedia *sqlx.Stmt `query:"get-media"`
|
GetMedia *sqlx.Stmt `query:"get-media"`
|
||||||
DeleteMedia *sqlx.Stmt `query:"delete-media"`
|
DeleteMedia *sqlx.Stmt `query:"delete-media"`
|
||||||
|
|
17
schema.sql
17
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 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 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 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 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');
|
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
|
-- subscribers
|
||||||
DROP TABLE IF EXISTS subscribers CASCADE;
|
DROP TABLE IF EXISTS subscribers CASCADE;
|
||||||
CREATE TABLE subscribers (
|
CREATE TABLE subscribers (
|
||||||
|
|
138
users.go
138
users.go
|
@ -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})
|
|
||||||
}
|
|
Loading…
Reference in New Issue