2018-10-25 21:51:47 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2019-01-03 19:18:47 +08:00
|
|
|
"net/http"
|
2018-10-25 21:51:47 +08:00
|
|
|
"net/url"
|
2019-07-21 22:41:11 +08:00
|
|
|
"regexp"
|
2018-10-25 21:51:47 +08:00
|
|
|
"strconv"
|
|
|
|
|
|
|
|
"github.com/labstack/echo"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// stdInputMaxLen is the maximum allowed length for a standard input field.
|
|
|
|
stdInputMaxLen = 200
|
|
|
|
|
|
|
|
// bodyMaxLen is the maximum allowed length for e-mail bodies.
|
|
|
|
bodyMaxLen = 1000000
|
|
|
|
|
|
|
|
// defaultPerPage is the default number of results returned in an GET call.
|
|
|
|
defaultPerPage = 20
|
|
|
|
|
|
|
|
// maxPerPage is the maximum number of allowed for paginated records.
|
|
|
|
maxPerPage = 100
|
|
|
|
)
|
|
|
|
|
|
|
|
type okResp struct {
|
|
|
|
Data interface{} `json:"data"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// pagination represents a query's pagination (limit, offset) related values.
|
|
|
|
type pagination struct {
|
|
|
|
PerPage int `json:"per_page"`
|
|
|
|
Page int `json:"page"`
|
|
|
|
Offset int `json:"offset"`
|
|
|
|
Limit int `json:"limit"`
|
|
|
|
}
|
|
|
|
|
2019-07-21 22:41:11 +08:00
|
|
|
var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
|
|
|
|
2019-01-03 19:18:47 +08:00
|
|
|
// registerHandlers registers HTTP handlers.
|
|
|
|
func registerHandlers(e *echo.Echo) {
|
|
|
|
e.GET("/", handleIndexPage)
|
|
|
|
e.GET("/api/config.js", handleGetConfigScript)
|
|
|
|
e.GET("/api/dashboard/stats", handleGetDashboardStats)
|
|
|
|
|
|
|
|
e.GET("/api/subscribers/:id", handleGetSubscriber)
|
2019-07-21 21:48:41 +08:00
|
|
|
e.GET("/api/subscribers/:id/export", handleExportSubscriberData)
|
2019-01-03 19:18:47 +08:00
|
|
|
e.POST("/api/subscribers", handleCreateSubscriber)
|
|
|
|
e.PUT("/api/subscribers/:id", handleUpdateSubscriber)
|
|
|
|
e.PUT("/api/subscribers/blacklist", handleBlacklistSubscribers)
|
|
|
|
e.PUT("/api/subscribers/:id/blacklist", handleBlacklistSubscribers)
|
|
|
|
e.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
|
|
|
|
e.PUT("/api/subscribers/lists", handleManageSubscriberLists)
|
|
|
|
e.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
|
|
|
|
e.DELETE("/api/subscribers", handleDeleteSubscribers)
|
|
|
|
|
|
|
|
// Subscriber operations based on arbitrary SQL queries.
|
|
|
|
// These aren't very REST-like.
|
|
|
|
e.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
|
|
|
|
e.PUT("/api/subscribers/query/blacklist", handleBlacklistSubscribersByQuery)
|
|
|
|
e.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
|
|
|
|
e.GET("/api/subscribers", handleQuerySubscribers)
|
|
|
|
|
|
|
|
e.GET("/api/import/subscribers", handleGetImportSubscribers)
|
|
|
|
e.GET("/api/import/subscribers/logs", handleGetImportSubscriberStats)
|
|
|
|
e.POST("/api/import/subscribers", handleImportSubscribers)
|
|
|
|
e.DELETE("/api/import/subscribers", handleStopImportSubscribers)
|
|
|
|
|
|
|
|
e.GET("/api/lists", handleGetLists)
|
|
|
|
e.GET("/api/lists/:id", handleGetLists)
|
|
|
|
e.POST("/api/lists", handleCreateList)
|
|
|
|
e.PUT("/api/lists/:id", handleUpdateList)
|
|
|
|
e.DELETE("/api/lists/:id", handleDeleteLists)
|
|
|
|
|
|
|
|
e.GET("/api/campaigns", handleGetCampaigns)
|
|
|
|
e.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
|
|
|
|
e.GET("/api/campaigns/:id", handleGetCampaigns)
|
|
|
|
e.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
|
|
|
|
e.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
|
|
|
|
e.POST("/api/campaigns/:id/test", handleTestCampaign)
|
|
|
|
e.POST("/api/campaigns", handleCreateCampaign)
|
|
|
|
e.PUT("/api/campaigns/:id", handleUpdateCampaign)
|
|
|
|
e.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
|
|
|
|
e.DELETE("/api/campaigns/:id", handleDeleteCampaign)
|
|
|
|
|
|
|
|
e.GET("/api/media", handleGetMedia)
|
|
|
|
e.POST("/api/media", handleUploadMedia)
|
|
|
|
e.DELETE("/api/media/:id", handleDeleteMedia)
|
|
|
|
|
|
|
|
e.GET("/api/templates", handleGetTemplates)
|
|
|
|
e.GET("/api/templates/:id", handleGetTemplates)
|
|
|
|
e.GET("/api/templates/:id/preview", handlePreviewTemplate)
|
|
|
|
e.POST("/api/templates/preview", handlePreviewTemplate)
|
|
|
|
e.POST("/api/templates", handleCreateTemplate)
|
|
|
|
e.PUT("/api/templates/:id", handleUpdateTemplate)
|
|
|
|
e.PUT("/api/templates/:id/default", handleTemplateSetDefault)
|
|
|
|
e.DELETE("/api/templates/:id", handleDeleteTemplate)
|
|
|
|
|
|
|
|
// Subscriber facing views.
|
2019-07-21 22:41:11 +08:00
|
|
|
e.GET("/subscription/:campUUID/:subUUID", validateUUID(handleSubscriptionPage,
|
|
|
|
"campUUID", "subUUID"))
|
|
|
|
e.POST("/subscription/:campUUID/:subUUID", validateUUID(handleSubscriptionPage,
|
|
|
|
"campUUID", "subUUID"))
|
|
|
|
e.POST("/subscription/export/:subUUID", validateUUID(handleSelfExportSubscriberData,
|
|
|
|
"subUUID"))
|
|
|
|
e.POST("/subscription/wipe/:subUUID", validateUUID(handleWipeSubscriberData,
|
|
|
|
"subUUID"))
|
|
|
|
e.GET("/link/:linkUUID/:campUUID/:subUUID", validateUUID(handleLinkRedirect,
|
|
|
|
"linkUUID", "campUUID", "subUUID"))
|
|
|
|
e.GET("/campaign/:campUUID/:subUUID/px.png", validateUUID(handleRegisterCampaignView,
|
|
|
|
"campUUID", "subUUID"))
|
2019-01-03 19:18:47 +08:00
|
|
|
|
|
|
|
// Static views.
|
|
|
|
e.GET("/lists", handleIndexPage)
|
|
|
|
e.GET("/subscribers", handleIndexPage)
|
|
|
|
e.GET("/subscribers/lists/:listID", handleIndexPage)
|
|
|
|
e.GET("/subscribers/import", handleIndexPage)
|
|
|
|
e.GET("/campaigns", handleIndexPage)
|
|
|
|
e.GET("/campaigns/new", handleIndexPage)
|
|
|
|
e.GET("/campaigns/media", handleIndexPage)
|
|
|
|
e.GET("/campaigns/templates", handleIndexPage)
|
|
|
|
e.GET("/campaigns/:campignID", handleIndexPage)
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// handleIndex is the root handler that renders the login page if there's no
|
|
|
|
// authenticated session, or redirects to the dashboard, if there's one.
|
|
|
|
func handleIndexPage(c echo.Context) error {
|
|
|
|
app := c.Get("app").(*App)
|
2019-01-03 19:18:47 +08:00
|
|
|
|
|
|
|
b, err := app.FS.Read("/frontend/index.html")
|
|
|
|
if err != nil {
|
2019-03-09 15:44:53 +08:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
2019-01-03 19:18:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
c.Response().Header().Set("Content-Type", "text/html")
|
|
|
|
return c.String(http.StatusOK, string(b))
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
2019-07-21 22:41:11 +08:00
|
|
|
// validateUUID validates the UUID string format for a given set of params.
|
|
|
|
func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
|
|
|
|
return func(c echo.Context) error {
|
|
|
|
for _, p := range params {
|
|
|
|
if !reUUID.MatchString(c.Param(p)) {
|
|
|
|
return c.Render(http.StatusBadRequest, "message",
|
|
|
|
makeMsgTpl("Invalid request", "",
|
|
|
|
`One or more UUIDs in the request are invalid.`))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return next(c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-10-25 21:51:47 +08:00
|
|
|
// getPagination takes form values and extracts pagination values from it.
|
|
|
|
func getPagination(q url.Values) pagination {
|
|
|
|
var (
|
2019-05-15 00:36:14 +08:00
|
|
|
page, _ = strconv.Atoi(q.Get("page"))
|
|
|
|
perPage = defaultPerPage
|
2018-10-25 21:51:47 +08:00
|
|
|
)
|
|
|
|
|
2019-05-15 00:36:14 +08:00
|
|
|
pp := q.Get("per_page")
|
|
|
|
if pp == "all" {
|
|
|
|
// No limit.
|
|
|
|
perPage = 0
|
|
|
|
} else {
|
|
|
|
ppi, _ := strconv.Atoi(pp)
|
|
|
|
if ppi < 1 || ppi > maxPerPage {
|
|
|
|
perPage = defaultPerPage
|
|
|
|
}
|
2018-10-25 21:51:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if page < 1 {
|
|
|
|
page = 0
|
|
|
|
} else {
|
|
|
|
page--
|
|
|
|
}
|
|
|
|
|
|
|
|
return pagination{
|
|
|
|
Page: page + 1,
|
|
|
|
PerPage: perPage,
|
|
|
|
Offset: page * perPage,
|
|
|
|
Limit: perPage,
|
|
|
|
}
|
|
|
|
}
|