mirror of
https://github.com/knadh/listmonk.git
synced 2025-12-05 16:00:03 +01:00
Refactor all HTTP handlers and attach them to a single struct.
- Attach all HTTP handlers to a new `Handlers{}` struct.
- Remove all `handle*` function prefixes.
- Remove awkward, repetitive `app = c.Get("app").(*App)` from all handlers
and instead, simply access it from `h.app` from `Handlers{}`
Originally proposed in #2292.
This commit is contained in:
62
cmd/admin.go
62
cmd/admin.go
@@ -23,50 +23,42 @@ type serverConfig struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// handleGetServerConfig returns general server config.
|
||||
func handleGetServerConfig(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetServerConfig returns general server config.
|
||||
func (h *Handlers) GetServerConfig(c echo.Context) error {
|
||||
out := serverConfig{
|
||||
RootURL: app.constants.RootURL,
|
||||
FromEmail: app.constants.FromEmail,
|
||||
Lang: app.constants.Lang,
|
||||
Permissions: app.constants.PermissionsRaw,
|
||||
HasLegacyUser: app.constants.HasLegacyUser,
|
||||
RootURL: h.app.constants.RootURL,
|
||||
FromEmail: h.app.constants.FromEmail,
|
||||
Lang: h.app.constants.Lang,
|
||||
Permissions: h.app.constants.PermissionsRaw,
|
||||
HasLegacyUser: h.app.constants.HasLegacyUser,
|
||||
}
|
||||
|
||||
// Language list.
|
||||
langList, err := getI18nLangList(app)
|
||||
langList, err := getI18nLangList(h.app)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
fmt.Sprintf("Error loading language list: %v", err))
|
||||
}
|
||||
out.Langs = langList
|
||||
|
||||
out.Messengers = make([]string, 0, len(app.messengers))
|
||||
for _, m := range app.messengers {
|
||||
out.Messengers = make([]string, 0, len(h.app.messengers))
|
||||
for _, m := range h.app.messengers {
|
||||
out.Messengers = append(out.Messengers, m.Name())
|
||||
}
|
||||
|
||||
app.Lock()
|
||||
out.NeedsRestart = app.needsRestart
|
||||
out.Update = app.update
|
||||
app.Unlock()
|
||||
h.app.Lock()
|
||||
out.NeedsRestart = h.app.needsRestart
|
||||
out.Update = h.app.update
|
||||
h.app.Unlock()
|
||||
out.Version = versionString
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetDashboardCharts returns chart data points to render ont he dashboard.
|
||||
func handleGetDashboardCharts(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetDashboardCharts returns chart data points to render ont he dashboard.
|
||||
func (h *Handlers) GetDashboardCharts(c echo.Context) error {
|
||||
// Get the chart data from the DB.
|
||||
out, err := app.core.GetDashboardCharts()
|
||||
out, err := h.app.core.GetDashboardCharts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,14 +66,10 @@ func handleGetDashboardCharts(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetDashboardCounts returns stats counts to show on the dashboard.
|
||||
func handleGetDashboardCounts(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetDashboardCounts returns stats counts to show on the dashboard.
|
||||
func (h *Handlers) GetDashboardCounts(c echo.Context) error {
|
||||
// Get the chart data from the DB.
|
||||
out, err := app.core.GetDashboardCounts()
|
||||
out, err := h.app.core.GetDashboardCounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -89,17 +77,13 @@ func handleGetDashboardCounts(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleReloadApp restarts the app.
|
||||
func handleReloadApp(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// ReloadApp sends a reload signal to the app, causing a full restart.
|
||||
func (h *Handlers) ReloadApp(c echo.Context) error {
|
||||
go func() {
|
||||
<-time.After(time.Millisecond * 500)
|
||||
|
||||
// Send the reload signal to trigger the wait loop in main.
|
||||
app.chReload <- syscall.SIGHUP
|
||||
h.app.chReload <- syscall.SIGHUP
|
||||
}()
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
|
||||
112
cmd/archive.go
112
cmd/archive.go
@@ -23,15 +23,11 @@ type campArchive struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// handleGetCampaignArchives renders the public campaign archives page.
|
||||
func handleGetCampaignArchives(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetCampaignArchives renders the public campaign archives page.
|
||||
func (h *Handlers) GetCampaignArchives(c echo.Context) error {
|
||||
// Get archives from the DB.
|
||||
pg := app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
camps, total, err := getCampaignArchives(pg.Offset, pg.Limit, false, app)
|
||||
pg := h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
camps, total, err := h.getCampaignArchives(pg.Offset, pg.Limit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,19 +49,15 @@ func handleGetCampaignArchives(c echo.Context) error {
|
||||
return c.JSON(200, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetCampaignArchivesFeed renders the public campaign archives RSS feed.
|
||||
func handleGetCampaignArchivesFeed(c echo.Context) error {
|
||||
// GetCampaignArchivesFeed renders the public campaign archives RSS feed.
|
||||
func (h *Handlers) GetCampaignArchivesFeed(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
var (
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
showFullContent = app.constants.EnablePublicArchiveRSSContent
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
showFullContent = h.app.constants.EnablePublicArchiveRSSContent
|
||||
)
|
||||
|
||||
// Get archives from the DB.
|
||||
camps, _, err := getCampaignArchives(pg.Offset, pg.Limit, showFullContent, app)
|
||||
camps, _, err := h.getCampaignArchives(pg.Offset, pg.Limit, showFullContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -89,35 +81,31 @@ func handleGetCampaignArchivesFeed(c echo.Context) error {
|
||||
|
||||
// Generate the feed.
|
||||
feed := &feeds.Feed{
|
||||
Title: app.constants.SiteName,
|
||||
Link: &feeds.Link{Href: app.constants.RootURL},
|
||||
Description: app.i18n.T("public.archiveTitle"),
|
||||
Title: h.app.constants.SiteName,
|
||||
Link: &feeds.Link{Href: h.app.constants.RootURL},
|
||||
Description: h.app.i18n.T("public.archiveTitle"),
|
||||
Items: out,
|
||||
}
|
||||
|
||||
if err := feed.WriteRss(c.Response().Writer); err != nil {
|
||||
app.log.Printf("error generating archive RSS feed: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorProcessingRequest"))
|
||||
h.app.log.Printf("error generating archive RSS feed: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.errorProcessingRequest"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCampaignArchivesPage renders the public campaign archives page.
|
||||
func handleCampaignArchivesPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CampaignArchivesPage renders the public campaign archives page.
|
||||
func (h *Handlers) CampaignArchivesPage(c echo.Context) error {
|
||||
// Get archives from the DB.
|
||||
pg := app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
out, total, err := getCampaignArchives(pg.Offset, pg.Limit, false, app)
|
||||
pg := h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
out, total, err := h.getCampaignArchives(pg.Offset, pg.Limit, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pg.SetTotal(total)
|
||||
|
||||
title := app.i18n.T("public.archiveTitle")
|
||||
title := h.app.i18n.T("public.archiveTitle")
|
||||
return c.Render(http.StatusOK, "archive", struct {
|
||||
Title string
|
||||
Description string
|
||||
@@ -127,12 +115,8 @@ func handleCampaignArchivesPage(c echo.Context) error {
|
||||
}{title, title, out, pg.TotalPages, template.HTML(pg.HTML("?page=%d"))})
|
||||
}
|
||||
|
||||
// handleCampaignArchivePage renders the public campaign archives page.
|
||||
func handleCampaignArchivePage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CampaignArchivePage renders the public campaign archives page.
|
||||
func (h *Handlers) CampaignArchivePage(c echo.Context) error {
|
||||
// ID can be the UUID or slug.
|
||||
var (
|
||||
idStr = c.Param("id")
|
||||
@@ -145,7 +129,7 @@ func handleCampaignArchivePage(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the campaign from the DB.
|
||||
pubCamp, err := app.core.GetArchivedCampaign(0, uuid, slug)
|
||||
pubCamp, err := h.app.core.GetArchivedCampaign(0, uuid, slug)
|
||||
if err != nil || pubCamp.Type != models.CampaignTypeRegular {
|
||||
notFound := false
|
||||
|
||||
@@ -162,48 +146,44 @@ func handleCampaignArchivePage(c echo.Context) error {
|
||||
// 404.
|
||||
if notFound {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", app.i18n.T("public.campaignNotFound")))
|
||||
makeMsgTpl(h.app.i18n.T("public.notFoundTitle"), "", h.app.i18n.T("public.campaignNotFound")))
|
||||
}
|
||||
|
||||
// Some other internal error.
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
// "Compile" the campaign template with appropriate data.
|
||||
out, err := compileArchiveCampaigns([]models.Campaign{pubCamp}, app)
|
||||
out, err := h.compileArchiveCampaigns([]models.Campaign{pubCamp})
|
||||
if err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
// Render the campaign body.
|
||||
camp := out[0].Campaign
|
||||
msg, err := app.manager.NewCampaignMessage(camp, out[0].Subscriber)
|
||||
msg, err := h.app.manager.NewCampaignMessage(camp, out[0].Subscriber)
|
||||
if err != nil {
|
||||
app.log.Printf("error rendering campaign: %v", err)
|
||||
h.app.log.Printf("error rendering campaign: %v", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
return c.HTML(http.StatusOK, string(msg.Body()))
|
||||
}
|
||||
|
||||
// handleCampaignArchivePageLatest renders the latest public campaign.
|
||||
func handleCampaignArchivePageLatest(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CampaignArchivePageLatest renders the latest public campaign.
|
||||
func (h *Handlers) CampaignArchivePageLatest(c echo.Context) error {
|
||||
// Get the latest campaign from the DB.
|
||||
camps, _, err := getCampaignArchives(0, 1, true, app)
|
||||
camps, _, err := h.getCampaignArchives(0, 1, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(camps) == 0 {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", app.i18n.T("public.campaignNotFound")))
|
||||
makeMsgTpl(h.app.i18n.T("public.notFoundTitle"), "", h.app.i18n.T("public.campaignNotFound")))
|
||||
}
|
||||
camp := camps[0]
|
||||
|
||||
@@ -211,13 +191,13 @@ func handleCampaignArchivePageLatest(c echo.Context) error {
|
||||
}
|
||||
|
||||
// getCampaignArchives fetches the public campaign archives from the DB.
|
||||
func getCampaignArchives(offset, limit int, renderBody bool, app *App) ([]campArchive, int, error) {
|
||||
pubCamps, total, err := app.core.GetArchivedCampaigns(offset, limit)
|
||||
func (h *Handlers) getCampaignArchives(offset, limit int, renderBody bool) ([]campArchive, int, error) {
|
||||
pubCamps, total, err := h.app.core.GetArchivedCampaigns(offset, limit)
|
||||
if err != nil {
|
||||
return []campArchive{}, total, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||
return []campArchive{}, total, echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("public.errorFetchingCampaign"))
|
||||
}
|
||||
|
||||
msgs, err := compileArchiveCampaigns(pubCamps, app)
|
||||
msgs, err := h.compileArchiveCampaigns(pubCamps)
|
||||
if err != nil {
|
||||
return []campArchive{}, total, err
|
||||
}
|
||||
@@ -235,14 +215,14 @@ func getCampaignArchives(offset, limit int, renderBody bool, app *App) ([]campAr
|
||||
|
||||
// The campaign may have a custom slug.
|
||||
if camp.ArchiveSlug.Valid {
|
||||
archive.URL, _ = url.JoinPath(app.constants.ArchiveURL, camp.ArchiveSlug.String)
|
||||
archive.URL, _ = url.JoinPath(h.app.constants.ArchiveURL, camp.ArchiveSlug.String)
|
||||
} else {
|
||||
archive.URL, _ = url.JoinPath(app.constants.ArchiveURL, camp.UUID)
|
||||
archive.URL, _ = url.JoinPath(h.app.constants.ArchiveURL, camp.UUID)
|
||||
}
|
||||
|
||||
// Render the full template body if requested.
|
||||
if renderBody {
|
||||
msg, err := app.manager.NewCampaignMessage(camp, m.Subscriber)
|
||||
msg, err := h.app.manager.NewCampaignMessage(camp, m.Subscriber)
|
||||
if err != nil {
|
||||
return []campArchive{}, total, err
|
||||
}
|
||||
@@ -256,7 +236,7 @@ func getCampaignArchives(offset, limit int, renderBody bool, app *App) ([]campAr
|
||||
}
|
||||
|
||||
// compileArchiveCampaigns compiles the campaign template with the subscriber data.
|
||||
func compileArchiveCampaigns(camps []models.Campaign, app *App) ([]manager.CampaignMessage, error) {
|
||||
func (h *Handlers) compileArchiveCampaigns(camps []models.Campaign) ([]manager.CampaignMessage, error) {
|
||||
|
||||
var (
|
||||
b = bytes.Buffer{}
|
||||
@@ -264,16 +244,16 @@ func compileArchiveCampaigns(camps []models.Campaign, app *App) ([]manager.Campa
|
||||
)
|
||||
for _, c := range camps {
|
||||
camp := c
|
||||
if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
app.log.Printf("error compiling template: %v", err)
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||
if err := camp.CompileTemplate(h.app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
h.app.log.Printf("error compiling template: %v", err)
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("public.errorFetchingCampaign"))
|
||||
}
|
||||
|
||||
// Load the dummy subscriber meta.
|
||||
var sub models.Subscriber
|
||||
if err := json.Unmarshal([]byte(camp.ArchiveMeta), &sub); err != nil {
|
||||
app.log.Printf("error unmarshalling campaign archive meta: %v", err)
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("public.errorFetchingCampaign"))
|
||||
h.app.log.Printf("error unmarshalling campaign archive meta: %v", err)
|
||||
return nil, echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("public.errorFetchingCampaign"))
|
||||
}
|
||||
|
||||
m := manager.CampaignMessage{
|
||||
|
||||
160
cmd/auth.go
160
cmd/auth.go
@@ -41,58 +41,50 @@ var oidcProviders = map[string]bool{
|
||||
"github.com": true,
|
||||
}
|
||||
|
||||
// handleLoginPage renders the login page and handles the login form.
|
||||
func handleLoginPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// LoginPage renders the login page and handles the login form.
|
||||
func (h *Handlers) LoginPage(c echo.Context) error {
|
||||
// Has the user been setup?
|
||||
app.Lock()
|
||||
needsUserSetup := app.needsUserSetup
|
||||
app.Unlock()
|
||||
h.app.Lock()
|
||||
needsUserSetup := h.app.needsUserSetup
|
||||
h.app.Unlock()
|
||||
|
||||
if needsUserSetup {
|
||||
return handleLoginSetupPage(c)
|
||||
return h.LoginSetupPage(c)
|
||||
}
|
||||
|
||||
// Process POST login request.
|
||||
var loginErr error
|
||||
if c.Request().Method == http.MethodPost {
|
||||
loginErr = doLogin(c)
|
||||
loginErr = h.doLogin(c)
|
||||
if loginErr == nil {
|
||||
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.FormValue("next")))
|
||||
}
|
||||
}
|
||||
|
||||
// Render the page, with or without POST.
|
||||
return renderLoginPage(c, loginErr)
|
||||
return h.renderLoginPage(c, loginErr)
|
||||
}
|
||||
|
||||
// handleLoginSetupPage renders the first time user login page and handles the login form.
|
||||
func handleLoginSetupPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// LoginSetupPage renders the first time user login page and handles the login form.
|
||||
func (h *Handlers) LoginSetupPage(c echo.Context) error {
|
||||
// Process POST login request.
|
||||
var loginErr error
|
||||
if c.Request().Method == http.MethodPost {
|
||||
loginErr = doFirstTimeSetup(c)
|
||||
loginErr = h.doFirstTimeSetup(c)
|
||||
if loginErr == nil {
|
||||
app.Lock()
|
||||
app.needsUserSetup = false
|
||||
app.Unlock()
|
||||
h.app.Lock()
|
||||
h.app.needsUserSetup = false
|
||||
h.app.Unlock()
|
||||
return c.Redirect(http.StatusFound, utils.SanitizeURI(c.FormValue("next")))
|
||||
}
|
||||
}
|
||||
|
||||
// Render the page, with or without POST.
|
||||
return renderLoginSetupPage(c, loginErr)
|
||||
return h.renderLoginSetupPage(c, loginErr)
|
||||
}
|
||||
|
||||
// handleLogout logs a user out.
|
||||
func handleLogout(c echo.Context) error {
|
||||
// Logout logs a user out.
|
||||
func (h *Handlers) Logout(c echo.Context) error {
|
||||
// Delete the session from the DB and cookie.
|
||||
sess := c.Get(auth.SessionKey).(*simplesessions.Session)
|
||||
_ = sess.Destroy()
|
||||
@@ -100,16 +92,12 @@ func handleLogout(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleOIDCLogin initializes an OIDC request and redirects to the OIDC provider for login.
|
||||
func handleOIDCLogin(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// OIDCLogin initializes an OIDC request and redirects to the OIDC provider for login.
|
||||
func (h *Handlers) OIDCLogin(c echo.Context) error {
|
||||
// Verify that the request came from the login page (CSRF).
|
||||
nonce, err := c.Cookie("nonce")
|
||||
if err != nil || nonce.Value == "" || nonce.Value != c.FormValue("nonce") {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest"))
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, h.app.i18n.T("users.invalidRequest"))
|
||||
}
|
||||
|
||||
// Sanitize the URL and make it relative.
|
||||
@@ -123,72 +111,68 @@ func handleOIDCLogin(c echo.Context) error {
|
||||
|
||||
b, err := json.Marshal(state)
|
||||
if err != nil {
|
||||
app.log.Printf("error marshalling OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("globals.messages.internalError"))
|
||||
h.app.log.Printf("error marshalling OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
// Redirect to the external OIDC provider.
|
||||
return c.Redirect(http.StatusFound, app.auth.GetOIDCAuthURL(base64.URLEncoding.EncodeToString(b), nonce.Value))
|
||||
return c.Redirect(http.StatusFound, h.app.auth.GetOIDCAuthURL(base64.URLEncoding.EncodeToString(b), nonce.Value))
|
||||
}
|
||||
|
||||
// handleOIDCFinish receives the redirect callback from the OIDC provider and completes the handshake.
|
||||
func handleOIDCFinish(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// OIDCFinish receives the redirect callback from the OIDC provider and completes the handshake.
|
||||
func (h *Handlers) OIDCFinish(c echo.Context) error {
|
||||
// Verify that the request actually originated from the login request (which sets the nonce value).
|
||||
nonce, err := c.Cookie("nonce")
|
||||
if err != nil || nonce.Value == "" {
|
||||
return renderLoginPage(c, echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest")))
|
||||
return h.renderLoginPage(c, echo.NewHTTPError(http.StatusUnauthorized, h.app.i18n.T("users.invalidRequest")))
|
||||
}
|
||||
|
||||
// Validate the OIDC token.
|
||||
oidcToken, claims, err := app.auth.ExchangeOIDCToken(c.Request().URL.Query().Get("code"), nonce.Value)
|
||||
oidcToken, claims, err := h.app.auth.ExchangeOIDCToken(c.Request().URL.Query().Get("code"), nonce.Value)
|
||||
if err != nil {
|
||||
return renderLoginPage(c, err)
|
||||
return h.renderLoginPage(c, err)
|
||||
}
|
||||
|
||||
// Validate the state.
|
||||
var state oidcState
|
||||
stateB, err := base64.URLEncoding.DecodeString(c.QueryParam("state"))
|
||||
if err != nil {
|
||||
app.log.Printf("error decoding OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("globals.messages.internalError"))
|
||||
h.app.log.Printf("error decoding OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("globals.messages.internalError"))
|
||||
}
|
||||
if err := json.Unmarshal(stateB, &state); err != nil {
|
||||
app.log.Printf("error unmarshalling OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("globals.messages.internalError"))
|
||||
h.app.log.Printf("error unmarshalling OIDC state: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("globals.messages.internalError"))
|
||||
}
|
||||
if state.Nonce != nonce.Value {
|
||||
return renderLoginPage(c, echo.NewHTTPError(http.StatusUnauthorized, app.i18n.T("users.invalidRequest")))
|
||||
return h.renderLoginPage(c, echo.NewHTTPError(http.StatusUnauthorized, h.app.i18n.T("users.invalidRequest")))
|
||||
}
|
||||
|
||||
// Validate e-mail from the claim.
|
||||
email := strings.TrimSpace(claims.Email)
|
||||
if email == "" {
|
||||
return renderLoginPage(c, errors.New(app.i18n.Ts("globals.messages.invalidFields", "name", "email")))
|
||||
return h.renderLoginPage(c, errors.New(h.app.i18n.Ts("globals.messages.invalidFields", "name", "email")))
|
||||
}
|
||||
em, err := mail.ParseAddress(email)
|
||||
if err != nil {
|
||||
return renderLoginPage(c, err)
|
||||
return h.renderLoginPage(c, err)
|
||||
}
|
||||
email = strings.ToLower(em.Address)
|
||||
|
||||
// Get the user by e-mail received from OIDC.
|
||||
user, err := app.core.GetUser(0, "", email)
|
||||
user, err := h.app.core.GetUser(0, "", email)
|
||||
if err != nil {
|
||||
return renderLoginPage(c, err)
|
||||
return h.renderLoginPage(c, err)
|
||||
}
|
||||
|
||||
// Update the user login state (avatar, logged in date) in the DB.
|
||||
if err := app.core.UpdateUserLogin(user.ID, claims.Picture); err != nil {
|
||||
return renderLoginPage(c, err)
|
||||
if err := h.app.core.UpdateUserLogin(user.ID, claims.Picture); err != nil {
|
||||
return h.renderLoginPage(c, err)
|
||||
}
|
||||
|
||||
// Set the session in the DB and cookie.
|
||||
if err := app.auth.SaveSession(user, oidcToken, c); err != nil {
|
||||
return renderLoginPage(c, err)
|
||||
if err := h.app.auth.SaveSession(user, oidcToken, c); err != nil {
|
||||
return h.renderLoginPage(c, err)
|
||||
}
|
||||
|
||||
// Redirect to the next page.
|
||||
@@ -196,11 +180,7 @@ func handleOIDCFinish(c echo.Context) error {
|
||||
}
|
||||
|
||||
// renderLoginPage renders the login page and handles the login form.
|
||||
func renderLoginPage(c echo.Context, loginErr error) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) renderLoginPage(c echo.Context, loginErr error) error {
|
||||
next := utils.SanitizeURI(c.FormValue("next"))
|
||||
if next == "/" {
|
||||
next = uriAdmin
|
||||
@@ -210,9 +190,9 @@ func renderLoginPage(c echo.Context, loginErr error) error {
|
||||
oidcProvider = ""
|
||||
oidcLogo = ""
|
||||
)
|
||||
if app.constants.Security.OIDC.Enabled {
|
||||
if h.app.constants.Security.OIDC.Enabled {
|
||||
oidcLogo = "oidc.png"
|
||||
u, err := url.Parse(app.constants.Security.OIDC.Provider)
|
||||
u, err := url.Parse(h.app.constants.Security.OIDC.Provider)
|
||||
if err == nil {
|
||||
h := strings.Split(u.Hostname(), ".")
|
||||
|
||||
@@ -231,7 +211,7 @@ func renderLoginPage(c echo.Context, loginErr error) error {
|
||||
}
|
||||
|
||||
out := loginTpl{
|
||||
Title: app.i18n.T("users.login"),
|
||||
Title: h.app.i18n.T("users.login"),
|
||||
PasswordEnabled: true,
|
||||
OIDCProvider: oidcProvider,
|
||||
OIDCProviderLogo: oidcLogo,
|
||||
@@ -250,8 +230,8 @@ func renderLoginPage(c echo.Context, loginErr error) error {
|
||||
// Generate and set a nonce for preventing CSRF requests that will be valided in the subsequent requests.
|
||||
nonce, err := utils.GenerateRandomString(16)
|
||||
if err != nil {
|
||||
app.log.Printf("error generating OIDC nonce: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.internalError"))
|
||||
h.app.log.Printf("error generating OIDC nonce: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.internalError"))
|
||||
}
|
||||
c.SetCookie(&http.Cookie{
|
||||
Name: "nonce",
|
||||
@@ -267,18 +247,14 @@ func renderLoginPage(c echo.Context, loginErr error) error {
|
||||
}
|
||||
|
||||
// renderLoginSetupPage renders the first time user setup page.
|
||||
func renderLoginSetupPage(c echo.Context, loginErr error) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) renderLoginSetupPage(c echo.Context, loginErr error) error {
|
||||
next := utils.SanitizeURI(c.FormValue("next"))
|
||||
if next == "/" {
|
||||
next = uriAdmin
|
||||
}
|
||||
|
||||
out := loginTpl{
|
||||
Title: app.i18n.T("users.login"),
|
||||
Title: h.app.i18n.T("users.login"),
|
||||
PasswordEnabled: true,
|
||||
NextURI: next,
|
||||
}
|
||||
@@ -296,25 +272,21 @@ func renderLoginSetupPage(c echo.Context, loginErr error) error {
|
||||
}
|
||||
|
||||
// doLogin logs a user in with a username and password.
|
||||
func doLogin(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) doLogin(c echo.Context) error {
|
||||
var (
|
||||
username = strings.TrimSpace(c.FormValue("username"))
|
||||
password = strings.TrimSpace(c.FormValue("password"))
|
||||
)
|
||||
|
||||
if !strHasLen(username, 3, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
if !strHasLen(password, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
|
||||
// Log the user in by fetching and verifying credentials from the DB.
|
||||
user, err := app.core.LoginUser(username, password)
|
||||
user, err := h.app.core.LoginUser(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -325,7 +297,7 @@ func doLogin(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Set the session in the DB and cookie.
|
||||
if err := app.auth.SaveSession(user, "", c); err != nil {
|
||||
if err := h.app.auth.SaveSession(user, "", c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -333,11 +305,7 @@ func doLogin(c echo.Context) error {
|
||||
}
|
||||
|
||||
// doFirstTimeSetup sets a user up for the first time.
|
||||
func doFirstTimeSetup(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) doFirstTimeSetup(c echo.Context) error {
|
||||
var (
|
||||
email = strings.TrimSpace(c.FormValue("email"))
|
||||
username = strings.TrimSpace(c.FormValue("username"))
|
||||
@@ -345,16 +313,16 @@ func doFirstTimeSetup(c echo.Context) error {
|
||||
password2 = strings.TrimSpace(c.FormValue("password2"))
|
||||
)
|
||||
if !utils.ValidateEmail(email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
}
|
||||
if !strHasLen(username, 3, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
if !strHasLen(password, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
if password != password2 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("users.passwordMismatch"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("users.passwordMismatch"))
|
||||
}
|
||||
|
||||
// Create the default "Super Admin" with all permission.
|
||||
@@ -362,12 +330,12 @@ func doFirstTimeSetup(c echo.Context) error {
|
||||
Type: auth.RoleTypeUser,
|
||||
Name: null.NewString("Super Admin", true),
|
||||
}
|
||||
for p := range app.constants.Permissions {
|
||||
for p := range h.app.constants.Permissions {
|
||||
r.Permissions = append(r.Permissions, p)
|
||||
}
|
||||
|
||||
// Create the role in the DB.
|
||||
role, err := app.core.CreateRole(r)
|
||||
role, err := h.app.core.CreateRole(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -384,18 +352,18 @@ func doFirstTimeSetup(c echo.Context) error {
|
||||
UserRoleID: role.ID,
|
||||
Status: auth.UserStatusEnabled,
|
||||
}
|
||||
if _, err := app.core.CreateUser(u); err != nil {
|
||||
if _, err := h.app.core.CreateUser(u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Log the user in directly.
|
||||
user, err := app.core.LoginUser(username, password)
|
||||
user, err := h.app.core.LoginUser(username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the session in the DB and cookie.
|
||||
if err := app.auth.SaveSession(user, "", c); err != nil {
|
||||
if err := h.app.auth.SaveSession(user, "", c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
114
cmd/bounce.go
114
cmd/bounce.go
@@ -11,16 +11,12 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleGetBounces handles retrieval of bounce records.
|
||||
func handleGetBounces(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetBounces handles retrieval of bounce records.
|
||||
func (h *Handlers) GetBounces(c echo.Context) error {
|
||||
// Fetch one bounce from the DB.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id > 0 {
|
||||
out, err := app.core.GetBounce(id)
|
||||
out, err := h.app.core.GetBounce(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -30,13 +26,13 @@ func handleGetBounces(c echo.Context) error {
|
||||
|
||||
// Query and fetch bounces from the DB.
|
||||
var (
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
campID, _ = strconv.Atoi(c.QueryParam("campaign_id"))
|
||||
source = c.FormValue("source")
|
||||
orderBy = c.FormValue("order_by")
|
||||
order = c.FormValue("order")
|
||||
)
|
||||
res, total, err := app.core.QueryBounces(campID, 0, source, orderBy, order, pg.Offset, pg.Limit)
|
||||
res, total, err := h.app.core.QueryBounces(campID, 0, source, orderBy, order, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -56,19 +52,15 @@ func handleGetBounces(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetSubscriberBounces retrieves a subscriber's bounce records.
|
||||
func handleGetSubscriberBounces(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetSubscriberBounces retrieves a subscriber's bounce records.
|
||||
func (h *Handlers) GetSubscriberBounces(c echo.Context) error {
|
||||
subID, _ := strconv.Atoi(c.Param("id"))
|
||||
if subID < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Query and fetch bounces from the DB.
|
||||
out, _, err := app.core.QueryBounces(0, subID, "", "", "", 0, 1000)
|
||||
out, _, err := h.app.core.QueryBounces(0, subID, "", "", "", 0, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,12 +68,8 @@ func handleGetSubscriberBounces(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleDeleteBounces handles bounce deletion, either a single one (ID in the URI), or a list.
|
||||
func handleDeleteBounces(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteBounces handles bounce deletion, either a single one (ID in the URI), or a list.
|
||||
func (h *Handlers) DeleteBounces(c echo.Context) error {
|
||||
// Is it an /:id call?
|
||||
var (
|
||||
all, _ = strconv.ParseBool(c.QueryParam("all"))
|
||||
@@ -92,41 +80,37 @@ func handleDeleteBounces(c echo.Context) error {
|
||||
if idStr != "" {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
ids = append(ids, id)
|
||||
} else if !all {
|
||||
// There are multiple IDs in the query string.
|
||||
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidID", "error", err.Error()))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidID", "error", err.Error()))
|
||||
}
|
||||
|
||||
if len(i) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidID"))
|
||||
}
|
||||
ids = i
|
||||
}
|
||||
|
||||
// Delete bounces from the DB.
|
||||
if err := app.core.DeleteBounces(ids); err != nil {
|
||||
if err := h.app.core.DeleteBounces(ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleBounceWebhook renders the HTML preview of a template.
|
||||
func handleBounceWebhook(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// BounceWebhook renders the HTML preview of a template.
|
||||
func (h *Handlers) BounceWebhook(c echo.Context) error {
|
||||
// Read the request body instead of using c.Bind() to read to save the entire raw request as meta.
|
||||
rawReq, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
app.log.Printf("error reading ses notification body: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
||||
h.app.log.Printf("error reading ses notification body: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -139,10 +123,10 @@ func handleBounceWebhook(c echo.Context) error {
|
||||
case service == "":
|
||||
var b models.Bounce
|
||||
if err := json.Unmarshal(rawReq, &b); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidData")+":"+err.Error())
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidData")+":"+err.Error())
|
||||
}
|
||||
|
||||
if bv, err := validateBounceFields(b, app); err != nil {
|
||||
if bv, err := h.validateBounceFields(b); err != nil {
|
||||
return err
|
||||
} else {
|
||||
b = bv
|
||||
@@ -159,99 +143,99 @@ func handleBounceWebhook(c echo.Context) error {
|
||||
bounces = append(bounces, b)
|
||||
|
||||
// Amazon SES.
|
||||
case service == "ses" && app.constants.BounceSESEnabled:
|
||||
case service == "ses" && h.app.constants.BounceSESEnabled:
|
||||
switch c.Request().Header.Get("X-Amz-Sns-Message-Type") {
|
||||
// SNS webhook registration confirmation. Only after these are processed will the endpoint
|
||||
// start getting bounce notifications.
|
||||
case "SubscriptionConfirmation", "UnsubscribeConfirmation":
|
||||
if err := app.bounce.SES.ProcessSubscription(rawReq); err != nil {
|
||||
app.log.Printf("error processing SNS (SES) subscription: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
if err := h.app.bounce.SES.ProcessSubscription(rawReq); err != nil {
|
||||
h.app.log.Printf("error processing SNS (SES) subscription: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
// Bounce notification.
|
||||
case "Notification":
|
||||
b, err := app.bounce.SES.ProcessBounce(rawReq)
|
||||
b, err := h.app.bounce.SES.ProcessBounce(rawReq)
|
||||
if err != nil {
|
||||
app.log.Printf("error processing SES notification: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
h.app.log.Printf("error processing SES notification: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
bounces = append(bounces, b)
|
||||
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
// SendGrid.
|
||||
case service == "sendgrid" && app.constants.BounceSendgridEnabled:
|
||||
case service == "sendgrid" && h.app.constants.BounceSendgridEnabled:
|
||||
var (
|
||||
sig = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Signature")
|
||||
ts = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Timestamp")
|
||||
)
|
||||
|
||||
// Sendgrid sends multiple bounces.
|
||||
bs, err := app.bounce.Sendgrid.ProcessBounce(sig, ts, rawReq)
|
||||
bs, err := h.app.bounce.Sendgrid.ProcessBounce(sig, ts, rawReq)
|
||||
if err != nil {
|
||||
app.log.Printf("error processing sendgrid notification: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
h.app.log.Printf("error processing sendgrid notification: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
bounces = append(bounces, bs...)
|
||||
|
||||
// Postmark.
|
||||
case service == "postmark" && app.constants.BouncePostmarkEnabled:
|
||||
bs, err := app.bounce.Postmark.ProcessBounce(rawReq, c)
|
||||
case service == "postmark" && h.app.constants.BouncePostmarkEnabled:
|
||||
bs, err := h.app.bounce.Postmark.ProcessBounce(rawReq, c)
|
||||
if err != nil {
|
||||
app.log.Printf("error processing postmark notification: %v", err)
|
||||
h.app.log.Printf("error processing postmark notification: %v", err)
|
||||
if _, ok := err.(*echo.HTTPError); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
bounces = append(bounces, bs...)
|
||||
|
||||
// ForwardEmail.
|
||||
case service == "forwardemail" && app.constants.BounceForwardemailEnabled:
|
||||
case service == "forwardemail" && h.app.constants.BounceForwardemailEnabled:
|
||||
var (
|
||||
sig = c.Request().Header.Get("X-Webhook-Signature")
|
||||
)
|
||||
|
||||
bs, err := app.bounce.Forwardemail.ProcessBounce(sig, rawReq)
|
||||
bs, err := h.app.bounce.Forwardemail.ProcessBounce(sig, rawReq)
|
||||
if err != nil {
|
||||
app.log.Printf("error processing forwardemail notification: %v", err)
|
||||
h.app.log.Printf("error processing forwardemail notification: %v", err)
|
||||
if _, ok := err.(*echo.HTTPError); ok {
|
||||
return err
|
||||
}
|
||||
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
bounces = append(bounces, bs...)
|
||||
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("bounces.unknownService"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("bounces.unknownService"))
|
||||
}
|
||||
|
||||
// Insert bounces into the DB.
|
||||
for _, b := range bounces {
|
||||
if err := app.bounce.Record(b); err != nil {
|
||||
app.log.Printf("error recording bounce: %v", err)
|
||||
if err := h.app.bounce.Record(b); err != nil {
|
||||
h.app.log.Printf("error recording bounce: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
func validateBounceFields(b models.Bounce, app *App) (models.Bounce, error) {
|
||||
func (h *Handlers) validateBounceFields(b models.Bounce) (models.Bounce, error) {
|
||||
if b.Email == "" && b.SubscriberUUID == "" {
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email / subscriber_uuid"))
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "email / subscriber_uuid"))
|
||||
}
|
||||
|
||||
if b.SubscriberUUID != "" && !reUUID.MatchString(b.SubscriberUUID) {
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_uuid"))
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "subscriber_uuid"))
|
||||
}
|
||||
|
||||
if b.Email != "" {
|
||||
em, err := app.importer.SanitizeEmail(b.Email)
|
||||
em, err := h.app.importer.SanitizeEmail(b.Email)
|
||||
if err != nil {
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -259,7 +243,7 @@ func validateBounceFields(b models.Bounce, app *App) (models.Bounce, error) {
|
||||
}
|
||||
|
||||
if b.Type != models.BounceTypeHard && b.Type != models.BounceTypeSoft && b.Type != models.BounceTypeComplaint {
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "type"))
|
||||
return b, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "type"))
|
||||
}
|
||||
|
||||
return b, nil
|
||||
|
||||
298
cmd/campaigns.go
298
cmd/campaigns.go
@@ -46,16 +46,14 @@ type campContentReq struct {
|
||||
}
|
||||
|
||||
var (
|
||||
regexFromAddress = regexp.MustCompile(`((.+?)\s)?<(.+?)@(.+?)>`)
|
||||
regexSlug = regexp.MustCompile(`[^\p{L}\p{M}\p{N}]`)
|
||||
reFromAddress = regexp.MustCompile(`((.+?)\s)?<(.+?)@(.+?)>`)
|
||||
reSlug = regexp.MustCompile(`[^\p{L}\p{M}\p{N}]`)
|
||||
)
|
||||
|
||||
// handleGetCampaigns handles retrieval of campaigns.
|
||||
func handleGetCampaigns(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// GetCampaigns handles retrieval of campaigns.
|
||||
func (h *Handlers) GetCampaigns(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var (
|
||||
hasAllPerm = user.HasPerm(auth.PermCampaignsGetAll)
|
||||
@@ -69,7 +67,7 @@ func handleGetCampaigns(c echo.Context) error {
|
||||
}
|
||||
|
||||
var (
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
|
||||
status = c.QueryParams()["status"]
|
||||
tags = c.QueryParams()["tag"]
|
||||
@@ -80,7 +78,7 @@ func handleGetCampaigns(c echo.Context) error {
|
||||
)
|
||||
|
||||
// Query and retrieve campaigns from the DB.
|
||||
res, total, err := app.core.QueryCampaigns(query, status, tags, orderBy, order, hasAllPerm, permittedLists, pg.Offset, pg.Limit)
|
||||
res, total, err := h.app.core.QueryCampaigns(query, status, tags, orderBy, order, hasAllPerm, permittedLists, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -108,24 +106,20 @@ func handleGetCampaigns(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetCampaign handles retrieval of campaigns.
|
||||
func handleGetCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetCampaign handles retrieval of campaigns.
|
||||
func (h *Handlers) GetCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the campaign from the DB.
|
||||
out, err := app.core.GetCampaign(id, "", "")
|
||||
out, err := h.app.core.GetCampaign(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -139,25 +133,21 @@ func handleGetCampaign(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handlePreviewCampaign renders the HTML preview of a campaign body.
|
||||
func handlePreviewCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// PreviewCampaign renders the HTML preview of a campaign body.
|
||||
func (h *Handlers) PreviewCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch the campaign body from the DB.
|
||||
tplID, _ := strconv.Atoi(c.FormValue("template_id"))
|
||||
camp, err := app.core.GetCampaignForPreview(id, tplID)
|
||||
camp, err := h.app.core.GetCampaignForPreview(id, tplID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,18 +161,18 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||
// Use a dummy campaign ID to prevent views and clicks from {{ TrackView }}
|
||||
// and {{ TrackLink }} being registered on preview.
|
||||
camp.UUID = dummySubscriber.UUID
|
||||
if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
app.log.Printf("error compiling template: %v", err)
|
||||
if err := camp.CompileTemplate(h.app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
h.app.log.Printf("error compiling template: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Render the message body.
|
||||
msg, err := app.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
msg, err := h.app.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
if err != nil {
|
||||
app.log.Printf("error rendering message: %v", err)
|
||||
h.app.log.Printf("error rendering message: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
}
|
||||
|
||||
if camp.ContentType == models.CampaignContentTypePlain {
|
||||
@@ -192,15 +182,11 @@ func handlePreviewCampaign(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, string(msg.Body()))
|
||||
}
|
||||
|
||||
// handleCampaignContent handles campaign content (body) format conversions.
|
||||
func handleCampaignContent(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CampaignContent handles campaign content (body) format conversions.
|
||||
func (h *Handlers) CampaignContent(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
var camp campContentReq
|
||||
@@ -216,21 +202,17 @@ func handleCampaignContent(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleCreateCampaign handles campaign creation.
|
||||
// CreateCampaign handles campaign creation.
|
||||
// Newly created campaigns are always drafts.
|
||||
func handleCreateCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
o campReq
|
||||
)
|
||||
|
||||
func (h *Handlers) CreateCampaign(c echo.Context) error {
|
||||
var o campReq
|
||||
if err := c.Bind(&o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the campaign's 'opt-in', prepare a default message.
|
||||
if o.Type == models.CampaignTypeOptin {
|
||||
op, err := makeOptinCampaignMessage(o, app)
|
||||
op, err := h.makeOptinCampaignMessage(o)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -247,7 +229,7 @@ func handleCreateCampaign(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Validate.
|
||||
if c, err := validateCampaignFields(o, app); err != nil {
|
||||
if c, err := h.validateCampaignFields(o); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
o = c
|
||||
@@ -257,7 +239,7 @@ func handleCreateCampaign(c echo.Context) error {
|
||||
o.ArchiveTemplateID = o.TemplateID
|
||||
}
|
||||
|
||||
out, err := app.core.CreateCampaign(o.Campaign, o.ListIDs, o.MediaIDs)
|
||||
out, err := h.app.core.CreateCampaign(o.Campaign, o.ListIDs, o.MediaIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -265,32 +247,28 @@ func handleCreateCampaign(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateCampaign handles campaign modification.
|
||||
// UpdateCampaign handles campaign modification.
|
||||
// Campaigns that are done cannot be modified.
|
||||
func handleUpdateCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
id, _ = strconv.Atoi(c.Param("id"))
|
||||
)
|
||||
|
||||
func (h *Handlers) UpdateCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Retrieve the campaign from the DB.
|
||||
cm, err := app.core.GetCampaign(id, "", "")
|
||||
cm, err := h.app.core.GetCampaign(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !canEditCampaign(cm.Status) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.cantUpdate"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("campaigns.cantUpdate"))
|
||||
}
|
||||
|
||||
// Read the incoming params into the existing campaign fields from the DB.
|
||||
@@ -301,13 +279,13 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if c, err := validateCampaignFields(o, app); err != nil {
|
||||
if c, err := h.validateCampaignFields(o); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
o = c
|
||||
}
|
||||
|
||||
out, err := app.core.UpdateCampaign(id, o.Campaign, o.ListIDs, o.MediaIDs)
|
||||
out, err := h.app.core.UpdateCampaign(id, o.Campaign, o.ListIDs, o.MediaIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -315,19 +293,15 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateCampaignStatus handles campaign status modification.
|
||||
func handleUpdateCampaignStatus(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
id, _ = strconv.Atoi(c.Param("id"))
|
||||
)
|
||||
|
||||
// UpdateCampaignStatus handles campaign status modification.
|
||||
func (h *Handlers) UpdateCampaignStatus(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -339,28 +313,24 @@ func handleUpdateCampaignStatus(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Update the campaign status in the DB.
|
||||
out, err := app.core.UpdateCampaignStatus(id, req.Status)
|
||||
out, err := h.app.core.UpdateCampaignStatus(id, req.Status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the campaign is being stopped, send the signal to the manager to stop it in flight.
|
||||
if req.Status == models.CampaignStatusPaused || req.Status == models.CampaignStatusCancelled {
|
||||
app.manager.StopCampaign(id)
|
||||
h.app.manager.StopCampaign(id)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateCampaignArchive handles campaign status modification.
|
||||
func handleUpdateCampaignArchive(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
id, _ = strconv.Atoi(c.Param("id"))
|
||||
)
|
||||
|
||||
// UpdateCampaignArchive handles campaign status modification.
|
||||
func (h *Handlers) UpdateCampaignArchive(c echo.Context) error {
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := h.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -377,51 +347,43 @@ func handleUpdateCampaignArchive(c echo.Context) error {
|
||||
if req.ArchiveSlug != "" {
|
||||
// Format the slug to be alpha-numeric-dash.
|
||||
s := strings.ToLower(req.ArchiveSlug)
|
||||
s = strings.TrimSpace(regexSlug.ReplaceAllString(s, " "))
|
||||
s = strings.TrimSpace(reSlug.ReplaceAllString(s, " "))
|
||||
s = regexpSpaces.ReplaceAllString(s, "-")
|
||||
req.ArchiveSlug = s
|
||||
}
|
||||
|
||||
if err := app.core.UpdateCampaignArchive(id, req.Archive, req.TemplateID, req.Meta, req.ArchiveSlug); err != nil {
|
||||
if err := h.app.core.UpdateCampaignArchive(id, req.Archive, req.TemplateID, req.Meta, req.ArchiveSlug); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{req})
|
||||
}
|
||||
|
||||
// handleDeleteCampaign handles campaign deletion.
|
||||
// DeleteCampaign handles campaign deletion.
|
||||
// Only scheduled campaigns that have not started yet can be deleted.
|
||||
func handleDeleteCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
id, _ = strconv.Atoi(c.Param("id"))
|
||||
)
|
||||
|
||||
func (h *Handlers) DeleteCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the campaign from the DB.
|
||||
if err := app.core.DeleteCampaign(id); err != nil {
|
||||
if err := h.app.core.DeleteCampaign(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleGetRunningCampaignStats returns stats of a given set of campaign IDs.
|
||||
func handleGetRunningCampaignStats(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetRunningCampaignStats returns stats of a given set of campaign IDs.
|
||||
func (h *Handlers) GetRunningCampaignStats(c echo.Context) error {
|
||||
// Get the running campaign stats from the DB.
|
||||
out, err := app.core.GetRunningCampaignStats()
|
||||
out, err := h.app.core.GetRunningCampaignStats()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -444,27 +406,23 @@ func handleGetRunningCampaignStats(c echo.Context) error {
|
||||
out[i].NetRate = rate
|
||||
|
||||
// Realtime running rate over the last minute.
|
||||
out[i].Rate = app.manager.GetCampaignStats(c.ID).SendRate
|
||||
out[i].Rate = h.app.manager.GetCampaignStats(c.ID).SendRate
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleTestCampaign handles the sending of a campaign message to
|
||||
// TestCampaign handles the sending of a campaign message to
|
||||
// arbitrary subscribers for testing.
|
||||
func handleTestCampaign(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) TestCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.errorID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
if err := h.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -475,13 +433,13 @@ func handleTestCampaign(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Validate.
|
||||
if c, err := validateCampaignFields(req, app); err != nil {
|
||||
if c, err := h.validateCampaignFields(req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
req = c
|
||||
}
|
||||
if len(req.SubscriberEmails) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noSubsToTest"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("campaigns.noSubsToTest"))
|
||||
}
|
||||
|
||||
// Sanitize subscriber e-mails.
|
||||
@@ -490,14 +448,14 @@ func handleTestCampaign(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the subscribers from the DB by their e-mails.
|
||||
subs, err := app.core.GetSubscribersByEmail(req.SubscriberEmails)
|
||||
subs, err := h.app.core.GetSubscribersByEmail(req.SubscriberEmails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the campaign from the DB for previewing.
|
||||
tplID, _ := strconv.Atoi(c.FormValue("template_id"))
|
||||
camp, err := app.core.GetCampaignForPreview(id, tplID)
|
||||
camp, err := h.app.core.GetCampaignForPreview(id, tplID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -522,31 +480,27 @@ func handleTestCampaign(c echo.Context) error {
|
||||
for _, s := range subs {
|
||||
sub := s
|
||||
|
||||
if err := sendTestMessage(sub, &camp, app); err != nil {
|
||||
app.log.Printf("error sending test message: %v", err)
|
||||
if err := h.sendTestMessage(sub, &camp); err != nil {
|
||||
h.app.log.Printf("error sending test message: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("campaigns.errorSendTest", "error", err.Error()))
|
||||
h.app.i18n.Ts("campaigns.errorSendTest", "error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleGetCampaignViewAnalytics retrieves view counts for a campaign.
|
||||
func handleGetCampaignViewAnalytics(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetCampaignViewAnalytics retrieves view counts for a campaign.
|
||||
func (h *Handlers) GetCampaignViewAnalytics(c echo.Context) error {
|
||||
ids, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.missingFields", "name", "`id`"))
|
||||
h.app.i18n.Ts("globals.messages.missingFields", "name", "`id`"))
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -555,12 +509,12 @@ func handleGetCampaignViewAnalytics(c echo.Context) error {
|
||||
to = c.QueryParams().Get("to")
|
||||
)
|
||||
if !strHasLen(from, 10, 30) || !strHasLen(to, 10, 30) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("analytics.invalidDates"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("analytics.invalidDates"))
|
||||
}
|
||||
|
||||
// Campaign link stats.
|
||||
if typ == "links" {
|
||||
out, err := app.core.GetCampaignAnalyticsLinks(ids, typ, from, to)
|
||||
out, err := h.app.core.GetCampaignAnalyticsLinks(ids, typ, from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -569,7 +523,7 @@ func handleGetCampaignViewAnalytics(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the analytics numbers from the DB for the campaigns.
|
||||
out, err := app.core.GetCampaignAnalyticsCounts(ids, typ, from, to)
|
||||
out, err := h.app.core.GetCampaignAnalyticsCounts(ids, typ, from, to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -578,60 +532,60 @@ func handleGetCampaignViewAnalytics(c echo.Context) error {
|
||||
}
|
||||
|
||||
// sendTestMessage takes a campaign and a subscriber and sends out a sample campaign message.
|
||||
func sendTestMessage(sub models.Subscriber, camp *models.Campaign, app *App) error {
|
||||
if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
|
||||
app.log.Printf("error compiling template: %v", err)
|
||||
func (h *Handlers) sendTestMessage(sub models.Subscriber, camp *models.Campaign) error {
|
||||
if err := camp.CompileTemplate(h.app.manager.TemplateFuncs(camp)); err != nil {
|
||||
h.app.log.Printf("error compiling template: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Create a sample campaign message.
|
||||
msg, err := app.manager.NewCampaignMessage(camp, sub)
|
||||
msg, err := h.app.manager.NewCampaignMessage(camp, sub)
|
||||
if err != nil {
|
||||
app.log.Printf("error rendering message: %v", err)
|
||||
return echo.NewHTTPError(http.StatusNotFound, app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
h.app.log.Printf("error rendering message: %v", err)
|
||||
return echo.NewHTTPError(http.StatusNotFound, h.app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
}
|
||||
|
||||
return app.manager.PushCampaignMessage(msg)
|
||||
return h.app.manager.PushCampaignMessage(msg)
|
||||
}
|
||||
|
||||
// validateCampaignFields validates incoming campaign field values.
|
||||
func validateCampaignFields(c campReq, app *App) (campReq, error) {
|
||||
func (h *Handlers) validateCampaignFields(c campReq) (campReq, error) {
|
||||
if c.FromEmail == "" {
|
||||
c.FromEmail = app.constants.FromEmail
|
||||
} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
|
||||
if _, err := app.importer.SanitizeEmail(c.FromEmail); err != nil {
|
||||
return c, errors.New(app.i18n.T("campaigns.fieldInvalidFromEmail"))
|
||||
c.FromEmail = h.app.constants.FromEmail
|
||||
} else if !reFromAddress.Match([]byte(c.FromEmail)) {
|
||||
if _, err := h.app.importer.SanitizeEmail(c.FromEmail); err != nil {
|
||||
return c, errors.New(h.app.i18n.T("campaigns.fieldInvalidFromEmail"))
|
||||
}
|
||||
}
|
||||
|
||||
if !strHasLen(c.Name, 1, stdInputMaxLen) {
|
||||
return c, errors.New(app.i18n.T("campaigns.fieldInvalidName"))
|
||||
return c, errors.New(h.app.i18n.T("campaigns.fieldInvalidName"))
|
||||
}
|
||||
|
||||
// Larger char limit for subject as it can contain {{ go templating }} logic.
|
||||
if !strHasLen(c.Subject, 1, 5000) {
|
||||
return c, errors.New(app.i18n.T("campaigns.fieldInvalidSubject"))
|
||||
return c, errors.New(h.app.i18n.T("campaigns.fieldInvalidSubject"))
|
||||
}
|
||||
|
||||
// If there's a "send_at" date, it should be in the future.
|
||||
if c.SendAt.Valid {
|
||||
if c.SendAt.Time.Before(time.Now()) {
|
||||
return c, errors.New(app.i18n.T("campaigns.fieldInvalidSendAt"))
|
||||
return c, errors.New(h.app.i18n.T("campaigns.fieldInvalidSendAt"))
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.ListIDs) == 0 {
|
||||
return c, errors.New(app.i18n.T("campaigns.fieldInvalidListIDs"))
|
||||
return c, errors.New(h.app.i18n.T("campaigns.fieldInvalidListIDs"))
|
||||
}
|
||||
|
||||
if !app.manager.HasMessenger(c.Messenger) {
|
||||
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", c.Messenger))
|
||||
if !h.app.manager.HasMessenger(c.Messenger) {
|
||||
return c, errors.New(h.app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", c.Messenger))
|
||||
}
|
||||
|
||||
camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
|
||||
if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
|
||||
if err := c.CompileTemplate(h.app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
return c, errors.New(h.app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
|
||||
}
|
||||
|
||||
if len(c.Headers) == 0 {
|
||||
@@ -645,7 +599,7 @@ func validateCampaignFields(c campReq, app *App) (campReq, error) {
|
||||
if c.ArchiveSlug.String != "" {
|
||||
// Format the slug to be alpha-numeric-dash.
|
||||
s := strings.ToLower(c.ArchiveSlug.String)
|
||||
s = strings.TrimSpace(regexSlug.ReplaceAllString(s, " "))
|
||||
s = strings.TrimSpace(reSlug.ReplaceAllString(s, " "))
|
||||
s = regexpSpaces.ReplaceAllString(s, "-")
|
||||
|
||||
c.ArchiveSlug = null.NewString(s, true)
|
||||
@@ -657,29 +611,21 @@ func validateCampaignFields(c campReq, app *App) (campReq, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// canEditCampaign returns true if a campaign is in a status where updating
|
||||
// its properties is allowed.
|
||||
func canEditCampaign(status string) bool {
|
||||
return status == models.CampaignStatusDraft ||
|
||||
status == models.CampaignStatusPaused ||
|
||||
status == models.CampaignStatusScheduled
|
||||
}
|
||||
|
||||
// makeOptinCampaignMessage makes a default opt-in campaign message body.
|
||||
func makeOptinCampaignMessage(o campReq, app *App) (campReq, error) {
|
||||
func (h *Handlers) makeOptinCampaignMessage(o campReq) (campReq, error) {
|
||||
if len(o.ListIDs) == 0 {
|
||||
return o, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.fieldInvalidListIDs"))
|
||||
return o, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("campaigns.fieldInvalidListIDs"))
|
||||
}
|
||||
|
||||
// Fetch double opt-in lists from the given list IDs from the DB.
|
||||
lists, err := app.core.GetListsByOptin(o.ListIDs, models.ListOptinDouble)
|
||||
lists, err := h.app.core.GetListsByOptin(o.ListIDs, models.ListOptinDouble)
|
||||
if err != nil {
|
||||
return o, err
|
||||
}
|
||||
|
||||
// There are no double opt-in lists.
|
||||
if len(lists) == 0 {
|
||||
return o, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noOptinLists"))
|
||||
return o, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("campaigns.noOptinLists"))
|
||||
}
|
||||
|
||||
// Construct the opt-in URL with list IDs.
|
||||
@@ -692,13 +638,13 @@ func makeOptinCampaignMessage(o campReq, app *App) (campReq, error) {
|
||||
|
||||
// Prepare sample opt-in message for the campaign.
|
||||
var b bytes.Buffer
|
||||
if err := app.notifTpls.tpls.ExecuteTemplate(&b, "optin-campaign", struct {
|
||||
if err := h.app.notifTpls.tpls.ExecuteTemplate(&b, "optin-campaign", struct {
|
||||
Lists []models.List
|
||||
OptinURLAttr template.HTMLAttr
|
||||
}{lists, optinURLAttr}); err != nil {
|
||||
app.log.Printf("error compiling 'optin-campaign' template: %v", err)
|
||||
h.app.log.Printf("error compiling 'optin-campaign' template: %v", err)
|
||||
return o, echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
o.Body = b.String()
|
||||
@@ -706,11 +652,9 @@ func makeOptinCampaignMessage(o campReq, app *App) (campReq, error) {
|
||||
}
|
||||
|
||||
// checkCampaignPerm checks if the user has get or manage access to the given campaign.
|
||||
func checkCampaignPerm(types auth.PermType, id int, c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) checkCampaignPerm(types auth.PermType, id int, c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
perm := auth.PermCampaignsGet
|
||||
if types&auth.PermTypeGet != 0 {
|
||||
@@ -732,13 +676,21 @@ func checkCampaignPerm(types auth.PermType, id int, c echo.Context) error {
|
||||
// all campaigns. If there are no *_all permissions, then ensure that the
|
||||
// campaign belongs to the lists that the user has access to.
|
||||
if hasAllPerm, permittedListIDs := user.GetPermittedLists(auth.PermTypeGet | auth.PermTypeManage); !hasAllPerm {
|
||||
if ok, err := app.core.CampaignHasLists(id, permittedListIDs); err != nil {
|
||||
if ok, err := h.app.core.CampaignHasLists(id, permittedListIDs); err != nil {
|
||||
return err
|
||||
} else if !ok {
|
||||
return echo.NewHTTPError(http.StatusForbidden,
|
||||
app.i18n.Ts("globals.messages.permissionDenied", "name", perm))
|
||||
h.app.i18n.Ts("globals.messages.permissionDenied", "name", perm))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// canEditCampaign returns true if a campaign is in a status where updating
|
||||
// its properties is allowed.
|
||||
func canEditCampaign(status string) bool {
|
||||
return status == models.CampaignStatusDraft ||
|
||||
status == models.CampaignStatusPaused ||
|
||||
status == models.CampaignStatusScheduled
|
||||
}
|
||||
|
||||
@@ -9,21 +9,17 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleEventStream serves an endpoint that never closes and pushes a
|
||||
// EventStream serves an endpoint that never closes and pushes a
|
||||
// live event stream (text/event-stream) such as a error messages.
|
||||
func handleEventStream(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
h := c.Response().Header()
|
||||
h.Set(echo.HeaderContentType, "text/event-stream")
|
||||
h.Set(echo.HeaderCacheControl, "no-store")
|
||||
h.Set(echo.HeaderConnection, "keep-alive")
|
||||
func (h *Handlers) EventStream(c echo.Context) error {
|
||||
hdr := c.Response().Header()
|
||||
hdr.Set(echo.HeaderContentType, "text/event-stream")
|
||||
hdr.Set(echo.HeaderCacheControl, "no-store")
|
||||
hdr.Set(echo.HeaderConnection, "keep-alive")
|
||||
|
||||
// Subscribe to the event stream with a random ID.
|
||||
id := fmt.Sprintf("api:%v", time.Now().UnixNano())
|
||||
sub, err := app.events.Subscribe(id)
|
||||
sub, err := h.app.events.Subscribe(id)
|
||||
if err != nil {
|
||||
log.Fatalf("error subscribing to events: %v", err)
|
||||
}
|
||||
@@ -34,7 +30,7 @@ func handleEventStream(c echo.Context) error {
|
||||
case e := <-sub:
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
app.log.Printf("error marshalling event: %v", err)
|
||||
h.app.log.Printf("error marshalling event: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -43,7 +39,7 @@ func handleEventStream(c echo.Context) error {
|
||||
|
||||
case <-ctx.Done():
|
||||
// On HTTP connection close, unsubscribe.
|
||||
app.events.Unsubscribe(id)
|
||||
h.app.events.Unsubscribe(id)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
254
cmd/handlers.go
254
cmd/handlers.go
@@ -24,6 +24,10 @@ type okResp struct {
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
type Handlers struct {
|
||||
app *App
|
||||
}
|
||||
|
||||
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}$")
|
||||
)
|
||||
@@ -74,130 +78,134 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||
|
||||
// Public unauthenticated endpoints.
|
||||
p = e.Group("")
|
||||
|
||||
h = &Handlers{
|
||||
app: app,
|
||||
}
|
||||
)
|
||||
|
||||
// Authenticated endpoints.
|
||||
a.GET(path.Join(uriAdmin, ""), handleAdminPage)
|
||||
a.GET(path.Join(uriAdmin, ""), h.AdminPage)
|
||||
a.GET(path.Join(uriAdmin, "/custom.css"), serveCustomAppearance("admin.custom_css"))
|
||||
a.GET(path.Join(uriAdmin, "/custom.js"), serveCustomAppearance("admin.custom_js"))
|
||||
a.GET(path.Join(uriAdmin, "/*"), handleAdminPage)
|
||||
a.GET(path.Join(uriAdmin, "/*"), h.AdminPage)
|
||||
|
||||
pm := app.auth.Perm
|
||||
|
||||
// API endpoints.
|
||||
api.GET("/api/health", handleHealthCheck)
|
||||
api.GET("/api/config", handleGetServerConfig)
|
||||
api.GET("/api/lang/:lang", handleGetI18nLang)
|
||||
api.GET("/api/dashboard/charts", handleGetDashboardCharts)
|
||||
api.GET("/api/dashboard/counts", handleGetDashboardCounts)
|
||||
api.GET("/api/health", h.HealthCheck)
|
||||
api.GET("/api/config", h.GetServerConfig)
|
||||
api.GET("/api/lang/:lang", h.GetI18nLang)
|
||||
api.GET("/api/dashboard/charts", h.GetDashboardCharts)
|
||||
api.GET("/api/dashboard/counts", h.GetDashboardCounts)
|
||||
|
||||
api.GET("/api/settings", pm(handleGetSettings, "settings:get"))
|
||||
api.PUT("/api/settings", pm(handleUpdateSettings, "settings:manage"))
|
||||
api.POST("/api/settings/smtp/test", pm(handleTestSMTPSettings, "settings:manage"))
|
||||
api.POST("/api/admin/reload", pm(handleReloadApp, "settings:manage"))
|
||||
api.GET("/api/logs", pm(handleGetLogs, "settings:get"))
|
||||
api.GET("/api/events", pm(handleEventStream, "settings:get"))
|
||||
api.GET("/api/about", handleGetAboutInfo)
|
||||
api.GET("/api/settings", pm(h.GetSettings, "settings:get"))
|
||||
api.PUT("/api/settings", pm(h.UpdateSettings, "settings:manage"))
|
||||
api.POST("/api/settings/smtp/test", pm(h.TestSMTPSettings, "settings:manage"))
|
||||
api.POST("/api/admin/reload", pm(h.ReloadApp, "settings:manage"))
|
||||
api.GET("/api/logs", pm(h.GetLogs, "settings:get"))
|
||||
api.GET("/api/events", pm(h.EventStream, "settings:get"))
|
||||
api.GET("/api/about", h.GetAboutInfo)
|
||||
|
||||
api.GET("/api/subscribers", pm(handleQuerySubscribers, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id", pm(handleGetSubscriber, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id/export", pm(handleExportSubscriberData, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id/bounces", pm(handleGetSubscriberBounces, "bounces:get"))
|
||||
api.DELETE("/api/subscribers/:id/bounces", pm(handleDeleteSubscriberBounces, "bounces:manage"))
|
||||
api.POST("/api/subscribers", pm(handleCreateSubscriber, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/:id", pm(handleUpdateSubscriber, "subscribers:manage"))
|
||||
api.POST("/api/subscribers/:id/optin", pm(handleSubscriberSendOptin, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/blocklist", pm(handleBlocklistSubscribers, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/:id/blocklist", pm(handleBlocklistSubscribers, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/lists/:id", pm(handleManageSubscriberLists, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/lists", pm(handleManageSubscriberLists, "subscribers:manage"))
|
||||
api.DELETE("/api/subscribers/:id", pm(handleDeleteSubscribers, "subscribers:manage"))
|
||||
api.DELETE("/api/subscribers", pm(handleDeleteSubscribers, "subscribers:manage"))
|
||||
api.GET("/api/subscribers", pm(h.QuerySubscribers, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id", pm(h.GetSubscriber, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id/export", pm(h.ExportSubscriberData, "subscribers:get_all", "subscribers:get"))
|
||||
api.GET("/api/subscribers/:id/bounces", pm(h.GetSubscriberBounces, "bounces:get"))
|
||||
api.DELETE("/api/subscribers/:id/bounces", pm(h.DeleteSubscriberBounces, "bounces:manage"))
|
||||
api.POST("/api/subscribers", pm(h.CreateSubscriber, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/:id", pm(h.UpdateSubscriber, "subscribers:manage"))
|
||||
api.POST("/api/subscribers/:id/optin", pm(h.SubscriberSendOptin, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/blocklist", pm(h.BlocklistSubscribers, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/:id/blocklist", pm(h.BlocklistSubscribers, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/lists/:id", pm(h.ManageSubscriberLists, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/lists", pm(h.ManageSubscriberLists, "subscribers:manage"))
|
||||
api.DELETE("/api/subscribers/:id", pm(h.DeleteSubscribers, "subscribers:manage"))
|
||||
api.DELETE("/api/subscribers", pm(h.DeleteSubscribers, "subscribers:manage"))
|
||||
|
||||
api.GET("/api/bounces", pm(handleGetBounces, "bounces:get"))
|
||||
api.GET("/api/bounces/:id", pm(handleGetBounces, "bounces:get"))
|
||||
api.DELETE("/api/bounces", pm(handleDeleteBounces, "bounces:manage"))
|
||||
api.DELETE("/api/bounces/:id", pm(handleDeleteBounces, "bounces:manage"))
|
||||
api.GET("/api/bounces", pm(h.GetBounces, "bounces:get"))
|
||||
api.GET("/api/bounces/:id", pm(h.GetBounces, "bounces:get"))
|
||||
api.DELETE("/api/bounces", pm(h.DeleteBounces, "bounces:manage"))
|
||||
api.DELETE("/api/bounces/:id", pm(h.DeleteBounces, "bounces:manage"))
|
||||
|
||||
// Subscriber operations based on arbitrary SQL queries.
|
||||
// These aren't very REST-like.
|
||||
api.POST("/api/subscribers/query/delete", pm(handleDeleteSubscribersByQuery, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/query/blocklist", pm(handleBlocklistSubscribersByQuery, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/query/lists", pm(handleManageSubscriberListsByQuery, "subscribers:manage"))
|
||||
api.POST("/api/subscribers/query/delete", pm(h.DeleteSubscribersByQuery, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/query/blocklist", pm(h.BlocklistSubscribersByQuery, "subscribers:manage"))
|
||||
api.PUT("/api/subscribers/query/lists", pm(h.ManageSubscriberListsByQuery, "subscribers:manage"))
|
||||
api.GET("/api/subscribers/export",
|
||||
pm(middleware.GzipWithConfig(middleware.GzipConfig{Level: 9})(handleExportSubscribers), "subscribers:get_all", "subscribers:get"))
|
||||
pm(middleware.GzipWithConfig(middleware.GzipConfig{Level: 9})(h.ExportSubscribers), "subscribers:get_all", "subscribers:get"))
|
||||
|
||||
api.GET("/api/import/subscribers", pm(handleGetImportSubscribers, "subscribers:import"))
|
||||
api.GET("/api/import/subscribers/logs", pm(handleGetImportSubscriberStats, "subscribers:import"))
|
||||
api.POST("/api/import/subscribers", pm(handleImportSubscribers, "subscribers:import"))
|
||||
api.DELETE("/api/import/subscribers", pm(handleStopImportSubscribers, "subscribers:import"))
|
||||
api.GET("/api/import/subscribers", pm(h.GetImportSubscribers, "subscribers:import"))
|
||||
api.GET("/api/import/subscribers/logs", pm(h.GetImportSubscriberStats, "subscribers:import"))
|
||||
api.POST("/api/import/subscribers", pm(h.ImportSubscribers, "subscribers:import"))
|
||||
api.DELETE("/api/import/subscribers", pm(h.StopImportSubscribers, "subscribers:import"))
|
||||
|
||||
// Individual list permissions are applied directly within handleGetLists.
|
||||
api.GET("/api/lists", handleGetLists)
|
||||
api.GET("/api/lists/:id", handleGetList)
|
||||
api.POST("/api/lists", pm(handleCreateList, "lists:manage_all"))
|
||||
api.PUT("/api/lists/:id", handleUpdateList)
|
||||
api.DELETE("/api/lists/:id", handleDeleteLists)
|
||||
api.GET("/api/lists", h.GetLists)
|
||||
api.GET("/api/lists/:id", h.GetList)
|
||||
api.POST("/api/lists", pm(h.CreateList, "lists:manage_all"))
|
||||
api.PUT("/api/lists/:id", h.UpdateList)
|
||||
api.DELETE("/api/lists/:id", h.DeleteLists)
|
||||
|
||||
api.GET("/api/campaigns", pm(handleGetCampaigns, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/running/stats", pm(handleGetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/:id", pm(handleGetCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/analytics/:type", pm(handleGetCampaignViewAnalytics, "campaigns:get_analytics"))
|
||||
api.GET("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/content", pm(handleCampaignContent, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.POST("/api/campaigns/:id/text", pm(handlePreviewCampaign, "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/test", pm(handleTestCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.POST("/api/campaigns", pm(handleCreateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id", pm(handleUpdateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id/status", pm(handleUpdateCampaignStatus, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id/archive", pm(handleUpdateCampaignArchive, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.DELETE("/api/campaigns/:id", pm(handleDeleteCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.GET("/api/campaigns", pm(h.GetCampaigns, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/running/stats", pm(h.GetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/:id", pm(h.GetCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.GET("/api/campaigns/analytics/:type", pm(h.GetCampaignViewAnalytics, "campaigns:get_analytics"))
|
||||
api.GET("/api/campaigns/:id/preview", pm(h.PreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/preview", pm(h.PreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/content", pm(h.CampaignContent, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.POST("/api/campaigns/:id/text", pm(h.PreviewCampaign, "campaigns:get"))
|
||||
api.POST("/api/campaigns/:id/test", pm(h.TestCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.POST("/api/campaigns", pm(h.CreateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id", pm(h.UpdateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id/status", pm(h.UpdateCampaignStatus, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.PUT("/api/campaigns/:id/archive", pm(h.UpdateCampaignArchive, "campaigns:manage_all", "campaigns:manage"))
|
||||
api.DELETE("/api/campaigns/:id", pm(h.DeleteCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
|
||||
api.GET("/api/media", pm(handleGetMedia, "media:get"))
|
||||
api.GET("/api/media/:id", pm(handleGetMedia, "media:get"))
|
||||
api.POST("/api/media", pm(handleUploadMedia, "media:manage"))
|
||||
api.DELETE("/api/media/:id", pm(handleDeleteMedia, "media:manage"))
|
||||
api.GET("/api/media", pm(h.GetMedia, "media:get"))
|
||||
api.GET("/api/media/:id", pm(h.GetMedia, "media:get"))
|
||||
api.POST("/api/media", pm(h.UploadMedia, "media:manage"))
|
||||
api.DELETE("/api/media/:id", pm(h.DeleteMedia, "media:manage"))
|
||||
|
||||
api.GET("/api/templates", pm(handleGetTemplates, "templates:get"))
|
||||
api.GET("/api/templates/:id", pm(handleGetTemplates, "templates:get"))
|
||||
api.GET("/api/templates/:id/preview", pm(handlePreviewTemplate, "templates:get"))
|
||||
api.POST("/api/templates/preview", pm(handlePreviewTemplate, "templates:get"))
|
||||
api.POST("/api/templates", pm(handleCreateTemplate, "templates:manage"))
|
||||
api.PUT("/api/templates/:id", pm(handleUpdateTemplate, "templates:manage"))
|
||||
api.PUT("/api/templates/:id/default", pm(handleTemplateSetDefault, "templates:manage"))
|
||||
api.DELETE("/api/templates/:id", pm(handleDeleteTemplate, "templates:manage"))
|
||||
api.GET("/api/templates", pm(h.GetTemplates, "templates:get"))
|
||||
api.GET("/api/templates/:id", pm(h.GetTemplates, "templates:get"))
|
||||
api.GET("/api/templates/:id/preview", pm(h.PreviewTemplate, "templates:get"))
|
||||
api.POST("/api/templates/preview", pm(h.PreviewTemplate, "templates:get"))
|
||||
api.POST("/api/templates", pm(h.CreateTemplate, "templates:manage"))
|
||||
api.PUT("/api/templates/:id", pm(h.UpdateTemplate, "templates:manage"))
|
||||
api.PUT("/api/templates/:id/default", pm(h.TemplateSetDefault, "templates:manage"))
|
||||
api.DELETE("/api/templates/:id", pm(h.DeleteTemplate, "templates:manage"))
|
||||
|
||||
api.DELETE("/api/maintenance/subscribers/:type", pm(handleGCSubscribers, "settings:maintain"))
|
||||
api.DELETE("/api/maintenance/analytics/:type", pm(handleGCCampaignAnalytics, "settings:maintain"))
|
||||
api.DELETE("/api/maintenance/subscriptions/unconfirmed", pm(handleGCSubscriptions, "settings:maintain"))
|
||||
api.DELETE("/api/maintenance/subscribers/:type", pm(h.GCSubscribers, "settings:maintain"))
|
||||
api.DELETE("/api/maintenance/analytics/:type", pm(h.GCCampaignAnalytics, "settings:maintain"))
|
||||
api.DELETE("/api/maintenance/subscriptions/unconfirmed", pm(h.GCSubscriptions, "settings:maintain"))
|
||||
|
||||
api.POST("/api/tx", pm(handleSendTxMessage, "tx:send"))
|
||||
api.POST("/api/tx", pm(h.SendTxMessage, "tx:send"))
|
||||
|
||||
api.GET("/api/profile", handleGetUserProfile)
|
||||
api.PUT("/api/profile", handleUpdateUserProfile)
|
||||
api.GET("/api/users", pm(handleGetUsers, "users:get"))
|
||||
api.GET("/api/users/:id", pm(handleGetUsers, "users:get"))
|
||||
api.POST("/api/users", pm(handleCreateUser, "users:manage"))
|
||||
api.PUT("/api/users/:id", pm(handleUpdateUser, "users:manage"))
|
||||
api.DELETE("/api/users", pm(handleDeleteUsers, "users:manage"))
|
||||
api.DELETE("/api/users/:id", pm(handleDeleteUsers, "users:manage"))
|
||||
api.POST("/api/logout", handleLogout)
|
||||
api.GET("/api/profile", h.GetUserProfile)
|
||||
api.PUT("/api/profile", h.UpdateUserProfile)
|
||||
api.GET("/api/users", pm(h.GetUsers, "users:get"))
|
||||
api.GET("/api/users/:id", pm(h.GetUsers, "users:get"))
|
||||
api.POST("/api/users", pm(h.CreateUser, "users:manage"))
|
||||
api.PUT("/api/users/:id", pm(h.UpdateUser, "users:manage"))
|
||||
api.DELETE("/api/users", pm(h.DeleteUsers, "users:manage"))
|
||||
api.DELETE("/api/users/:id", pm(h.DeleteUsers, "users:manage"))
|
||||
api.POST("/api/logout", h.Logout)
|
||||
|
||||
api.GET("/api/roles/users", pm(handleGetUserRoles, "roles:get"))
|
||||
api.GET("/api/roles/lists", pm(handleGeListRoles, "roles:get"))
|
||||
api.POST("/api/roles/users", pm(handleCreateUserRole, "roles:manage"))
|
||||
api.POST("/api/roles/lists", pm(handleCreateListRole, "roles:manage"))
|
||||
api.PUT("/api/roles/users/:id", pm(handleUpdateUserRole, "roles:manage"))
|
||||
api.PUT("/api/roles/lists/:id", pm(handleUpdateListRole, "roles:manage"))
|
||||
api.DELETE("/api/roles/:id", pm(handleDeleteRole, "roles:manage"))
|
||||
api.GET("/api/roles/users", pm(h.GetUserRoles, "roles:get"))
|
||||
api.GET("/api/roles/lists", pm(h.GeListRoles, "roles:get"))
|
||||
api.POST("/api/roles/users", pm(h.CreateUserRole, "roles:manage"))
|
||||
api.POST("/api/roles/lists", pm(h.CreateListRole, "roles:manage"))
|
||||
api.PUT("/api/roles/users/:id", pm(h.UpdateUserRole, "roles:manage"))
|
||||
api.PUT("/api/roles/lists/:id", pm(h.UpdateListRole, "roles:manage"))
|
||||
api.DELETE("/api/roles/:id", pm(h.DeleteRole, "roles:manage"))
|
||||
|
||||
if app.constants.BounceWebhooksEnabled {
|
||||
// Private authenticated bounce endpoint.
|
||||
api.POST("/webhooks/bounce", pm(handleBounceWebhook, "webhooks:post_bounce"))
|
||||
api.POST("/webhooks/bounce", pm(h.BounceWebhook, "webhooks:post_bounce"))
|
||||
|
||||
// Public bounce endpoints for webservices like SES.
|
||||
p.POST("/webhooks/service/:service", handleBounceWebhook)
|
||||
p.POST("/webhooks/service/:service", h.BounceWebhook)
|
||||
}
|
||||
|
||||
// =================================================================
|
||||
@@ -209,54 +217,54 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||
})
|
||||
|
||||
// Public admin endpoints (login page, OIDC endpoints).
|
||||
p.GET(path.Join(uriAdmin, "/login"), handleLoginPage)
|
||||
p.POST(path.Join(uriAdmin, "/login"), handleLoginPage)
|
||||
p.GET(path.Join(uriAdmin, "/login"), h.LoginPage)
|
||||
p.POST(path.Join(uriAdmin, "/login"), h.LoginPage)
|
||||
|
||||
if app.constants.Security.OIDC.Enabled {
|
||||
p.POST("/auth/oidc", handleOIDCLogin)
|
||||
p.GET("/auth/oidc", handleOIDCFinish)
|
||||
p.POST("/auth/oidc", h.OIDCLogin)
|
||||
p.GET("/auth/oidc", h.OIDCFinish)
|
||||
}
|
||||
|
||||
// Public APIs.
|
||||
p.GET("/api/public/lists", handleGetPublicLists)
|
||||
p.POST("/api/public/subscription", handlePublicSubscription)
|
||||
p.GET("/api/public/lists", h.GetPublicLists)
|
||||
p.POST("/api/public/subscription", h.PublicSubscription)
|
||||
if app.constants.EnablePublicArchive {
|
||||
p.GET("/api/public/archive", handleGetCampaignArchives)
|
||||
p.GET("/api/public/archive", h.GetCampaignArchives)
|
||||
}
|
||||
|
||||
// /public/static/* file server is registered in initHTTPServer().
|
||||
// Public subscriber facing views.
|
||||
p.GET("/subscription/form", handleSubscriptionFormPage)
|
||||
p.POST("/subscription/form", handleSubscriptionForm)
|
||||
p.GET("/subscription/:campUUID/:subUUID", noIndex(validateUUID(subscriberExists(handleSubscriptionPage),
|
||||
p.GET("/subscription/form", h.SubscriptionFormPage)
|
||||
p.POST("/subscription/form", h.SubscriptionForm)
|
||||
p.GET("/subscription/:campUUID/:subUUID", noIndex(validateUUID(subscriberExists(h.SubscriptionPage),
|
||||
"campUUID", "subUUID")))
|
||||
p.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPrefs),
|
||||
p.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(h.SubscriptionPrefs),
|
||||
"campUUID", "subUUID"))
|
||||
p.GET("/subscription/optin/:subUUID", noIndex(validateUUID(subscriberExists(handleOptinPage), "subUUID")))
|
||||
p.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(handleOptinPage), "subUUID"))
|
||||
p.POST("/subscription/export/:subUUID", validateUUID(subscriberExists(handleSelfExportSubscriberData),
|
||||
p.GET("/subscription/optin/:subUUID", noIndex(validateUUID(subscriberExists(h.OptinPage), "subUUID")))
|
||||
p.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(h.OptinPage), "subUUID"))
|
||||
p.POST("/subscription/export/:subUUID", validateUUID(subscriberExists(h.SelfExportSubscriberData),
|
||||
"subUUID"))
|
||||
p.POST("/subscription/wipe/:subUUID", validateUUID(subscriberExists(handleWipeSubscriberData),
|
||||
p.POST("/subscription/wipe/:subUUID", validateUUID(subscriberExists(h.WipeSubscriberData),
|
||||
"subUUID"))
|
||||
p.GET("/link/:linkUUID/:campUUID/:subUUID", noIndex(validateUUID(handleLinkRedirect,
|
||||
p.GET("/link/:linkUUID/:campUUID/:subUUID", noIndex(validateUUID(h.LinkRedirect,
|
||||
"linkUUID", "campUUID", "subUUID")))
|
||||
p.GET("/campaign/:campUUID/:subUUID", noIndex(validateUUID(handleViewCampaignMessage,
|
||||
p.GET("/campaign/:campUUID/:subUUID", noIndex(validateUUID(h.ViewCampaignMessage,
|
||||
"campUUID", "subUUID")))
|
||||
p.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
|
||||
p.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(h.RegisterCampaignView,
|
||||
"campUUID", "subUUID")))
|
||||
|
||||
if app.constants.EnablePublicArchive {
|
||||
p.GET("/archive", handleCampaignArchivesPage)
|
||||
p.GET("/archive.xml", handleGetCampaignArchivesFeed)
|
||||
p.GET("/archive/:id", handleCampaignArchivePage)
|
||||
p.GET("/archive/latest", handleCampaignArchivePageLatest)
|
||||
p.GET("/archive", h.CampaignArchivesPage)
|
||||
p.GET("/archive.xml", h.GetCampaignArchivesFeed)
|
||||
p.GET("/archive/:id", h.CampaignArchivePage)
|
||||
p.GET("/archive/latest", h.CampaignArchivePageLatest)
|
||||
}
|
||||
|
||||
p.GET("/public/custom.css", serveCustomAppearance("public.custom_css"))
|
||||
p.GET("/public/custom.js", serveCustomAppearance("public.custom_js"))
|
||||
|
||||
// Public health API endpoint.
|
||||
p.GET("/health", handleHealthCheck)
|
||||
p.GET("/health", h.HealthCheck)
|
||||
|
||||
// 404 pages.
|
||||
p.RouteNotFound("/*", func(c echo.Context) error {
|
||||
@@ -271,22 +279,20 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleAdminPage is the root handler that renders the Javascript admin frontend.
|
||||
func handleAdminPage(c echo.Context) error {
|
||||
app := c.Get("app").(*App)
|
||||
|
||||
b, err := app.fs.Read(path.Join(uriAdmin, "/index.html"))
|
||||
// AdminPage is the root handler that renders the Javascript admin frontend.
|
||||
func (h *Handlers) AdminPage(c echo.Context) error {
|
||||
b, err := h.app.fs.Read(path.Join(uriAdmin, "/index.html"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
b = bytes.ReplaceAll(b, []byte("asset_version"), []byte(app.constants.AssetVersion))
|
||||
b = bytes.ReplaceAll(b, []byte("asset_version"), []byte(h.app.constants.AssetVersion))
|
||||
|
||||
return c.HTMLBlob(http.StatusOK, b)
|
||||
}
|
||||
|
||||
// handleHealthCheck is a healthcheck endpoint that returns a 200 response.
|
||||
func handleHealthCheck(c echo.Context) error {
|
||||
// HealthCheck is a healthcheck endpoint that returns a 200 response.
|
||||
func (h *Handlers) HealthCheck(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
|
||||
10
cmd/i18n.go
10
cmd/i18n.go
@@ -24,18 +24,14 @@ type i18nLangRaw struct {
|
||||
|
||||
var reLangCode = regexp.MustCompile(`[^a-zA-Z_0-9\\-]`)
|
||||
|
||||
// handleGetI18nLang returns the JSON language pack given the language code.
|
||||
func handleGetI18nLang(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetI18nLang returns the JSON language pack given the language code.
|
||||
func (h *Handlers) GetI18nLang(c echo.Context) error {
|
||||
lang := c.Param("lang")
|
||||
if len(lang) > 6 || reLangCode.MatchString(lang) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid language code.")
|
||||
}
|
||||
|
||||
i, ok, err := getI18nLang(lang, app.fs)
|
||||
i, ok, err := getI18nLang(lang, h.app.fs)
|
||||
if err != nil && !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Unknown language.")
|
||||
}
|
||||
|
||||
@@ -12,28 +12,24 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleImportSubscribers handles the uploading and bulk importing of
|
||||
// ImportSubscribers handles the uploading and bulk importing of
|
||||
// a ZIP file of one or more CSV files.
|
||||
func handleImportSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) ImportSubscribers(c echo.Context) error {
|
||||
// Is an import already running?
|
||||
if app.importer.GetStats().Status == subimporter.StatusImporting {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.alreadyRunning"))
|
||||
if h.app.importer.GetStats().Status == subimporter.StatusImporting {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("import.alreadyRunning"))
|
||||
}
|
||||
|
||||
// Unmarshal the JSON params.
|
||||
var opt subimporter.SessionOpt
|
||||
if err := json.Unmarshal([]byte(c.FormValue("params")), &opt); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("import.invalidParams", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.invalidParams", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Validate mode.
|
||||
if opt.Mode != subimporter.ModeSubscribe && opt.Mode != subimporter.ModeBlocklist {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidMode"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("import.invalidMode"))
|
||||
}
|
||||
|
||||
// If no status is specified, pick a default one.
|
||||
@@ -49,18 +45,18 @@ func handleImportSubscribers(c echo.Context) error {
|
||||
if opt.SubStatus != models.SubscriptionStatusUnconfirmed &&
|
||||
opt.SubStatus != models.SubscriptionStatusConfirmed &&
|
||||
opt.SubStatus != models.SubscriptionStatusUnsubscribed {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidSubStatus"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("import.invalidSubStatus"))
|
||||
}
|
||||
|
||||
if len(opt.Delim) != 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("import.invalidDelim"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("import.invalidDelim"))
|
||||
}
|
||||
|
||||
// Open the HTTP file.
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("import.invalidFile", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.invalidFile", "error", err.Error()))
|
||||
}
|
||||
|
||||
src, err := file.Open()
|
||||
@@ -73,21 +69,21 @@ func handleImportSubscribers(c echo.Context) error {
|
||||
out, err := os.CreateTemp("", "listmonk")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("import.errorCopyingFile", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.errorCopyingFile", "error", err.Error()))
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err = io.Copy(out, src); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("import.errorCopyingFile", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.errorCopyingFile", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Start the importer session.
|
||||
opt.Filename = file.Filename
|
||||
sess, err := app.importer.NewSession(opt)
|
||||
sess, err := h.app.importer.NewSession(opt)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("import.errorStarting", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.errorStarting", "error", err.Error()))
|
||||
}
|
||||
go sess.Start()
|
||||
|
||||
@@ -103,42 +99,30 @@ func handleImportSubscribers(c echo.Context) error {
|
||||
dir, files, err := sess.ExtractZIP(out.Name(), 1)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("import.errorProcessingZIP", "error", err.Error()))
|
||||
h.app.i18n.Ts("import.errorProcessingZIP", "error", err.Error()))
|
||||
}
|
||||
|
||||
go sess.LoadCSV(dir+"/"+files[0], rune(opt.Delim[0]))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
|
||||
return c.JSON(http.StatusOK, okResp{h.app.importer.GetStats()})
|
||||
}
|
||||
|
||||
// handleGetImportSubscribers returns import statistics.
|
||||
func handleGetImportSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
s := app.importer.GetStats()
|
||||
// GetImportSubscribers returns import statistics.
|
||||
func (h *Handlers) GetImportSubscribers(c echo.Context) error {
|
||||
s := h.app.importer.GetStats()
|
||||
return c.JSON(http.StatusOK, okResp{s})
|
||||
}
|
||||
|
||||
// handleGetImportSubscriberStats returns import statistics.
|
||||
func handleGetImportSubscriberStats(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{string(app.importer.GetLogs())})
|
||||
// GetImportSubscriberStats returns import statistics.
|
||||
func (h *Handlers) GetImportSubscriberStats(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{string(h.app.importer.GetLogs())})
|
||||
}
|
||||
|
||||
// handleStopImportSubscribers sends a stop signal to the importer.
|
||||
// StopImportSubscribers sends a stop signal to the importer.
|
||||
// If there's an ongoing import, it'll be stopped, and if an import
|
||||
// is finished, it's state is cleared.
|
||||
func handleStopImportSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
app.importer.Stop()
|
||||
return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
|
||||
func (h *Handlers) StopImportSubscribers(c echo.Context) error {
|
||||
h.app.importer.Stop()
|
||||
return c.JSON(http.StatusOK, okResp{h.app.importer.GetStats()})
|
||||
}
|
||||
|
||||
12
cmd/init.go
12
cmd/init.go
@@ -488,9 +488,9 @@ func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
|
||||
}
|
||||
|
||||
// initCampaignManager initializes the campaign manager.
|
||||
func initCampaignManager(q *models.Queries, cs *constants, app *App) *manager.Manager {
|
||||
func initCampaignManager(cb notifCB, q *models.Queries, cs *constants, co *core.Core, md media.Store, i *i18n.I18n) *manager.Manager {
|
||||
campNotifCB := func(subject string, data any) error {
|
||||
return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data, nil)
|
||||
return cb(cs.NotifyEmails, subject, notifTplCampaign, data, nil)
|
||||
}
|
||||
|
||||
if ko.Bool("passive") {
|
||||
@@ -517,19 +517,19 @@ func initCampaignManager(q *models.Queries, cs *constants, app *App) *manager.Ma
|
||||
SlidingWindowRate: ko.Int("app.message_sliding_window_rate"),
|
||||
ScanInterval: time.Second * 5,
|
||||
ScanCampaigns: !ko.Bool("passive"),
|
||||
}, newManagerStore(q, app.core, app.media), campNotifCB, app.i18n, lo)
|
||||
}, newManagerStore(q, co, md), campNotifCB, i, lo)
|
||||
}
|
||||
|
||||
// initTxTemplates initializes and compiles the transactional templates and caches them in-memory.
|
||||
func initTxTemplates(m *manager.Manager, app *App) {
|
||||
tpls, err := app.core.GetTemplates(models.TemplateTypeTx, false)
|
||||
func initTxTemplates(m *manager.Manager, co *core.Core) {
|
||||
tpls, err := co.GetTemplates(models.TemplateTypeTx, false)
|
||||
if err != nil {
|
||||
lo.Fatalf("error loading transactional templates: %v", err)
|
||||
}
|
||||
|
||||
for _, t := range tpls {
|
||||
tpl := t
|
||||
if err := tpl.Compile(app.manager.GenericTemplateFuncs()); err != nil {
|
||||
if err := tpl.Compile(m.GenericTemplateFuncs()); err != nil {
|
||||
lo.Printf("error compiling transactional template %d: %v", tpl.ID, err)
|
||||
continue
|
||||
}
|
||||
|
||||
67
cmd/lists.go
67
cmd/lists.go
@@ -10,12 +10,11 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleGetLists retrieves lists with additional metadata like subscriber counts.
|
||||
func handleGetLists(c echo.Context) error {
|
||||
// GetLists retrieves lists with additional metadata like subscriber counts.
|
||||
func (h *Handlers) GetLists(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
|
||||
// Get the list IDs (or blanket permission) the user has access to.
|
||||
@@ -24,7 +23,7 @@ func handleGetLists(c echo.Context) error {
|
||||
// Minimal query simply returns the list of all lists without JOIN subscriber counts. This is fast.
|
||||
minimal, _ := strconv.ParseBool(c.FormValue("minimal"))
|
||||
if minimal {
|
||||
res, err := app.core.GetLists("", hasAllPerm, permittedIDs)
|
||||
res, err := h.app.core.GetLists("", hasAllPerm, permittedIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,7 +52,7 @@ func handleGetLists(c echo.Context) error {
|
||||
optin = c.FormValue("optin")
|
||||
order = c.FormValue("order")
|
||||
)
|
||||
res, total, err := app.core.QueryLists(query, typ, optin, tags, orderBy, order, hasAllPerm, permittedIDs, pg.Offset, pg.Limit)
|
||||
res, total, err := h.app.core.QueryLists(query, typ, optin, tags, orderBy, order, hasAllPerm, permittedIDs, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -69,17 +68,15 @@ func handleGetLists(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGetList retrieves a single list by id.
|
||||
// GetList retrieves a single list by id.
|
||||
// It's permission checked by the listPerm middleware.
|
||||
func handleGetList(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) GetList(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the list.
|
||||
@@ -88,7 +85,7 @@ func handleGetList(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the list from the DB.
|
||||
out, err := app.core.GetList(id, "")
|
||||
out, err := h.app.core.GetList(id, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,12 +93,8 @@ func handleGetList(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleCreateList handles list creation.
|
||||
func handleCreateList(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CreateList handles list creation.
|
||||
func (h *Handlers) CreateList(c echo.Context) error {
|
||||
l := models.List{}
|
||||
if err := c.Bind(&l); err != nil {
|
||||
return err
|
||||
@@ -109,10 +102,10 @@ func handleCreateList(c echo.Context) error {
|
||||
|
||||
// Validate.
|
||||
if !strHasLen(l.Name, 1, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("lists.invalidName"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("lists.invalidName"))
|
||||
}
|
||||
|
||||
out, err := app.core.CreateList(l)
|
||||
out, err := h.app.core.CreateList(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -120,17 +113,15 @@ func handleCreateList(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateList handles list modification.
|
||||
// UpdateList handles list modification.
|
||||
// It's permission checked by the listPerm middleware.
|
||||
func handleUpdateList(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) UpdateList(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the list.
|
||||
@@ -146,11 +137,11 @@ func handleUpdateList(c echo.Context) error {
|
||||
|
||||
// Validate.
|
||||
if !strHasLen(l.Name, 1, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("lists.invalidName"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("lists.invalidName"))
|
||||
}
|
||||
|
||||
// Update the list in the DB.
|
||||
out, err := app.core.UpdateList(id, l)
|
||||
out, err := h.app.core.UpdateList(id, l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -158,20 +149,18 @@ func handleUpdateList(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleDeleteLists handles list deletion, either a single one (ID in the URI), or a list.
|
||||
// DeleteLists handles list deletion, either a single one (ID in the URI), or a list.
|
||||
// It's permission checked by the listPerm middleware.
|
||||
func handleDeleteLists(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) DeleteLists(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var (
|
||||
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
ids []int
|
||||
)
|
||||
if id < 1 && len(ids) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
if id > 0 {
|
||||
@@ -184,7 +173,7 @@ func handleDeleteLists(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Delete the lists from the DB.
|
||||
if err := app.core.DeleteLists(ids); err != nil {
|
||||
if err := h.app.core.DeleteLists(ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,7 @@ const (
|
||||
emailMsgr = "email"
|
||||
)
|
||||
|
||||
// App contains the "global" components that are
|
||||
// passed around, especially through HTTP handlers.
|
||||
// App contains the "global" shared components, controllers and fields.
|
||||
type App struct {
|
||||
core *core.Core
|
||||
fs stuffbin.FileSystem
|
||||
@@ -216,7 +215,7 @@ func main() {
|
||||
})
|
||||
|
||||
app.queries = queries
|
||||
app.manager = initCampaignManager(app.queries, app.constants, app)
|
||||
app.manager = initCampaignManager(app.sendNotification, app.queries, app.constants, app.core, app.media, app.i18n)
|
||||
app.importer = initImporter(app.queries, db, app.core, app)
|
||||
|
||||
hasUsers, auth := initAuth(app.core, db.DB, ko)
|
||||
@@ -228,7 +227,7 @@ func main() {
|
||||
|
||||
// Initialize admin email notification templates.
|
||||
app.notifTpls = initNotifTemplates(fs, app.i18n, app.constants)
|
||||
initTxTemplates(app.manager, app)
|
||||
initTxTemplates(app.manager, app.core)
|
||||
|
||||
// Initialize the bounce manager that processes bounces from webhooks and
|
||||
// POP3 mailbox scanning.
|
||||
@@ -270,7 +269,7 @@ func main() {
|
||||
|
||||
// Star the update checker.
|
||||
if ko.Bool("app.check_updates") {
|
||||
go checkUpdates(versionString, time.Hour*24, app)
|
||||
go app.checkUpdates(versionString, time.Hour*24)
|
||||
}
|
||||
|
||||
// Wait for the reload signal with a callback to gracefully shut down resources.
|
||||
|
||||
@@ -7,12 +7,8 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleGCSubscribers garbage collects (deletes) orphaned or blocklisted subscribers.
|
||||
func handleGCSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GCSubscribers garbage collects (deletes) orphaned or blocklisted subscribers.
|
||||
func (h *Handlers) GCSubscribers(c echo.Context) error {
|
||||
var (
|
||||
typ = c.Param("type")
|
||||
|
||||
@@ -22,11 +18,11 @@ func handleGCSubscribers(c echo.Context) error {
|
||||
|
||||
switch typ {
|
||||
case "blocklisted":
|
||||
n, err = app.core.DeleteBlocklistedSubscribers()
|
||||
n, err = h.app.core.DeleteBlocklistedSubscribers()
|
||||
case "orphan":
|
||||
n, err = app.core.DeleteOrphanSubscribers()
|
||||
n, err = h.app.core.DeleteOrphanSubscribers()
|
||||
default:
|
||||
err = echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
err = echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -38,20 +34,16 @@ func handleGCSubscribers(c echo.Context) error {
|
||||
}{n}})
|
||||
}
|
||||
|
||||
// handleGCSubscriptions garbage collects (deletes) orphaned or blocklisted subscribers.
|
||||
func handleGCSubscriptions(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GCSubscriptions garbage collects (deletes) orphaned or blocklisted subscribers.
|
||||
func (h *Handlers) GCSubscriptions(c echo.Context) error {
|
||||
// Validate the date.
|
||||
t, err := time.Parse(time.RFC3339, c.FormValue("before_date"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
// Delete unconfirmed subscriptions from the DB in bulk.
|
||||
n, err := app.core.DeleteUnconfirmedSubscriptions(t)
|
||||
n, err := h.app.core.DeleteUnconfirmedSubscriptions(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -61,30 +53,26 @@ func handleGCSubscriptions(c echo.Context) error {
|
||||
}{n}})
|
||||
}
|
||||
|
||||
// handleGCCampaignAnalytics garbage collects (deletes) campaign analytics.
|
||||
func handleGCCampaignAnalytics(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
typ = c.Param("type")
|
||||
)
|
||||
// GCCampaignAnalytics garbage collects (deletes) campaign analytics.
|
||||
func (h *Handlers) GCCampaignAnalytics(c echo.Context) error {
|
||||
|
||||
t, err := time.Parse(time.RFC3339, c.FormValue("before_date"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
switch typ {
|
||||
switch c.Param("type") {
|
||||
case "all":
|
||||
if err := app.core.DeleteCampaignViews(t); err != nil {
|
||||
if err := h.app.core.DeleteCampaignViews(t); err != nil {
|
||||
return err
|
||||
}
|
||||
err = app.core.DeleteCampaignLinkClicks(t)
|
||||
err = h.app.core.DeleteCampaignLinkClicks(t)
|
||||
case "views":
|
||||
err = app.core.DeleteCampaignViews(t)
|
||||
err = h.app.core.DeleteCampaignViews(t)
|
||||
case "clicks":
|
||||
err = app.core.DeleteCampaignLinkClicks(t)
|
||||
err = h.app.core.DeleteCampaignLinkClicks(t)
|
||||
default:
|
||||
err = echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
|
||||
err = echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidData"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
82
cmd/media.go
82
cmd/media.go
@@ -23,23 +23,19 @@ var (
|
||||
imageExts = []string{"gif", "png", "jpg", "jpeg"}
|
||||
)
|
||||
|
||||
// handleUploadMedia handles media file uploads.
|
||||
func handleUploadMedia(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UploadMedia handles media file uploads.
|
||||
func (h *Handlers) UploadMedia(c echo.Context) error {
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("media.invalidFile", "error", err.Error()))
|
||||
h.app.i18n.Ts("media.invalidFile", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Read the file from the HTTP form.
|
||||
src, err := file.Open()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("media.errorReadingFile", "error", err.Error()))
|
||||
h.app.i18n.Ts("media.errorReadingFile", "error", err.Error()))
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
@@ -50,10 +46,10 @@ func handleUploadMedia(c echo.Context) error {
|
||||
)
|
||||
|
||||
// Validate file extension.
|
||||
if !inArray("*", app.constants.MediaUpload.Extensions) {
|
||||
if ok := inArray(ext, app.constants.MediaUpload.Extensions); !ok {
|
||||
if !inArray("*", h.app.constants.MediaUpload.Extensions) {
|
||||
if ok := inArray(ext, h.app.constants.MediaUpload.Extensions); !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("media.unsupportedFileType", "type", ext))
|
||||
h.app.i18n.Ts("media.unsupportedFileType", "type", ext))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,22 +57,22 @@ func handleUploadMedia(c echo.Context) error {
|
||||
fName := makeFilename(file.Filename)
|
||||
|
||||
// If the filename already exists in the DB, make it unique by adding a random suffix.
|
||||
if _, err := app.core.GetMedia(0, "", fName, app.media); err == nil {
|
||||
if _, err := h.app.core.GetMedia(0, "", fName, h.app.media); err == nil {
|
||||
suffix, err := generateRandomString(6)
|
||||
if err != nil {
|
||||
app.log.Printf("error generating random string: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("globals.messages.internalError"))
|
||||
h.app.log.Printf("error generating random string: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
fName = appendSuffixToFilename(fName, suffix)
|
||||
}
|
||||
|
||||
// Upload the file to the media store.
|
||||
fName, err = app.media.Put(fName, contentType, src)
|
||||
fName, err = h.app.media.Put(fName, contentType, src)
|
||||
if err != nil {
|
||||
app.log.Printf("error uploading file: %v", err)
|
||||
h.app.log.Printf("error uploading file: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("media.errorUploading", "error", err.Error()))
|
||||
h.app.i18n.Ts("media.errorUploading", "error", err.Error()))
|
||||
}
|
||||
|
||||
// This keeps track of whether the file has to be deleted from the DB and the store
|
||||
@@ -87,10 +83,10 @@ func handleUploadMedia(c echo.Context) error {
|
||||
)
|
||||
defer func() {
|
||||
if cleanUp {
|
||||
app.media.Delete(fName)
|
||||
h.app.media.Delete(fName)
|
||||
|
||||
if thumbfName != "" {
|
||||
app.media.Delete(thumbfName)
|
||||
h.app.media.Delete(thumbfName)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -101,23 +97,23 @@ func handleUploadMedia(c echo.Context) error {
|
||||
// Create thumbnail from file for non-vector formats.
|
||||
isImage := inArray(ext, imageExts)
|
||||
if isImage {
|
||||
thumbFile, w, h, err := processImage(file)
|
||||
thumbFile, wi, he, err := processImage(file)
|
||||
if err != nil {
|
||||
cleanUp = true
|
||||
app.log.Printf("error resizing image: %v", err)
|
||||
h.app.log.Printf("error resizing image: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("media.errorResizing", "error", err.Error()))
|
||||
h.app.i18n.Ts("media.errorResizing", "error", err.Error()))
|
||||
}
|
||||
width = w
|
||||
height = h
|
||||
width = wi
|
||||
height = he
|
||||
|
||||
// Upload thumbnail.
|
||||
tf, err := app.media.Put(thumbPrefix+fName, contentType, thumbFile)
|
||||
tf, err := h.app.media.Put(thumbPrefix+fName, contentType, thumbFile)
|
||||
if err != nil {
|
||||
cleanUp = true
|
||||
app.log.Printf("error saving thumbnail: %v", err)
|
||||
h.app.log.Printf("error saving thumbnail: %v", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("media.errorSavingThumbnail", "error", err.Error()))
|
||||
h.app.i18n.Ts("media.errorSavingThumbnail", "error", err.Error()))
|
||||
}
|
||||
thumbfName = tf
|
||||
}
|
||||
@@ -135,7 +131,7 @@ func handleUploadMedia(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Insert the media into the DB.
|
||||
m, err := app.core.InsertMedia(fName, thumbfName, contentType, meta, app.constants.MediaUpload.Provider, app.media)
|
||||
m, err := h.app.core.InsertMedia(fName, thumbfName, contentType, meta, h.app.constants.MediaUpload.Provider, h.app.media)
|
||||
if err != nil {
|
||||
cleanUp = true
|
||||
return err
|
||||
@@ -144,16 +140,12 @@ func handleUploadMedia(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{m})
|
||||
}
|
||||
|
||||
// handleGetMedia handles retrieval of uploaded media.
|
||||
func handleGetMedia(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetMedia handles retrieval of uploaded media.
|
||||
func (h *Handlers) GetMedia(c echo.Context) error {
|
||||
// Fetch one media item from the DB.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id > 0 {
|
||||
out, err := app.core.GetMedia(id, "", "", app.media)
|
||||
out, err := h.app.core.GetMedia(id, "", "", h.app.media)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -163,10 +155,10 @@ func handleGetMedia(c echo.Context) error {
|
||||
|
||||
// Get the media from the DB.
|
||||
var (
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
query = c.FormValue("query")
|
||||
)
|
||||
res, total, err := app.core.QueryMedia(app.constants.MediaUpload.Provider, app.media, query, pg.Offset, pg.Limit)
|
||||
res, total, err := h.app.core.QueryMedia(h.app.constants.MediaUpload.Provider, h.app.media, query, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -181,26 +173,22 @@ func handleGetMedia(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// deleteMedia handles deletion of uploaded media.
|
||||
func handleDeleteMedia(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteMedia handles deletion of uploaded media.
|
||||
func (h *Handlers) DeleteMedia(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the media from the DB. The query returns the filename.
|
||||
fname, err := app.core.DeleteMedia(id)
|
||||
fname, err := h.app.core.DeleteMedia(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the files from the media store.
|
||||
app.media.Delete(fname)
|
||||
app.media.Delete(thumbPrefix + fname)
|
||||
h.app.media.Delete(fname)
|
||||
h.app.media.Delete(thumbPrefix + fname)
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/knadh/listmonk/models"
|
||||
)
|
||||
|
||||
type notifCB func(toEmails []string, subject, tplName string, data any, headers textproto.MIMEHeader) error
|
||||
|
||||
const (
|
||||
notifTplImport = "import-status"
|
||||
notifTplCampaign = "campaign-status"
|
||||
|
||||
320
cmd/public.go
320
cmd/public.go
@@ -111,17 +111,13 @@ func (t *tplRenderer) Render(w io.Writer, name string, data any, c echo.Context)
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetPublicLists returns the list of public lists with minimal fields
|
||||
// GetPublicLists returns the list of public lists with minimal fields
|
||||
// required to submit a subscription.
|
||||
func handleGetPublicLists(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) GetPublicLists(c echo.Context) error {
|
||||
// Get all public lists.
|
||||
lists, err := app.core.GetLists(models.ListTypePublic, true, nil)
|
||||
lists, err := h.app.core.GetLists(models.ListTypePublic, true, nil)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorFetchingLists"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.errorFetchingLists"))
|
||||
}
|
||||
|
||||
type list struct {
|
||||
@@ -140,102 +136,94 @@ func handleGetPublicLists(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// handleViewCampaignMessage renders the HTML view of a campaign message.
|
||||
// ViewCampaignMessage renders the HTML view of a campaign message.
|
||||
// This is the view the {{ MessageURL }} template tag links to in e-mail campaigns.
|
||||
func handleViewCampaignMessage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) ViewCampaignMessage(c echo.Context) error {
|
||||
// Get the campaign.
|
||||
campUUID := c.Param("campUUID")
|
||||
camp, err := app.core.GetCampaign(0, campUUID, "")
|
||||
camp, err := h.app.core.GetCampaign(0, campUUID, "")
|
||||
if err != nil {
|
||||
if er, ok := err.(*echo.HTTPError); ok {
|
||||
if er.Code == http.StatusBadRequest {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", app.i18n.T("public.campaignNotFound")))
|
||||
makeMsgTpl(h.app.i18n.T("public.notFoundTitle"), "", h.app.i18n.T("public.campaignNotFound")))
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
// Get the subscriber.
|
||||
subUUID := c.Param("subUUID")
|
||||
sub, err := app.core.GetSubscriber(0, subUUID, "")
|
||||
sub, err := h.app.core.GetSubscriber(0, subUUID, "")
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", app.i18n.T("public.errorFetchingEmail")))
|
||||
makeMsgTpl(h.app.i18n.T("public.notFoundTitle"), "", h.app.i18n.T("public.errorFetchingEmail")))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
// Compile the template.
|
||||
if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
app.log.Printf("error compiling template: %v", err)
|
||||
if err := camp.CompileTemplate(h.app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
h.app.log.Printf("error compiling template: %v", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
// Render the message body.
|
||||
msg, err := app.manager.NewCampaignMessage(&camp, sub)
|
||||
msg, err := h.app.manager.NewCampaignMessage(&camp, sub)
|
||||
if err != nil {
|
||||
app.log.Printf("error rendering message: %v", err)
|
||||
h.app.log.Printf("error rendering message: %v", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingCampaign")))
|
||||
}
|
||||
|
||||
return c.HTML(http.StatusOK, string(msg.Body()))
|
||||
}
|
||||
|
||||
// handleSubscriptionPage renders the subscription management page and handles unsubscriptions.
|
||||
// SubscriptionPage renders the subscription management page and handles unsubscriptions.
|
||||
// This is the view that {{ UnsubscribeURL }} in campaigns link to.
|
||||
func handleSubscriptionPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) SubscriptionPage(c echo.Context) error {
|
||||
var (
|
||||
subUUID = c.Param("subUUID")
|
||||
showManage, _ = strconv.ParseBool(c.FormValue("manage"))
|
||||
)
|
||||
|
||||
// Get the subscriber from the DB.
|
||||
s, err := app.core.GetSubscriber(0, subUUID, "")
|
||||
s, err := h.app.core.GetSubscriber(0, subUUID, "")
|
||||
if err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
// Prepare the public template.
|
||||
out := unsubTpl{
|
||||
Subscriber: s,
|
||||
SubUUID: subUUID,
|
||||
publicTpl: publicTpl{Title: app.i18n.T("public.unsubscribeTitle")},
|
||||
AllowBlocklist: app.constants.Privacy.AllowBlocklist,
|
||||
AllowExport: app.constants.Privacy.AllowExport,
|
||||
AllowWipe: app.constants.Privacy.AllowWipe,
|
||||
AllowPreferences: app.constants.Privacy.AllowPreferences,
|
||||
publicTpl: publicTpl{Title: h.app.i18n.T("public.unsubscribeTitle")},
|
||||
AllowBlocklist: h.app.constants.Privacy.AllowBlocklist,
|
||||
AllowExport: h.app.constants.Privacy.AllowExport,
|
||||
AllowWipe: h.app.constants.Privacy.AllowWipe,
|
||||
AllowPreferences: h.app.constants.Privacy.AllowPreferences,
|
||||
}
|
||||
|
||||
// If the subscriber is blocklisted, throw an error.
|
||||
if s.Status == models.SubscriberStatusBlockListed {
|
||||
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(app.i18n.T("public.noSubTitle"), "", app.i18n.Ts("public.blocklisted")))
|
||||
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(h.app.i18n.T("public.noSubTitle"), "", h.app.i18n.Ts("public.blocklisted")))
|
||||
}
|
||||
|
||||
// Only show preference management if it's enabled in settings.
|
||||
if app.constants.Privacy.AllowPreferences {
|
||||
if h.app.constants.Privacy.AllowPreferences {
|
||||
out.ShowManage = showManage
|
||||
|
||||
// Get the subscriber's lists from the DB to render in the template.
|
||||
subs, err := app.core.GetSubscriptions(0, subUUID, false)
|
||||
subs, err := h.app.core.GetSubscriptions(0, subUUID, false)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorFetchingLists"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.errorFetchingLists"))
|
||||
}
|
||||
|
||||
out.Subscriptions = make([]models.Subscription, 0, len(subs))
|
||||
@@ -252,14 +240,10 @@ func handleSubscriptionPage(c echo.Context) error {
|
||||
return c.Render(http.StatusOK, "subscription", out)
|
||||
}
|
||||
|
||||
// handleSubscriptionPrefs renders the subscription management page and
|
||||
// handles unsubscriptions. This is the view that {{ UnsubscribeURL }} in
|
||||
// SubscriptionPrefs renders the subscription management page and
|
||||
// s unsubscriptions. This is the view that {{ UnsubscribeURL }} in
|
||||
// campaigns link to.
|
||||
func handleSubscriptionPrefs(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) SubscriptionPrefs(c echo.Context) error {
|
||||
// Read the form.
|
||||
var req struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
@@ -269,51 +253,51 @@ func handleSubscriptionPrefs(c echo.Context) error {
|
||||
}
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("globals.messages.invalidData")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("globals.messages.invalidData")))
|
||||
}
|
||||
|
||||
// Simple unsubscribe.
|
||||
var (
|
||||
campUUID = c.Param("campUUID")
|
||||
subUUID = c.Param("subUUID")
|
||||
blocklist = app.constants.Privacy.AllowBlocklist && req.Blocklist
|
||||
blocklist = h.app.constants.Privacy.AllowBlocklist && req.Blocklist
|
||||
)
|
||||
if !req.Manage || blocklist {
|
||||
if err := app.core.UnsubscribeByCampaign(subUUID, campUUID, blocklist); err != nil {
|
||||
if err := h.app.core.UnsubscribeByCampaign(subUUID, campUUID, blocklist); err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.unsubbedTitle"), "", app.i18n.T("public.unsubbedInfo")))
|
||||
makeMsgTpl(h.app.i18n.T("public.unsubbedTitle"), "", h.app.i18n.T("public.unsubbedInfo")))
|
||||
}
|
||||
|
||||
// Is preference management enabled?
|
||||
if !app.constants.Privacy.AllowPreferences {
|
||||
if !h.app.constants.Privacy.AllowPreferences {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidFeature")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("public.invalidFeature")))
|
||||
}
|
||||
|
||||
// Manage preferences.
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if req.Name == "" || len(req.Name) > 256 {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("subscribers.invalidName")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("subscribers.invalidName")))
|
||||
}
|
||||
|
||||
// Get the subscriber from the DB.
|
||||
sub, err := app.core.GetSubscriber(0, subUUID, "")
|
||||
sub, err := h.app.core.GetSubscriber(0, subUUID, "")
|
||||
if err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("globals.messages.pFound",
|
||||
"name", app.i18n.T("globals.terms.subscriber"))))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("globals.messages.pFound",
|
||||
"name", h.app.i18n.T("globals.terms.subscriber"))))
|
||||
}
|
||||
sub.Name = req.Name
|
||||
|
||||
// Update the subscriber properties in the DB.
|
||||
if _, err := app.core.UpdateSubscriber(sub.ID, sub); err != nil {
|
||||
if _, err := h.app.core.UpdateSubscriber(sub.ID, sub); err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
// Get the subscriber's lists and whatever is not sent in the request (unchecked),
|
||||
@@ -324,9 +308,9 @@ func handleSubscriptionPrefs(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get subscription from teh DB.
|
||||
subs, err := app.core.GetSubscriptions(0, subUUID, false)
|
||||
subs, err := h.app.core.GetSubscriptions(0, subUUID, false)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.errorFetchingLists"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.errorFetchingLists"))
|
||||
}
|
||||
|
||||
// Filter the lists in the request against the subscriptions in the DB.
|
||||
@@ -341,24 +325,20 @@ func handleSubscriptionPrefs(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Unsubscribe from lists.
|
||||
if err := app.core.UnsubscribeLists([]int{sub.ID}, nil, unsubUUIDs); err != nil {
|
||||
if err := h.app.core.UnsubscribeLists([]int{sub.ID}, nil, unsubUUIDs); err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("public.errorProcessingRequest")))
|
||||
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("globals.messages.done"), "", app.i18n.T("public.prefsSaved")))
|
||||
makeMsgTpl(h.app.i18n.T("globals.messages.done"), "", h.app.i18n.T("public.prefsSaved")))
|
||||
}
|
||||
|
||||
// handleOptinPage renders the double opt-in confirmation page that subscribers
|
||||
// OptinPage renders the double opt-in confirmation page that subscribers
|
||||
// see when they click on the "Confirm subscription" button in double-optin
|
||||
// notifications.
|
||||
func handleOptinPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) OptinPage(c echo.Context) error {
|
||||
var (
|
||||
subUUID = c.Param("subUUID")
|
||||
confirm, _ = strconv.ParseBool(c.FormValue("confirm"))
|
||||
@@ -373,28 +353,28 @@ func handleOptinPage(c echo.Context) error {
|
||||
for _, l := range req.ListUUIDs {
|
||||
if !reUUID.MatchString(l) {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("globals.messages.invalidUUID")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("globals.messages.invalidUUID")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the list of subscription lists where the subscriber hasn't confirmed.
|
||||
lists, err := app.core.GetSubscriberLists(0, subUUID, nil, req.ListUUIDs, models.SubscriptionStatusUnconfirmed, "")
|
||||
lists, err := h.app.core.GetSubscriberLists(0, subUUID, nil, req.ListUUIDs, models.SubscriptionStatusUnconfirmed, "")
|
||||
if err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingLists")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingLists")))
|
||||
}
|
||||
|
||||
// There are no lists to confirm.
|
||||
if len(lists) == 0 {
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.noSubTitle"), "", app.i18n.Ts("public.noSubInfo")))
|
||||
makeMsgTpl(h.app.i18n.T("public.noSubTitle"), "", h.app.i18n.Ts("public.noSubInfo")))
|
||||
}
|
||||
|
||||
// Confirm.
|
||||
if confirm {
|
||||
meta := models.JSON{}
|
||||
if app.constants.Privacy.RecordOptinIP {
|
||||
if h.app.constants.Privacy.RecordOptinIP {
|
||||
if h := c.Request().Header.Get("X-Forwarded-For"); h != "" {
|
||||
meta["optin_ip"] = h
|
||||
} else if h := c.Request().RemoteAddr; h != "" {
|
||||
@@ -403,94 +383,86 @@ func handleOptinPage(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Confirm subscriptions in the DB.
|
||||
if err := app.core.ConfirmOptionSubscription(subUUID, req.ListUUIDs, meta); err != nil {
|
||||
app.log.Printf("error unsubscribing: %v", err)
|
||||
if err := h.app.core.ConfirmOptionSubscription(subUUID, req.ListUUIDs, meta); err != nil {
|
||||
h.app.log.Printf("error unsubscribing: %v", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.subConfirmedTitle"), "", app.i18n.Ts("public.subConfirmed")))
|
||||
makeMsgTpl(h.app.i18n.T("public.subConfirmedTitle"), "", h.app.i18n.Ts("public.subConfirmed")))
|
||||
}
|
||||
|
||||
var out optinTpl
|
||||
out.Lists = lists
|
||||
out.SubUUID = subUUID
|
||||
out.Title = app.i18n.T("public.confirmOptinSubTitle")
|
||||
out.Title = h.app.i18n.T("public.confirmOptinSubTitle")
|
||||
|
||||
return c.Render(http.StatusOK, "optin", out)
|
||||
}
|
||||
|
||||
// handleSubscriptionFormPage handles subscription requests coming from public
|
||||
// SubscriptionFormPage handles subscription requests coming from public
|
||||
// HTML subscription forms.
|
||||
func handleSubscriptionFormPage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
if !app.constants.EnablePublicSubPage {
|
||||
func (h *Handlers) SubscriptionFormPage(c echo.Context) error {
|
||||
if !h.app.constants.EnablePublicSubPage {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.invalidFeature")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.invalidFeature")))
|
||||
}
|
||||
|
||||
// Get all public lists from the DB.
|
||||
lists, err := app.core.GetLists(models.ListTypePublic, true, nil)
|
||||
lists, err := h.app.core.GetLists(models.ListTypePublic, true, nil)
|
||||
if err != nil {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorFetchingLists")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorFetchingLists")))
|
||||
}
|
||||
|
||||
// There are no public lists available for subscription.
|
||||
if len(lists) == 0 {
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.noListsAvailable")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.noListsAvailable")))
|
||||
}
|
||||
|
||||
out := subFormTpl{}
|
||||
out.Title = app.i18n.T("public.sub")
|
||||
out.Title = h.app.i18n.T("public.sub")
|
||||
out.Lists = lists
|
||||
|
||||
// Captcha is enabled. Set the key for the template to render.
|
||||
if app.constants.Security.EnableCaptcha {
|
||||
out.CaptchaKey = app.constants.Security.CaptchaKey
|
||||
if h.app.constants.Security.EnableCaptcha {
|
||||
out.CaptchaKey = h.app.constants.Security.CaptchaKey
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, "subscription-form", out)
|
||||
}
|
||||
|
||||
// handleSubscriptionForm handles subscription requests coming from public
|
||||
// SubscriptionForm handles subscription requests coming from public
|
||||
// HTML subscription forms.
|
||||
func handleSubscriptionForm(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) SubscriptionForm(c echo.Context) error {
|
||||
// If there's a nonce value, a bot could've filled the form.
|
||||
if c.FormValue("nonce") != "" {
|
||||
return echo.NewHTTPError(http.StatusBadGateway, app.i18n.T("public.invalidFeature"))
|
||||
return echo.NewHTTPError(http.StatusBadGateway, h.app.i18n.T("public.invalidFeature"))
|
||||
}
|
||||
|
||||
// Process CAPTCHA.
|
||||
if app.constants.Security.EnableCaptcha {
|
||||
err, ok := app.captcha.Verify(c.FormValue("h-captcha-response"))
|
||||
if h.app.constants.Security.EnableCaptcha {
|
||||
err, ok := h.app.captcha.Verify(c.FormValue("h-captcha-response"))
|
||||
if err != nil {
|
||||
app.log.Printf("Captcha request failed: %v", err)
|
||||
h.app.log.Printf("Captcha request failed: %v", err)
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.invalidCaptcha")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.T("public.invalidCaptcha")))
|
||||
}
|
||||
}
|
||||
|
||||
hasOptin, err := processSubForm(c)
|
||||
hasOptin, err := h.processSubForm(c)
|
||||
if err != nil {
|
||||
e, ok := err.(*echo.HTTPError)
|
||||
if !ok {
|
||||
return e
|
||||
}
|
||||
|
||||
return c.Render(e.Code, tplMessage, makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", e.Message)))
|
||||
return c.Render(e.Code, tplMessage, makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", e.Message)))
|
||||
}
|
||||
|
||||
// If there were double optin lists, show the opt-in pending message instead of
|
||||
@@ -500,21 +472,17 @@ func handleSubscriptionForm(c echo.Context) error {
|
||||
msg = "public.subOptinPending"
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(app.i18n.T("public.subTitle"), "", app.i18n.Ts(msg)))
|
||||
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(h.app.i18n.T("public.subTitle"), "", h.app.i18n.Ts(msg)))
|
||||
}
|
||||
|
||||
// handlePublicSubscription handles subscription requests coming from public
|
||||
// PublicSubscription handles subscription requests coming from public
|
||||
// API calls.
|
||||
func handlePublicSubscription(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
if !app.constants.EnablePublicSubPage {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.invalidFeature"))
|
||||
func (h *Handlers) PublicSubscription(c echo.Context) error {
|
||||
if !h.app.constants.EnablePublicSubPage {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.invalidFeature"))
|
||||
}
|
||||
|
||||
hasOptin, err := processSubForm(c)
|
||||
hasOptin, err := h.processSubForm(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -524,17 +492,13 @@ func handlePublicSubscription(c echo.Context) error {
|
||||
}{hasOptin}})
|
||||
}
|
||||
|
||||
// handleLinkRedirect redirects a link UUID to its original underlying link
|
||||
// LinkRedirect redirects a link UUID to its original underlying link
|
||||
// after recording the link click for a particular subscriber in the particular
|
||||
// campaign. These links are generated by {{ TrackLink }} tags in campaigns.
|
||||
func handleLinkRedirect(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) LinkRedirect(c echo.Context) error {
|
||||
// If individual tracking is disabled, do not record the subscriber ID.
|
||||
subUUID := c.Param("subUUID")
|
||||
if !app.constants.Privacy.IndividualTracking {
|
||||
if !h.app.constants.Privacy.IndividualTracking {
|
||||
subUUID = ""
|
||||
}
|
||||
|
||||
@@ -543,35 +507,31 @@ func handleLinkRedirect(c echo.Context) error {
|
||||
linkUUID = c.Param("linkUUID")
|
||||
campUUID = c.Param("campUUID")
|
||||
)
|
||||
url, err := app.core.RegisterCampaignLinkClick(linkUUID, campUUID, subUUID)
|
||||
url, err := h.app.core.RegisterCampaignLinkClick(linkUUID, campUUID, subUUID)
|
||||
if err != nil {
|
||||
e := err.(*echo.HTTPError)
|
||||
return c.Render(e.Code, tplMessage, makeMsgTpl(app.i18n.T("public.errorTitle"), "", e.Error()))
|
||||
return c.Render(e.Code, tplMessage, makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", e.Error()))
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusTemporaryRedirect, url)
|
||||
}
|
||||
|
||||
// handleRegisterCampaignView registers a campaign view which comes in
|
||||
// RegisterCampaignView registers a campaign view which comes in
|
||||
// the form of an pixel image request. Regardless of errors, this handler
|
||||
// should always render the pixel image bytes. The pixel URL is generated by
|
||||
// the {{ TrackView }} template tag in campaigns.
|
||||
func handleRegisterCampaignView(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) RegisterCampaignView(c echo.Context) error {
|
||||
// If individual tracking is disabled, do not record the subscriber ID.
|
||||
subUUID := c.Param("subUUID")
|
||||
if !app.constants.Privacy.IndividualTracking {
|
||||
if !h.app.constants.Privacy.IndividualTracking {
|
||||
subUUID = ""
|
||||
}
|
||||
|
||||
// Exclude dummy hits from template previews.
|
||||
campUUID := c.Param("campUUID")
|
||||
if campUUID != dummyUUID && subUUID != dummyUUID {
|
||||
if err := app.core.RegisterCampaignView(campUUID, subUUID); err != nil {
|
||||
app.log.Printf("error registering campaign view: %s", err)
|
||||
if err := h.app.core.RegisterCampaignView(campUUID, subUUID); err != nil {
|
||||
h.app.log.Printf("error registering campaign view: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -579,51 +539,47 @@ func handleRegisterCampaignView(c echo.Context) error {
|
||||
return c.Blob(http.StatusOK, "image/png", pixelPNG)
|
||||
}
|
||||
|
||||
// handleSelfExportSubscriberData pulls the subscriber's profile, list subscriptions,
|
||||
// SelfExportSubscriberData pulls the subscriber's profile, list subscriptions,
|
||||
// campaign views and clicks and produces a JSON report that is then e-mailed
|
||||
// to the subscriber. This is a privacy feature and the data that's exported
|
||||
// is dependent on the configuration.
|
||||
func handleSelfExportSubscriberData(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) SelfExportSubscriberData(c echo.Context) error {
|
||||
// Is export allowed?
|
||||
if !app.constants.Privacy.AllowExport {
|
||||
if !h.app.constants.Privacy.AllowExport {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.invalidFeature")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.invalidFeature")))
|
||||
}
|
||||
|
||||
// Get the subscriber's data. A single query that gets the profile,
|
||||
// list subscriptions, campaign views, and link clicks. Names of
|
||||
// private lists are replaced with "Private list".
|
||||
subUUID := c.Param("subUUID")
|
||||
data, b, err := exportSubscriberData(0, subUUID, app.constants.Privacy.Exportable, app)
|
||||
data, b, err := h.exportSubscriberData(0, subUUID, h.app.constants.Privacy.Exportable)
|
||||
if err != nil {
|
||||
app.log.Printf("error exporting subscriber data: %s", err)
|
||||
h.app.log.Printf("error exporting subscriber data: %s", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
// Prepare the attachment e-mail.
|
||||
var msg bytes.Buffer
|
||||
if err := app.notifTpls.tpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
|
||||
app.log.Printf("error compiling notification template '%s': %v", notifSubscriberData, err)
|
||||
if err := h.app.notifTpls.tpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
|
||||
h.app.log.Printf("error compiling notification template '%s': %v", notifSubscriberData, err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
var (
|
||||
subject = app.i18n.Ts("email.data.title")
|
||||
subject = h.app.i18n.Ts("email.data.title")
|
||||
body = msg.Bytes()
|
||||
)
|
||||
subject, body = getTplSubject(subject, body)
|
||||
|
||||
// E-mail the data as a JSON attachment to the subscriber.
|
||||
const fname = "data.json"
|
||||
if err := app.emailMessenger.Push(models.Message{
|
||||
ContentType: app.notifTpls.contentType,
|
||||
From: app.constants.FromEmail,
|
||||
if err := h.app.emailMessenger.Push(models.Message{
|
||||
ContentType: h.app.notifTpls.contentType,
|
||||
From: h.app.constants.FromEmail,
|
||||
To: []string{data.Email},
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
@@ -635,38 +591,34 @@ func handleSelfExportSubscriberData(c echo.Context) error {
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
app.log.Printf("error e-mailing subscriber profile: %s", err)
|
||||
h.app.log.Printf("error e-mailing subscriber profile: %s", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.dataSentTitle"), "", app.i18n.T("public.dataSent")))
|
||||
makeMsgTpl(h.app.i18n.T("public.dataSentTitle"), "", h.app.i18n.T("public.dataSent")))
|
||||
}
|
||||
|
||||
// handleWipeSubscriberData allows a subscriber to delete their data. The
|
||||
// WipeSubscriberData allows a subscriber to delete their data. The
|
||||
// profile and subscriptions are deleted, while the campaign_views and link
|
||||
// clicks remain as orphan data unconnected to any subscriber.
|
||||
func handleWipeSubscriberData(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) WipeSubscriberData(c echo.Context) error {
|
||||
// Is wiping allowed?
|
||||
if !app.constants.Privacy.AllowWipe {
|
||||
if !h.app.constants.Privacy.AllowWipe {
|
||||
return c.Render(http.StatusBadRequest, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.invalidFeature")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.invalidFeature")))
|
||||
}
|
||||
|
||||
subUUID := c.Param("subUUID")
|
||||
if err := app.core.DeleteSubscribers(nil, []string{subUUID}); err != nil {
|
||||
app.log.Printf("error wiping subscriber data: %s", err)
|
||||
if err := h.app.core.DeleteSubscribers(nil, []string{subUUID}); err != nil {
|
||||
h.app.log.Printf("error wiping subscriber data: %s", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest")))
|
||||
makeMsgTpl(h.app.i18n.T("public.errorTitle"), "", h.app.i18n.Ts("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
return c.Render(http.StatusOK, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.dataRemovedTitle"), "", app.i18n.T("public.dataRemoved")))
|
||||
makeMsgTpl(h.app.i18n.T("public.dataRemovedTitle"), "", h.app.i18n.T("public.dataRemoved")))
|
||||
}
|
||||
|
||||
// drawTransparentImage draws a transparent PNG of given dimensions
|
||||
@@ -684,11 +636,7 @@ func drawTransparentImage(h, w int) []byte {
|
||||
// processSubForm processes an incoming form/public API subscription request.
|
||||
// The bool indicates whether there was subscription to an optin list so that
|
||||
// an appropriate message can be shown.
|
||||
func processSubForm(c echo.Context) (bool, error) {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) processSubForm(c echo.Context) (bool, error) {
|
||||
// Get and validate fields.
|
||||
var req struct {
|
||||
Name string `form:"name" json:"name"`
|
||||
@@ -700,7 +648,7 @@ func processSubForm(c echo.Context) (bool, error) {
|
||||
}
|
||||
|
||||
if len(req.FormListUUIDs) == 0 {
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("public.noListsSelected"))
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("public.noListsSelected"))
|
||||
}
|
||||
|
||||
// If there's no name, use the name bit from the e-mail.
|
||||
@@ -711,10 +659,10 @@ func processSubForm(c echo.Context) (bool, error) {
|
||||
|
||||
// Validate fields.
|
||||
if len(req.Email) > 1000 {
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail"))
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.invalidEmail"))
|
||||
}
|
||||
|
||||
em, err := app.importer.SanitizeEmail(req.Email)
|
||||
em, err := h.app.importer.SanitizeEmail(req.Email)
|
||||
if err != nil {
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -722,25 +670,25 @@ func processSubForm(c echo.Context) (bool, error) {
|
||||
|
||||
req.Name = strings.TrimSpace(req.Name)
|
||||
if len(req.Name) == 0 || len(req.Name) > stdInputMaxLen {
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.invalidName"))
|
||||
}
|
||||
|
||||
listUUIDs := pq.StringArray(req.FormListUUIDs)
|
||||
|
||||
// Fetch the list types and ensure that they are not private.
|
||||
listTypes, err := app.core.GetListTypes(nil, req.FormListUUIDs)
|
||||
listTypes, err := h.app.core.GetListTypes(nil, req.FormListUUIDs)
|
||||
if err != nil {
|
||||
return false, echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%s", err.(*echo.HTTPError).Message))
|
||||
}
|
||||
|
||||
for _, t := range listTypes {
|
||||
if t == models.ListTypePrivate {
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidUUID"))
|
||||
return false, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidUUID"))
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the subscriber into the DB.
|
||||
_, hasOptin, err := app.core.InsertSubscriber(models.Subscriber{
|
||||
_, hasOptin, err := h.app.core.InsertSubscriber(models.Subscriber{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Status: models.SubscriberStatusEnabled,
|
||||
@@ -749,13 +697,13 @@ func processSubForm(c echo.Context) (bool, error) {
|
||||
// Subscriber already exists. Update subscriptions in the DB.
|
||||
if e, ok := err.(*echo.HTTPError); ok && e.Code == http.StatusConflict {
|
||||
// Get the subscriber from the DB by their email.
|
||||
sub, err := app.core.GetSubscriber(0, "", req.Email)
|
||||
sub, err := h.app.core.GetSubscriber(0, "", req.Email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Update the subscriber's subscriptions in the DB.
|
||||
_, hasOptin, err := app.core.UpdateSubscriberWithLists(sub.ID, sub, nil, listUUIDs, false, false)
|
||||
_, hasOptin, err := h.app.core.UpdateSubscriberWithLists(sub.ID, sub, nil, listUUIDs, false, false)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
104
cmd/roles.go
104
cmd/roles.go
@@ -10,14 +10,10 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleGetUserRoles retrieves roles.
|
||||
func handleGetUserRoles(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetUserRoles retrieves roles.
|
||||
func (h *Handlers) GetUserRoles(c echo.Context) error {
|
||||
// Get all roles.
|
||||
out, err := app.core.GetRoles()
|
||||
out, err := h.app.core.GetRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -25,14 +21,10 @@ func handleGetUserRoles(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleGeListRoles retrieves roles.
|
||||
func handleGeListRoles(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GeListRoles retrieves roles.
|
||||
func (h *Handlers) GeListRoles(c echo.Context) error {
|
||||
// Get all roles.
|
||||
out, err := app.core.GetListRoles()
|
||||
out, err := h.app.core.GetListRoles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -40,22 +32,18 @@ func handleGeListRoles(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleCreateUserRole handles role creation.
|
||||
func handleCreateUserRole(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CreateUserRole handles role creation.
|
||||
func (h *Handlers) CreateUserRole(c echo.Context) error {
|
||||
var r auth.Role
|
||||
if err := c.Bind(&r); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateUserRole(r, app); err != nil {
|
||||
if err := h.validateUserRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the role in the DB.
|
||||
out, err := app.core.CreateRole(r)
|
||||
out, err := h.app.core.CreateRole(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -63,22 +51,18 @@ func handleCreateUserRole(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleCreateListRole handles role creation.
|
||||
func handleCreateListRole(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CreateListRole handles role creation.
|
||||
func (h *Handlers) CreateListRole(c echo.Context) error {
|
||||
var r auth.ListRole
|
||||
if err := c.Bind(&r); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateListRole(r, app); err != nil {
|
||||
if err := h.validateListRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the role in the DB.
|
||||
out, err := app.core.CreateListRole(r)
|
||||
out, err := h.app.core.CreateListRole(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,16 +70,12 @@ func handleCreateListRole(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateUserRole handles role modification.
|
||||
func handleUpdateUserRole(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UpdateUserRole handles role modification.
|
||||
func (h *Handlers) UpdateUserRole(c echo.Context) error {
|
||||
// ID 1 is reserved for the Super Admin role and anything below that is invalid.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 2 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Incoming params.
|
||||
@@ -103,7 +83,7 @@ func handleUpdateUserRole(c echo.Context) error {
|
||||
if err := c.Bind(&r); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateUserRole(r, app); err != nil {
|
||||
if err := h.validateUserRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -111,29 +91,25 @@ func handleUpdateUserRole(c echo.Context) error {
|
||||
r.Name.String = strings.TrimSpace(r.Name.String)
|
||||
|
||||
// Update the role in the DB.
|
||||
out, err := app.core.UpdateUserRole(id, r)
|
||||
out, err := h.app.core.UpdateUserRole(id, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache API tokens for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateListRole handles role modification.
|
||||
func handleUpdateListRole(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UpdateListRole handles role modification.
|
||||
func (h *Handlers) UpdateListRole(c echo.Context) error {
|
||||
// ID 1 is reserved for the Super Admin role and anything below that is invalid.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 2 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Incoming params.
|
||||
@@ -142,7 +118,7 @@ func handleUpdateListRole(c echo.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateListRole(r, app); err != nil {
|
||||
if err := h.validateListRole(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -150,66 +126,62 @@ func handleUpdateListRole(c echo.Context) error {
|
||||
r.Name.String = strings.TrimSpace(r.Name.String)
|
||||
|
||||
// Update the role in the DB.
|
||||
out, err := app.core.UpdateListRole(id, r)
|
||||
out, err := h.app.core.UpdateListRole(id, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache API tokens for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleDeleteRole handles role deletion.
|
||||
func handleDeleteRole(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteRole handles role deletion.
|
||||
func (h *Handlers) DeleteRole(c echo.Context) error {
|
||||
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if id < 2 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the role from the DB.
|
||||
if err := app.core.DeleteRole(int(id)); err != nil {
|
||||
if err := h.app.core.DeleteRole(int(id)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache API tokens for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
func validateUserRole(r auth.Role, app *App) error {
|
||||
func (h *Handlers) validateUserRole(r auth.Role) error {
|
||||
if !strHasLen(r.Name.String, 1, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "name"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "name"))
|
||||
}
|
||||
|
||||
for _, p := range r.Permissions {
|
||||
if _, ok := app.constants.Permissions[p]; !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("permission: %s", p)))
|
||||
if _, ok := h.app.constants.Permissions[p]; !ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("permission: %s", p)))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateListRole(r auth.ListRole, app *App) error {
|
||||
func (h *Handlers) validateListRole(r auth.ListRole) error {
|
||||
if !strHasLen(r.Name.String, 1, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "name"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "name"))
|
||||
}
|
||||
|
||||
for _, l := range r.Lists {
|
||||
for _, p := range l.Permissions {
|
||||
if p != auth.PermListGet && p != auth.PermListManage {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("list permission: %s", p)))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("list permission: %s", p)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
100
cmd/settings.go
100
cmd/settings.go
@@ -50,13 +50,9 @@ var (
|
||||
reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
|
||||
)
|
||||
|
||||
// handleGetSettings returns settings from the DB.
|
||||
func handleGetSettings(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
s, err := app.core.GetSettings()
|
||||
// GetSettings returns settings from the DB.
|
||||
func (h *Handlers) GetSettings(c echo.Context) error {
|
||||
s, err := h.app.core.GetSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -82,12 +78,8 @@ func handleGetSettings(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{s})
|
||||
}
|
||||
|
||||
// handleUpdateSettings returns settings from the DB.
|
||||
func handleUpdateSettings(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UpdateSettings returns settings from the DB.
|
||||
func (h *Handlers) UpdateSettings(c echo.Context) error {
|
||||
// Unmarshal and marshal the fields once to sanitize the settings blob.
|
||||
var set models.Settings
|
||||
if err := c.Bind(&set); err != nil {
|
||||
@@ -95,7 +87,7 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the existing settings.
|
||||
cur, err := app.core.GetSettings()
|
||||
cur, err := h.app.core.GetSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -121,7 +113,7 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
|
||||
if _, ok := names[name]; ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
||||
h.app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
||||
}
|
||||
|
||||
names[name] = true
|
||||
@@ -151,7 +143,7 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
}
|
||||
}
|
||||
if !has {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("settings.errorNoSMTP"))
|
||||
}
|
||||
|
||||
// Always remove the trailing slash from the app root URL.
|
||||
@@ -172,7 +164,7 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
set.BounceBoxes[i].Host = strings.TrimSpace(s.Host)
|
||||
|
||||
if d, _ := time.ParseDuration(s.ScanInterval); d.Minutes() < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.bounces.invalidScanInterval"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("settings.bounces.invalidScanInterval"))
|
||||
}
|
||||
|
||||
// If there's no password coming in from the frontend, copy the existing
|
||||
@@ -203,10 +195,10 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
name := reAlphaNum.ReplaceAllString(strings.ToLower(m.Name), "")
|
||||
if _, ok := names[name]; ok {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
||||
h.app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
||||
}
|
||||
if len(name) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.invalidMessengerName"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("settings.invalidMessengerName"))
|
||||
}
|
||||
|
||||
set.Messengers[i].Name = name
|
||||
@@ -257,74 +249,66 @@ func handleUpdateSettings(c echo.Context) error {
|
||||
// Validate slow query caching cron.
|
||||
if set.CacheSlowQueries {
|
||||
if _, err := cron.ParseStandard(set.CacheSlowQueriesInterval); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidData")+": slow query cron: "+err.Error())
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidData")+": slow query cron: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Update the settings in the DB.
|
||||
if err := app.core.UpdateSettings(set); err != nil {
|
||||
if err := h.app.core.UpdateSettings(set); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there are any active campaigns, don't do an auto reload and
|
||||
// warn the user on the frontend.
|
||||
if app.manager.HasRunningCampaigns() {
|
||||
app.Lock()
|
||||
app.needsRestart = true
|
||||
app.Unlock()
|
||||
if h.app.manager.HasRunningCampaigns() {
|
||||
h.app.Lock()
|
||||
h.app.needsRestart = true
|
||||
h.app.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{struct {
|
||||
NeedsRestart bool `json:"needs_restart"`
|
||||
}{true}})
|
||||
}
|
||||
|
||||
// No running campaigns. Reload the app.
|
||||
// No running campaigns. Reload the h.app.
|
||||
go func() {
|
||||
<-time.After(time.Millisecond * 500)
|
||||
app.chReload <- syscall.SIGHUP
|
||||
h.app.chReload <- syscall.SIGHUP
|
||||
}()
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleGetLogs returns the log entries stored in the log buffer.
|
||||
func handleGetLogs(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
|
||||
// GetLogs returns the log entries stored in the log buffer.
|
||||
func (h *Handlers) GetLogs(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{h.app.bufLog.Lines()})
|
||||
}
|
||||
|
||||
// handleTestSMTPSettings returns the log entries stored in the log buffer.
|
||||
func handleTestSMTPSettings(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// TestSMTPSettings returns the log entries stored in the log buffer.
|
||||
func (h *Handlers) TestSMTPSettings(c echo.Context) error {
|
||||
// Copy the raw JSON post body.
|
||||
reqBody, err := io.ReadAll(c.Request().Body)
|
||||
if err != nil {
|
||||
app.log.Printf("error reading SMTP test: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
||||
h.app.log.Printf("error reading SMTP test: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
// Load the JSON into koanf to parse SMTP settings properly including timestrings.
|
||||
ko := koanf.New(".")
|
||||
if err := ko.Load(rawbytes.Provider(reqBody), json.Parser()); err != nil {
|
||||
app.log.Printf("error unmarshalling SMTP test request: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
||||
h.app.log.Printf("error unmarshalling SMTP test request: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
req := email.Server{}
|
||||
if err := ko.UnmarshalWithConf("", &req, koanf.UnmarshalConf{Tag: "json"}); err != nil {
|
||||
app.log.Printf("error scanning SMTP test request: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.internalError"))
|
||||
h.app.log.Printf("error scanning SMTP test request: %v", err)
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.internalError"))
|
||||
}
|
||||
|
||||
to := ko.String("email")
|
||||
if to == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.missingFields", "name", "email"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.missingFields", "name", "email"))
|
||||
}
|
||||
|
||||
// Initialize a new SMTP pool.
|
||||
@@ -334,38 +318,34 @@ func handleTestSMTPSettings(c echo.Context) error {
|
||||
msgr, err := email.New("", req)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorCreating", "name", "SMTP", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorCreating", "name", "SMTP", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Render the test email template body.
|
||||
var b bytes.Buffer
|
||||
if err := app.notifTpls.tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
|
||||
app.log.Printf("error compiling notification template '%s': %v", "smtp-test", err)
|
||||
if err := h.app.notifTpls.tpls.ExecuteTemplate(&b, "smtp-test", nil); err != nil {
|
||||
h.app.log.Printf("error compiling notification template '%s': %v", "smtp-test", err)
|
||||
return err
|
||||
}
|
||||
|
||||
m := models.Message{}
|
||||
m.ContentType = app.notifTpls.contentType
|
||||
m.From = app.constants.FromEmail
|
||||
m.ContentType = h.app.notifTpls.contentType
|
||||
m.From = h.app.constants.FromEmail
|
||||
m.To = []string{to}
|
||||
m.Subject = app.i18n.T("settings.smtp.testConnection")
|
||||
m.Subject = h.app.i18n.T("settings.smtp.testConnection")
|
||||
m.Body = b.Bytes()
|
||||
if err := msgr.Push(m); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
|
||||
return c.JSON(http.StatusOK, okResp{h.app.bufLog.Lines()})
|
||||
}
|
||||
|
||||
func handleGetAboutInfo(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) GetAboutInfo(c echo.Context) error {
|
||||
var mem runtime.MemStats
|
||||
runtime.ReadMemStats(&mem)
|
||||
|
||||
out := app.about
|
||||
out := h.app.about
|
||||
out.System.AllocMB = mem.Alloc / 1024 / 1024
|
||||
out.System.OSMB = mem.Sys / 1024 / 1024
|
||||
|
||||
|
||||
@@ -51,25 +51,23 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetSubscriber handles the retrieval of a single subscriber by ID.
|
||||
func handleGetSubscriber(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// GetSubscriber handles the retrieval of a single subscriber by ID.
|
||||
func (h *Handlers) GetSubscriber(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to at least one of the lists on the subscriber.
|
||||
if err := hasSubPerm(user, []int{id}, app); err != nil {
|
||||
if err := h.hasSubPerm(user, []int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch the subscriber from the DB.
|
||||
out, err := app.core.GetSubscriber(id, "", "")
|
||||
out, err := h.app.core.GetSubscriber(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -77,15 +75,13 @@ func handleGetSubscriber(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleQuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
|
||||
func handleQuerySubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// QuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
|
||||
func (h *Handlers) QuerySubscribers(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Filter list IDs by permission.
|
||||
listIDs, err := filterListQueryByPerm("list_id", c.QueryParams(), user, app)
|
||||
listIDs, err := h.filterListQueryByPerm("list_id", c.QueryParams(), user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -96,9 +92,9 @@ func handleQuerySubscribers(c echo.Context) error {
|
||||
subStatus = c.FormValue("subscription_status")
|
||||
orderBy = c.FormValue("order_by")
|
||||
order = c.FormValue("order")
|
||||
pg = app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
pg = h.app.paginator.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
res, total, err := app.core.QuerySubscribers(query, listIDs, subStatus, order, orderBy, pg.Offset, pg.Limit)
|
||||
res, total, err := h.app.core.QuerySubscribers(query, listIDs, subStatus, order, orderBy, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -114,15 +110,13 @@ func handleQuerySubscribers(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleExportSubscribers handles querying subscribers based on an arbitrary SQL expression.
|
||||
func handleExportSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// ExportSubscribers handles querying subscribers based on an arbitrary SQL expression.
|
||||
func (h *Handlers) ExportSubscribers(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Filter list IDs by permission.
|
||||
listIDs, err := filterListQueryByPerm("list_id", c.QueryParams(), user, app)
|
||||
listIDs, err := h.filterListQueryByPerm("list_id", c.QueryParams(), user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,7 +124,7 @@ func handleExportSubscribers(c echo.Context) error {
|
||||
// Export only specific subscriber IDs?
|
||||
subIDs, err := getQueryInts("id", c.QueryParams())
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Filter by subscription status
|
||||
@@ -138,21 +132,21 @@ func handleExportSubscribers(c echo.Context) error {
|
||||
|
||||
// Get the batched export iterator.
|
||||
query := sanitizeSQLExp(c.FormValue("query"))
|
||||
exp, err := app.core.ExportSubscribers(query, subIDs, listIDs, subStatus, app.constants.DBBatchSize)
|
||||
exp, err := h.app.core.ExportSubscribers(query, subIDs, listIDs, subStatus, h.app.constants.DBBatchSize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
h = c.Response().Header()
|
||||
wr = csv.NewWriter(c.Response())
|
||||
hdr = c.Response().Header()
|
||||
wr = csv.NewWriter(c.Response())
|
||||
)
|
||||
|
||||
h.Set(echo.HeaderContentType, echo.MIMEOctetStream)
|
||||
h.Set("Content-type", "text/csv")
|
||||
h.Set(echo.HeaderContentDisposition, "attachment; filename="+"subscribers.csv")
|
||||
h.Set("Content-Transfer-Encoding", "binary")
|
||||
h.Set("Cache-Control", "no-cache")
|
||||
hdr.Set(echo.HeaderContentType, echo.MIMEOctetStream)
|
||||
hdr.Set("Content-type", "text/csv")
|
||||
hdr.Set(echo.HeaderContentDisposition, "attachment; filename="+"subscribers.csv")
|
||||
hdr.Set("Content-Transfer-Encoding", "binary")
|
||||
hdr.Set("Cache-Control", "no-cache")
|
||||
wr.Write([]string{"uuid", "email", "name", "attributes", "status", "created_at", "updated_at"})
|
||||
|
||||
loop:
|
||||
@@ -169,7 +163,7 @@ loop:
|
||||
for _, r := range out {
|
||||
if err = wr.Write([]string{r.UUID, r.Email, r.Name, r.Attribs, r.Status,
|
||||
r.CreatedAt.Time.String(), r.UpdatedAt.Time.String()}); err != nil {
|
||||
app.log.Printf("error streaming CSV export: %v", err)
|
||||
h.app.log.Printf("error streaming CSV export: %v", err)
|
||||
break loop
|
||||
}
|
||||
}
|
||||
@@ -181,12 +175,10 @@ loop:
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCreateSubscriber handles the creation of a new subscriber.
|
||||
func handleCreateSubscriber(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// CreateSubscriber handles the creation of a new subscriber.
|
||||
func (h *Handlers) CreateSubscriber(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Get and validate fields.
|
||||
var req subimporter.SubReq
|
||||
@@ -195,7 +187,7 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Validate fields.
|
||||
req, err := app.importer.ValidateFields(req)
|
||||
req, err := h.app.importer.ValidateFields(req)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -204,7 +196,7 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||
listIDs := user.FilterListsByPerm(auth.PermTypeManage, req.Lists)
|
||||
|
||||
// Insert the subscriber into the DB.
|
||||
sub, _, err := app.core.InsertSubscriber(req.Subscriber, listIDs, nil, req.PreconfirmSubs)
|
||||
sub, _, err := h.app.core.InsertSubscriber(req.Subscriber, listIDs, nil, req.PreconfirmSubs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -212,16 +204,14 @@ func handleCreateSubscriber(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{sub})
|
||||
}
|
||||
|
||||
// handleUpdateSubscriber handles modification of a subscriber.
|
||||
func handleUpdateSubscriber(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// UpdateSubscriber handles modification of a subscriber.
|
||||
func (h *Handlers) UpdateSubscriber(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Get and validate fields.
|
||||
@@ -235,21 +225,21 @@ func handleUpdateSubscriber(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Sanitize and validate the email field.
|
||||
if em, err := app.importer.SanitizeEmail(req.Email); err != nil {
|
||||
if em, err := h.app.importer.SanitizeEmail(req.Email); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
req.Email = em
|
||||
}
|
||||
|
||||
if req.Name != "" && !strHasLen(req.Name, 1, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.invalidName"))
|
||||
}
|
||||
|
||||
// Filter lists against the current user's permitted lists.
|
||||
listIDs := user.FilterListsByPerm(auth.PermTypeManage, req.Lists)
|
||||
|
||||
// Update the subscriber in the DB.
|
||||
out, _, err := app.core.UpdateSubscriberWithLists(id, req.Subscriber, listIDs, nil, req.PreconfirmSubs, true)
|
||||
out, _, err := h.app.core.UpdateSubscriberWithLists(id, req.Subscriber, listIDs, nil, req.PreconfirmSubs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -257,38 +247,30 @@ func handleUpdateSubscriber(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
||||
func handleSubscriberSendOptin(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// SubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
||||
func (h *Handlers) SubscriberSendOptin(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Fetch the subscriber.
|
||||
out, err := app.core.GetSubscriber(id, "", "")
|
||||
out, err := h.app.core.GetSubscriber(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Trigger the opt-in confirmation e-mail hook.
|
||||
if _, err := optinConfirmHook(app)(out, nil); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("subscribers.errorSendingOptin"))
|
||||
if _, err := optinConfirmHook(h.app)(out, nil); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, h.app.i18n.T("subscribers.errorSendingOptin"))
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleBlocklistSubscribers handles the blocklisting of one or more subscribers.
|
||||
// BlocklistSubscribers handles the blocklisting of one or more subscribers.
|
||||
// It takes either an ID in the URI, or a list of IDs in the request body.
|
||||
func handleBlocklistSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) BlocklistSubscribers(c echo.Context) error {
|
||||
// Is it a /:id call?
|
||||
var (
|
||||
subIDs []int
|
||||
@@ -297,7 +279,7 @@ func handleBlocklistSubscribers(c echo.Context) error {
|
||||
if pID != "" {
|
||||
id, _ := strconv.Atoi(pID)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
subIDs = append(subIDs, id)
|
||||
@@ -306,7 +288,7 @@ func handleBlocklistSubscribers(c echo.Context) error {
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(req.SubscriberIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
@@ -317,21 +299,19 @@ func handleBlocklistSubscribers(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Update the subscribers in the DB.
|
||||
if err := app.core.BlocklistSubscribers(subIDs); err != nil {
|
||||
if err := h.app.core.BlocklistSubscribers(subIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleManageSubscriberLists handles bulk addition or removal of subscribers
|
||||
// ManageSubscriberLists handles bulk addition or removal of subscribers
|
||||
// from or to one or more target lists.
|
||||
// It takes either an ID in the URI, or a list of IDs in the request body.
|
||||
func handleManageSubscriberLists(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) ManageSubscriberLists(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Is it an /:id call?
|
||||
var (
|
||||
@@ -341,7 +321,7 @@ func handleManageSubscriberLists(c echo.Context) error {
|
||||
if pID != "" {
|
||||
id, _ := strconv.Atoi(pID)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
subIDs = append(subIDs, id)
|
||||
}
|
||||
@@ -349,16 +329,16 @@ func handleManageSubscriberLists(c echo.Context) error {
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(req.SubscriberIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.errorNoIDs"))
|
||||
}
|
||||
if len(subIDs) == 0 {
|
||||
subIDs = req.SubscriberIDs
|
||||
}
|
||||
if len(req.TargetListIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.errorNoListsGiven"))
|
||||
}
|
||||
|
||||
// Filter lists against the current user's permitted lists.
|
||||
@@ -368,13 +348,13 @@ func handleManageSubscriberLists(c echo.Context) error {
|
||||
var err error
|
||||
switch req.Action {
|
||||
case "add":
|
||||
err = app.core.AddSubscriptions(subIDs, listIDs, req.Status)
|
||||
err = h.app.core.AddSubscriptions(subIDs, listIDs, req.Status)
|
||||
case "remove":
|
||||
err = app.core.DeleteSubscriptions(subIDs, listIDs)
|
||||
err = h.app.core.DeleteSubscriptions(subIDs, listIDs)
|
||||
case "unsubscribe":
|
||||
err = app.core.UnsubscribeLists(subIDs, listIDs, nil)
|
||||
err = h.app.core.UnsubscribeLists(subIDs, listIDs, nil)
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.invalidAction"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -384,13 +364,9 @@ func handleManageSubscriberLists(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleDeleteSubscribers handles subscriber deletion.
|
||||
// DeleteSubscribers handles subscriber deletion.
|
||||
// It takes either an ID in the URI, or a list of IDs in the request body.
|
||||
func handleDeleteSubscribers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) DeleteSubscribers(c echo.Context) error {
|
||||
// Is it an /:id call?
|
||||
var (
|
||||
pID = c.Param("id")
|
||||
@@ -399,7 +375,7 @@ func handleDeleteSubscribers(c echo.Context) error {
|
||||
if pID != "" {
|
||||
id, _ := strconv.Atoi(pID)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
subIDs = append(subIDs, id)
|
||||
} else {
|
||||
@@ -407,29 +383,25 @@ func handleDeleteSubscribers(c echo.Context) error {
|
||||
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(i) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.errorNoIDs"))
|
||||
}
|
||||
subIDs = i
|
||||
}
|
||||
|
||||
// Delete the subscribers from the DB.
|
||||
if err := app.core.DeleteSubscribers(subIDs, nil); err != nil {
|
||||
if err := h.app.core.DeleteSubscribers(subIDs, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleDeleteSubscribersByQuery bulk deletes based on an
|
||||
// DeleteSubscribersByQuery bulk deletes based on an
|
||||
// arbitrary SQL expression.
|
||||
func handleDeleteSubscribersByQuery(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) DeleteSubscribersByQuery(c echo.Context) error {
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
@@ -439,48 +411,42 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
|
||||
// If the "all" flag is set, ignore any subquery that may be present.
|
||||
req.Query = ""
|
||||
} else if req.Query == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "query"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "query"))
|
||||
}
|
||||
|
||||
// Delete the subscribers from the DB.
|
||||
if err := app.core.DeleteSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
|
||||
if err := h.app.core.DeleteSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleBlocklistSubscribersByQuery bulk blocklists subscribers
|
||||
// BlocklistSubscribersByQuery bulk blocklists subscribers
|
||||
// based on an arbitrary SQL expression.
|
||||
func handleBlocklistSubscribersByQuery(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
func (h *Handlers) BlocklistSubscribersByQuery(c echo.Context) error {
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Query == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "query"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "query"))
|
||||
}
|
||||
|
||||
// Update the subscribers in the DB.
|
||||
if err := app.core.BlocklistSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
|
||||
if err := h.app.core.BlocklistSubscribersByQuery(req.Query, req.ListIDs, req.SubscriptionStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleManageSubscriberListsByQuery bulk adds/removes/unsubscribes subscribers
|
||||
// ManageSubscriberListsByQuery bulk adds/removes/unsubscribes subscribers
|
||||
// from one or more lists based on an arbitrary SQL expression.
|
||||
func handleManageSubscriberListsByQuery(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
func (h *Handlers) ManageSubscriberListsByQuery(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
@@ -488,7 +454,7 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
|
||||
}
|
||||
if len(req.TargetListIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.T("subscribers.errorNoListsGiven"))
|
||||
h.app.i18n.T("subscribers.errorNoListsGiven"))
|
||||
}
|
||||
|
||||
// Filter lists against the current user's permitted lists.
|
||||
@@ -499,13 +465,13 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
|
||||
var err error
|
||||
switch req.Action {
|
||||
case "add":
|
||||
err = app.core.AddSubscriptionsByQuery(req.Query, sourceListIDs, targetListIDs, req.Status, req.SubscriptionStatus)
|
||||
err = h.app.core.AddSubscriptionsByQuery(req.Query, sourceListIDs, targetListIDs, req.Status, req.SubscriptionStatus)
|
||||
case "remove":
|
||||
err = app.core.DeleteSubscriptionsByQuery(req.Query, sourceListIDs, targetListIDs, req.SubscriptionStatus)
|
||||
err = h.app.core.DeleteSubscriptionsByQuery(req.Query, sourceListIDs, targetListIDs, req.SubscriptionStatus)
|
||||
case "unsubscribe":
|
||||
err = app.core.UnsubscribeListsByQuery(req.Query, sourceListIDs, targetListIDs, req.SubscriptionStatus)
|
||||
err = h.app.core.UnsubscribeListsByQuery(req.Query, sourceListIDs, targetListIDs, req.SubscriptionStatus)
|
||||
default:
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("subscribers.invalidAction"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -515,48 +481,39 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleDeleteSubscriberBounces deletes all the bounces on a subscriber.
|
||||
func handleDeleteSubscriberBounces(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteSubscriberBounces deletes all the bounces on a subscriber.
|
||||
func (h *Handlers) DeleteSubscriberBounces(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the bounces from the DB.
|
||||
if err := app.core.DeleteSubscriberBounces(id, ""); err != nil {
|
||||
if err := h.app.core.DeleteSubscriberBounces(id, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleExportSubscriberData pulls the subscriber's profile,
|
||||
// ExportSubscriberData pulls the subscriber's profile,
|
||||
// list subscriptions, campaign views and clicks and produces
|
||||
// a JSON report. This is a privacy feature and depends on the
|
||||
// configuration in app.Constants.Privacy.
|
||||
func handleExportSubscriberData(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
pID = c.Param("id")
|
||||
)
|
||||
|
||||
id, _ := strconv.Atoi(pID)
|
||||
// configuration in h.app.Constants.Privacy.
|
||||
func (h *Handlers) ExportSubscriberData(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Get the subscriber's data. A single query that gets the profile,
|
||||
// list subscriptions, campaign views, and link clicks. Names of
|
||||
// private lists are replaced with "Private list".
|
||||
_, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app)
|
||||
_, b, err := h.exportSubscriberData(id, "", h.app.constants.Privacy.Exportable)
|
||||
if err != nil {
|
||||
app.log.Printf("error exporting subscriber data: %s", err)
|
||||
h.app.log.Printf("error exporting subscriber data: %s", err)
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Set headers to force the browser to prompt for download.
|
||||
@@ -569,8 +526,8 @@ func handleExportSubscriberData(c echo.Context) error {
|
||||
// subscriptions, campaign_views, link_clicks (if they're enabled in the config)
|
||||
// and returns a formatted, indented JSON payload. Either takes a numeric id
|
||||
// and an empty subUUID or takes 0 and a string subUUID.
|
||||
func exportSubscriberData(id int, subUUID string, exportables map[string]bool, app *App) (models.SubscriberExportProfile, []byte, error) {
|
||||
data, err := app.core.GetSubscriberProfileForExport(id, subUUID)
|
||||
func (h *Handlers) exportSubscriberData(id int, subUUID string, exportables map[string]bool) (models.SubscriberExportProfile, []byte, error) {
|
||||
data, err := h.app.core.GetSubscriberProfileForExport(id, subUUID)
|
||||
if err != nil {
|
||||
return data, nil, err
|
||||
}
|
||||
@@ -592,13 +549,116 @@ func exportSubscriberData(id int, subUUID string, exportables map[string]bool, a
|
||||
// Marshal the data into an indented payload.
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
app.log.Printf("error marshalling subscriber export data: %v", err)
|
||||
h.app.log.Printf("error marshalling subscriber export data: %v", err)
|
||||
return data, nil, err
|
||||
}
|
||||
|
||||
return data, b, nil
|
||||
}
|
||||
|
||||
// optinConfirmHook returns an enclosed callback that sends optin confirmation e-mails.
|
||||
// This is plugged into the 'core' package to send optin confirmations when a new subscriber is
|
||||
// created via `core.CreateSubscriber()`.
|
||||
func optinConfirmHook(app *App) func(sub models.Subscriber, listIDs []int) (int, error) {
|
||||
return func(sub models.Subscriber, listIDs []int) (int, error) {
|
||||
lists, err := app.core.GetSubscriberLists(sub.ID, "", listIDs, nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// None.
|
||||
if len(lists) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var (
|
||||
out = subOptin{Subscriber: sub, Lists: lists}
|
||||
qListIDs = url.Values{}
|
||||
)
|
||||
|
||||
// Construct the opt-in URL with list IDs.
|
||||
for _, l := range out.Lists {
|
||||
qListIDs.Add("l", l.UUID)
|
||||
}
|
||||
out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
|
||||
out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
||||
|
||||
// Unsub headers.
|
||||
hdr := textproto.MIMEHeader{}
|
||||
hdr.Set(models.EmailHeaderSubscriberUUID, sub.UUID)
|
||||
|
||||
// Attach List-Unsubscribe headers?
|
||||
if app.constants.Privacy.UnsubHeader {
|
||||
unsubURL := fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
||||
hdr.Set("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
|
||||
hdr.Set("List-Unsubscribe", `<`+unsubURL+`>`)
|
||||
}
|
||||
|
||||
// Send the e-mail.
|
||||
if err := app.sendNotification([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out, hdr); err != nil {
|
||||
app.log.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(lists), nil
|
||||
}
|
||||
}
|
||||
|
||||
// hasSubPerm checks whether the current user has permission to access the given list
|
||||
// of subscriber IDs.
|
||||
func (h *Handlers) hasSubPerm(u auth.User, subIDs []int) error {
|
||||
allPerm, listIDs := u.GetPermittedLists(auth.PermTypeGet | auth.PermTypeManage)
|
||||
|
||||
// User has blanket get_all|manage_all permission.
|
||||
if allPerm {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check whether the subscribers have the list IDs permitted to the user.
|
||||
res, err := h.app.core.HasSubscriberLists(subIDs, listIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, has := range res {
|
||||
if !has {
|
||||
return echo.NewHTTPError(http.StatusForbidden, h.app.i18n.Ts("globals.messages.permissionDenied", "name", fmt.Sprintf("subscriber: %d", id)))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterListQueryByPerm filters the list IDs in the query params and returns the list IDs to which the user has access.
|
||||
func (h *Handlers) filterListQueryByPerm(param string, qp url.Values, user auth.User) ([]int, error) {
|
||||
var listIDs []int
|
||||
|
||||
// If there are incoming list query params, filter them by permission.
|
||||
if qp.Has(param) {
|
||||
ids, err := getQueryInts(param, qp)
|
||||
if err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
listIDs = user.FilterListsByPerm(auth.PermTypeGet|auth.PermTypeManage, ids)
|
||||
}
|
||||
|
||||
// There are no incoming params. If the user doesn't have permission to get all subscribers,
|
||||
// filter by the lists they have access to.
|
||||
if len(listIDs) == 0 {
|
||||
if _, ok := user.PermissionsMap[auth.PermSubscribersGetAll]; !ok {
|
||||
if len(user.GetListIDs) > 0 {
|
||||
listIDs = user.GetListIDs
|
||||
} else {
|
||||
// User doesn't have access to any lists.
|
||||
listIDs = []int{-1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listIDs, nil
|
||||
}
|
||||
|
||||
// sanitizeSQLExp does basic sanitisation on arbitrary
|
||||
// SQL query expressions coming from the frontend.
|
||||
func sanitizeSQLExp(q string) string {
|
||||
@@ -633,106 +693,3 @@ func getQueryInts(param string, qp url.Values) ([]int, error) {
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// optinConfirmHook returns an enclosed callback that sends optin confirmation e-mails.
|
||||
// This is plugged into the 'core' package to send optin confirmations when a new subscriber is
|
||||
// created via `core.CreateSubscriber()`.
|
||||
func optinConfirmHook(app *App) func(sub models.Subscriber, listIDs []int) (int, error) {
|
||||
return func(sub models.Subscriber, listIDs []int) (int, error) {
|
||||
lists, err := app.core.GetSubscriberLists(sub.ID, "", listIDs, nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// None.
|
||||
if len(lists) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
var (
|
||||
out = subOptin{Subscriber: sub, Lists: lists}
|
||||
qListIDs = url.Values{}
|
||||
)
|
||||
|
||||
// Construct the opt-in URL with list IDs.
|
||||
for _, l := range out.Lists {
|
||||
qListIDs.Add("l", l.UUID)
|
||||
}
|
||||
out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
|
||||
out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
||||
|
||||
// Unsub headers.
|
||||
h := textproto.MIMEHeader{}
|
||||
h.Set(models.EmailHeaderSubscriberUUID, sub.UUID)
|
||||
|
||||
// Attach List-Unsubscribe headers?
|
||||
if app.constants.Privacy.UnsubHeader {
|
||||
unsubURL := fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
||||
h.Set("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
|
||||
h.Set("List-Unsubscribe", `<`+unsubURL+`>`)
|
||||
}
|
||||
|
||||
// Send the e-mail.
|
||||
if err := app.sendNotification([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out, h); err != nil {
|
||||
app.log.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err)
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(lists), nil
|
||||
}
|
||||
}
|
||||
|
||||
// hasSubPerm checks whether the current user has permission to access the given list
|
||||
// of subscriber IDs.
|
||||
func hasSubPerm(u auth.User, subIDs []int, app *App) error {
|
||||
allPerm, listIDs := u.GetPermittedLists(auth.PermTypeGet | auth.PermTypeManage)
|
||||
|
||||
// User has blanket get_all|manage_all permission.
|
||||
if allPerm {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check whether the subscribers have the list IDs permitted to the user.
|
||||
res, err := app.core.HasSubscriberLists(subIDs, listIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, has := range res {
|
||||
if !has {
|
||||
return echo.NewHTTPError(http.StatusForbidden, app.i18n.Ts("globals.messages.permissionDenied", "name", fmt.Sprintf("subscriber: %d", id)))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterListQueryByPerm filters the list IDs in the query params and returns the list IDs to which the user has access.
|
||||
func filterListQueryByPerm(param string, qp url.Values, user auth.User, app *App) ([]int, error) {
|
||||
var listIDs []int
|
||||
|
||||
// If there are incoming list query params, filter them by permission.
|
||||
if qp.Has(param) {
|
||||
ids, err := getQueryInts(param, qp)
|
||||
if err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
listIDs = user.FilterListsByPerm(auth.PermTypeGet|auth.PermTypeManage, ids)
|
||||
}
|
||||
|
||||
// There are no incoming params. If the user doesn't have permission to get all subscribers,
|
||||
// filter by the lists they have access to.
|
||||
if len(listIDs) == 0 {
|
||||
if _, ok := user.PermissionsMap[auth.PermSubscribersGetAll]; !ok {
|
||||
if len(user.GetListIDs) > 0 {
|
||||
listIDs = user.GetListIDs
|
||||
} else {
|
||||
// User doesn't have access to any lists.
|
||||
listIDs = []int{-1}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return listIDs, nil
|
||||
}
|
||||
|
||||
114
cmd/templates.go
114
cmd/templates.go
@@ -31,19 +31,15 @@ var (
|
||||
regexpTplTag = regexp.MustCompile(`{{(\s+)?template\s+?"content"(\s+)?\.(\s+)?}}`)
|
||||
)
|
||||
|
||||
// handleGetTemplates handles retrieval of templates.
|
||||
func handleGetTemplates(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetTemplates handles retrieval of templates.
|
||||
func (h *Handlers) GetTemplates(c echo.Context) error {
|
||||
// Fetch one list.
|
||||
var (
|
||||
id, _ = strconv.Atoi(c.Param("id"))
|
||||
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
|
||||
)
|
||||
if id > 0 {
|
||||
out, err := app.core.GetTemplate(id, noBody)
|
||||
out, err := h.app.core.GetTemplate(id, noBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -52,7 +48,7 @@ func handleGetTemplates(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Fetch templates from the DB (with or without body).
|
||||
out, err := app.core.GetTemplates("", noBody)
|
||||
out, err := h.app.core.GetTemplates("", noBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,12 +56,8 @@ func handleGetTemplates(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handlePreviewTemplate renders the HTML preview of a template.
|
||||
func handlePreviewTemplate(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// PreviewTemplate renders the HTML preview of a template.
|
||||
func (h *Handlers) PreviewTemplate(c echo.Context) error {
|
||||
tpl := models.Template{
|
||||
Type: c.FormValue("template_type"),
|
||||
Body: c.FormValue("body"),
|
||||
@@ -79,16 +71,16 @@ func handlePreviewTemplate(c echo.Context) error {
|
||||
|
||||
if tpl.Type == models.TemplateTypeCampaign && !regexpTplTag.MatchString(tpl.Body) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.placeholderHelp", "placeholder", tplTag))
|
||||
h.app.i18n.Ts("templates.placeholderHelp", "placeholder", tplTag))
|
||||
}
|
||||
} else {
|
||||
// There is no body. Fetch the template from the DB.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
t, err := app.core.GetTemplate(id, false)
|
||||
t, err := h.app.core.GetTemplate(id, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -101,28 +93,28 @@ func handlePreviewTemplate(c echo.Context) error {
|
||||
if tpl.Type == models.TemplateTypeCampaign {
|
||||
camp := models.Campaign{
|
||||
UUID: dummyUUID,
|
||||
Name: app.i18n.T("templates.dummyName"),
|
||||
Subject: app.i18n.T("templates.dummySubject"),
|
||||
Name: h.app.i18n.T("templates.dummyName"),
|
||||
Subject: h.app.i18n.T("templates.dummySubject"),
|
||||
FromEmail: "dummy-campaign@listmonk.app",
|
||||
TemplateBody: tpl.Body,
|
||||
Body: dummyTpl,
|
||||
}
|
||||
|
||||
if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
if err := camp.CompileTemplate(h.app.manager.TemplateFuncs(&camp)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Render the message body.
|
||||
msg, err := app.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
msg, err := h.app.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
h.app.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
}
|
||||
out = msg.Body()
|
||||
} else {
|
||||
// Compile transactional template.
|
||||
if err := tpl.Compile(app.manager.GenericTemplateFuncs()); err != nil {
|
||||
if err := tpl.Compile(h.app.manager.GenericTemplateFuncs()); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
@@ -140,17 +132,13 @@ func handlePreviewTemplate(c echo.Context) error {
|
||||
return c.HTML(http.StatusOK, string(out))
|
||||
}
|
||||
|
||||
// handleCreateTemplate handles template creation.
|
||||
func handleCreateTemplate(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CreateTemplate handles template creation.
|
||||
func (h *Handlers) CreateTemplate(c echo.Context) error {
|
||||
var o models.Template
|
||||
if err := c.Bind(&o); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTemplate(o, app); err != nil {
|
||||
if err := h.validateTemplate(o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -159,9 +147,9 @@ func handleCreateTemplate(c echo.Context) error {
|
||||
var funcs template.FuncMap
|
||||
if o.Type == models.TemplateTypeCampaign {
|
||||
o.Subject = ""
|
||||
funcs = app.manager.TemplateFuncs(nil)
|
||||
funcs = h.app.manager.TemplateFuncs(nil)
|
||||
} else {
|
||||
funcs = app.manager.GenericTemplateFuncs()
|
||||
funcs = h.app.manager.GenericTemplateFuncs()
|
||||
}
|
||||
|
||||
// Compile the template and validate.
|
||||
@@ -170,7 +158,7 @@ func handleCreateTemplate(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Create the template the in the DB.
|
||||
out, err := app.core.CreateTemplate(o.Name, o.Type, o.Subject, []byte(o.Body))
|
||||
out, err := h.app.core.CreateTemplate(o.Name, o.Type, o.Subject, []byte(o.Body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -178,28 +166,24 @@ func handleCreateTemplate(c echo.Context) error {
|
||||
// If it's a transactional template, cache it in the manager
|
||||
// to be used for arbitrary incoming tx message pushes.
|
||||
if o.Type == models.TemplateTypeTx {
|
||||
app.manager.CacheTpl(out.ID, &o)
|
||||
h.app.manager.CacheTpl(out.ID, &o)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleUpdateTemplate handles template modification.
|
||||
func handleUpdateTemplate(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UpdateTemplate handles template modification.
|
||||
func (h *Handlers) UpdateTemplate(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
var o models.Template
|
||||
if err := c.Bind(&o); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateTemplate(o, app); err != nil {
|
||||
if err := h.validateTemplate(o); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -208,9 +192,9 @@ func handleUpdateTemplate(c echo.Context) error {
|
||||
var funcs template.FuncMap
|
||||
if o.Type == models.TemplateTypeCampaign {
|
||||
o.Subject = ""
|
||||
funcs = app.manager.TemplateFuncs(nil)
|
||||
funcs = h.app.manager.TemplateFuncs(nil)
|
||||
} else {
|
||||
funcs = app.manager.GenericTemplateFuncs()
|
||||
funcs = h.app.manager.GenericTemplateFuncs()
|
||||
}
|
||||
|
||||
// Compile the template and validate.
|
||||
@@ -219,75 +203,67 @@ func handleUpdateTemplate(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Update the template in the DB.
|
||||
out, err := app.core.UpdateTemplate(id, o.Name, o.Subject, []byte(o.Body))
|
||||
out, err := h.app.core.UpdateTemplate(id, o.Name, o.Subject, []byte(o.Body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If it's a transactional template, cache it.
|
||||
if out.Type == models.TemplateTypeTx {
|
||||
app.manager.CacheTpl(out.ID, &o)
|
||||
h.app.manager.CacheTpl(out.ID, &o)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
|
||||
}
|
||||
|
||||
// handleTemplateSetDefault handles template modification.
|
||||
func handleTemplateSetDefault(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// TemplateSetDefault handles template modification.
|
||||
func (h *Handlers) TemplateSetDefault(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Update the template in the DB.
|
||||
if err := app.core.SetDefaultTemplate(id); err != nil {
|
||||
if err := h.app.core.SetDefaultTemplate(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return handleGetTemplates(c)
|
||||
return h.GetTemplates(c)
|
||||
}
|
||||
|
||||
// handleDeleteTemplate handles template deletion.
|
||||
func handleDeleteTemplate(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteTemplate handles template deletion.
|
||||
func (h *Handlers) DeleteTemplate(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the template from the DB.
|
||||
if err := app.core.DeleteTemplate(id); err != nil {
|
||||
if err := h.app.core.DeleteTemplate(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete cached template.
|
||||
app.manager.DeleteTpl(id)
|
||||
h.app.manager.DeleteTpl(id)
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// compileTemplate validates template fields.
|
||||
func validateTemplate(o models.Template, app *App) error {
|
||||
func (h *Handlers) validateTemplate(o models.Template) error {
|
||||
if !strHasLen(o.Name, 1, stdInputMaxLen) {
|
||||
return errors.New(app.i18n.T("campaigns.fieldInvalidName"))
|
||||
return errors.New(h.app.i18n.T("campaigns.fieldInvalidName"))
|
||||
}
|
||||
|
||||
if o.Type == models.TemplateTypeCampaign && !regexpTplTag.MatchString(o.Body) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("templates.placeholderHelp", "placeholder", tplTag))
|
||||
h.app.i18n.Ts("templates.placeholderHelp", "placeholder", tplTag))
|
||||
}
|
||||
|
||||
if o.Type == models.TemplateTypeTx && strings.TrimSpace(o.Subject) == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.missingFields", "name", "subject"))
|
||||
h.app.i18n.Ts("globals.messages.missingFields", "name", "subject"))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
48
cmd/tx.go
48
cmd/tx.go
@@ -13,12 +13,8 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// handleSendTxMessage handles the sending of a transactional message.
|
||||
func handleSendTxMessage(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// SendTxMessage handles the sending of a transactional message.
|
||||
func (h *Handlers) SendTxMessage(c echo.Context) error {
|
||||
var m models.TxMessage
|
||||
|
||||
// If it's a multipart form, there may be file attachments.
|
||||
@@ -26,18 +22,18 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", err.Error()))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", err.Error()))
|
||||
}
|
||||
|
||||
data, ok := form.Value["data"]
|
||||
if !ok || len(data) != 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "data"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "data"))
|
||||
}
|
||||
|
||||
// Parse the JSON data.
|
||||
if err := json.Unmarshal([]byte(data[0]), &m); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("data: %s", err.Error())))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("data: %s", err.Error())))
|
||||
}
|
||||
|
||||
// Attach files.
|
||||
@@ -45,14 +41,14 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
file, err := f.Open()
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error())))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error())))
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
b, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error())))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("file: %s", err.Error())))
|
||||
}
|
||||
|
||||
m.Attachments = append(m.Attachments, models.Attachment{
|
||||
@@ -67,17 +63,17 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Validate fields.
|
||||
if r, err := validateTxMessage(m, app); err != nil {
|
||||
if r, err := h.validateTxMessage(m); err != nil {
|
||||
return err
|
||||
} else {
|
||||
m = r
|
||||
}
|
||||
|
||||
// Get the cached tx template.
|
||||
tpl, err := app.manager.GetTpl(m.TemplateID)
|
||||
tpl, err := h.app.manager.GetTpl(m.TemplateID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.notFound", "name", fmt.Sprintf("template %d", m.TemplateID)))
|
||||
h.app.i18n.Ts("globals.messages.notFound", "name", fmt.Sprintf("template %d", m.TemplateID)))
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -103,7 +99,7 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get the subscriber.
|
||||
sub, err := app.core.GetSubscriber(subID, "", subEmail)
|
||||
sub, err := h.app.core.GetSubscriber(subID, "", subEmail)
|
||||
if err != nil {
|
||||
// If the subscriber is not found, log that error and move on without halting on the list.
|
||||
if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
|
||||
@@ -117,7 +113,7 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
// Render the message.
|
||||
if err := m.Render(sub, tpl); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.errorFetching", "name"))
|
||||
h.app.i18n.Ts("globals.messages.errorFetching", "name"))
|
||||
}
|
||||
|
||||
// Prepare the final message.
|
||||
@@ -147,8 +143,8 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := app.manager.PushMessage(msg); err != nil {
|
||||
app.log.Printf("error sending message (%s): %v", msg.Subject, err)
|
||||
if err := h.app.manager.PushMessage(msg); err != nil {
|
||||
h.app.log.Printf("error sending message (%s): %v", msg.Subject, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -161,14 +157,14 @@ func handleSendTxMessage(c echo.Context) error {
|
||||
}
|
||||
|
||||
// validateTxMessage validates the tx message fields.
|
||||
func validateTxMessage(m models.TxMessage, app *App) (models.TxMessage, error) {
|
||||
func (h *Handlers) validateTxMessage(m models.TxMessage) (models.TxMessage, error) {
|
||||
if len(m.SubscriberEmails) > 0 && m.SubscriberEmail != "" {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_email`"))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_email`"))
|
||||
}
|
||||
if len(m.SubscriberIDs) > 0 && m.SubscriberID != 0 {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_id`"))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", "do not send `subscriber_id`"))
|
||||
}
|
||||
|
||||
if m.SubscriberEmail != "" {
|
||||
@@ -181,12 +177,12 @@ func validateTxMessage(m models.TxMessage, app *App) (models.TxMessage, error) {
|
||||
|
||||
if (len(m.SubscriberEmails) == 0 && len(m.SubscriberIDs) == 0) || (len(m.SubscriberEmails) > 0 && len(m.SubscriberIDs) > 0) {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest,
|
||||
app.i18n.Ts("globals.messages.invalidFields", "name", "send subscriber_emails OR subscriber_ids"))
|
||||
h.app.i18n.Ts("globals.messages.invalidFields", "name", "send subscriber_emails OR subscriber_ids"))
|
||||
}
|
||||
|
||||
for n, email := range m.SubscriberEmails {
|
||||
if m.SubscriberEmail != "" {
|
||||
em, err := app.importer.SanitizeEmail(email)
|
||||
em, err := h.app.importer.SanitizeEmail(email)
|
||||
if err != nil {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
@@ -195,13 +191,13 @@ func validateTxMessage(m models.TxMessage, app *App) (models.TxMessage, error) {
|
||||
}
|
||||
|
||||
if m.FromEmail == "" {
|
||||
m.FromEmail = app.constants.FromEmail
|
||||
m.FromEmail = h.app.constants.FromEmail
|
||||
}
|
||||
|
||||
if m.Messenger == "" {
|
||||
m.Messenger = emailMsgr
|
||||
} else if !app.manager.HasMessenger(m.Messenger) {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", m.Messenger))
|
||||
} else if !h.app.manager.HasMessenger(m.Messenger) {
|
||||
return m, echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", m.Messenger))
|
||||
}
|
||||
|
||||
return m, nil
|
||||
|
||||
@@ -36,32 +36,32 @@ var reSemver = regexp.MustCompile(`-(.*)`)
|
||||
// checkUpdates is a blocking function that checks for updates to the app
|
||||
// at the given intervals. On detecting a new update (new semver), it
|
||||
// sets the global update status that renders a prompt on the UI.
|
||||
func checkUpdates(curVersion string, interval time.Duration, app *App) {
|
||||
func (a *App) checkUpdates(curVersion string, interval time.Duration) {
|
||||
// Strip -* suffix.
|
||||
curVersion = reSemver.ReplaceAllString(curVersion, "")
|
||||
|
||||
fnCheck := func() {
|
||||
resp, err := http.Get(updateCheckURL)
|
||||
if err != nil {
|
||||
app.log.Printf("error checking for remote update: %v", err)
|
||||
a.log.Printf("error checking for remote update: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
app.log.Printf("non 200 response on remote update check: %d", resp.StatusCode)
|
||||
a.log.Printf("non 200 response on remote update check: %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
app.log.Printf("error reading remote update payload: %v", err)
|
||||
a.log.Printf("error reading remote update payload: %v", err)
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var out AppUpdate
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
app.log.Printf("error unmarshalling remote update payload: %v", err)
|
||||
a.log.Printf("error unmarshalling remote update payload: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,13 +70,13 @@ func checkUpdates(curVersion string, interval time.Duration, app *App) {
|
||||
v := reSemver.ReplaceAllString(out.Update.ReleaseVersion, "")
|
||||
if semver.Compare(v, curVersion) > 0 {
|
||||
out.Update.IsNew = true
|
||||
app.log.Printf("new update %s found", out.Update.ReleaseVersion)
|
||||
a.log.Printf("new update %s found", out.Update.ReleaseVersion)
|
||||
}
|
||||
}
|
||||
|
||||
app.Lock()
|
||||
app.update = &out
|
||||
app.Unlock()
|
||||
a.Lock()
|
||||
a.update = &out
|
||||
a.Unlock()
|
||||
}
|
||||
|
||||
// Give a 15 minute buffer after app start in case the admin wants to disable
|
||||
|
||||
99
cmd/users.go
99
cmd/users.go
@@ -17,12 +17,8 @@ var (
|
||||
reUsername = regexp.MustCompile(`^[a-zA-Z0-9_\\-\\.]+$`)
|
||||
)
|
||||
|
||||
// handleGetUsers retrieves users.
|
||||
func handleGetUsers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// GetUsers retrieves users.
|
||||
func (h *Handlers) GetUsers(c echo.Context) error {
|
||||
var (
|
||||
single = false
|
||||
userID, _ = strconv.Atoi(c.Param("id"))
|
||||
@@ -33,7 +29,7 @@ func handleGetUsers(c echo.Context) error {
|
||||
|
||||
if single {
|
||||
// Get the user from the DB.
|
||||
out, err := app.core.GetUser(userID, "", "")
|
||||
out, err := h.app.core.GetUser(userID, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,7 +41,7 @@ func handleGetUsers(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Get all users from the DB.
|
||||
out, err := app.core.GetUsers()
|
||||
out, err := h.app.core.GetUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -58,12 +54,8 @@ func handleGetUsers(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// handleCreateUser handles user creation.
|
||||
func handleCreateUser(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// CreateUser handles user creation.
|
||||
func (h *Handlers) CreateUser(c echo.Context) error {
|
||||
var u = auth.User{}
|
||||
if err := c.Bind(&u); err != nil {
|
||||
return err
|
||||
@@ -75,18 +67,18 @@ func handleCreateUser(c echo.Context) error {
|
||||
|
||||
// Validate fields.
|
||||
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
if !reUsername.MatchString(u.Username) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
if u.Type != auth.UserTypeAPI {
|
||||
if !utils.ValidateEmail(email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
}
|
||||
if u.PasswordLogin {
|
||||
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +90,7 @@ func handleCreateUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Create the user in the DB.
|
||||
user, err := app.core.CreateUser(u)
|
||||
user, err := h.app.core.CreateUser(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -109,22 +101,18 @@ func handleCreateUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Cache the API token for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{user})
|
||||
}
|
||||
|
||||
// handleUpdateUser handles user modification.
|
||||
func handleUpdateUser(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// UpdateUser handles user modification.
|
||||
func (h *Handlers) UpdateUser(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Incoming params.
|
||||
@@ -139,31 +127,31 @@ func handleUpdateUser(c echo.Context) error {
|
||||
|
||||
// Validate fields.
|
||||
if !strHasLen(u.Username, 3, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
if !reUsername.MatchString(u.Username) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
|
||||
if u.Type != auth.UserTypeAPI {
|
||||
if !utils.ValidateEmail(email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
}
|
||||
|
||||
// Validate password if password login is enabled.
|
||||
if u.PasswordLogin && u.Password.String != "" {
|
||||
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
|
||||
if u.Password.String != "" {
|
||||
// If a password is sent, validate it before updating in the DB. If it's not set, leave the password in the DB untouched.
|
||||
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
} else {
|
||||
// Get the user from the DB.
|
||||
user, err := app.core.GetUser(id, "", "")
|
||||
user, err := h.app.core.GetUser(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -171,7 +159,7 @@ func handleUpdateUser(c echo.Context) error {
|
||||
// If password login is enabled, but there's no password in the DB and there's no incoming
|
||||
// password, throw an error.
|
||||
if !user.HasPassword {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,7 +173,7 @@ func handleUpdateUser(c echo.Context) error {
|
||||
}
|
||||
|
||||
// Update the user in the DB.
|
||||
user, err := app.core.UpdateUser(id, u)
|
||||
user, err := h.app.core.UpdateUser(id, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,48 +182,43 @@ func handleUpdateUser(c echo.Context) error {
|
||||
user.Password = null.String{}
|
||||
|
||||
// Cache the API token for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{user})
|
||||
}
|
||||
|
||||
// handleDeleteUsers handles user deletion, either a single one (ID in the URI), or a list.
|
||||
func handleDeleteUsers(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
)
|
||||
|
||||
// DeleteUsers handles user deletion, either a single one (ID in the URI), or a list.
|
||||
func (h *Handlers) DeleteUsers(c echo.Context) error {
|
||||
var (
|
||||
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
ids []int
|
||||
)
|
||||
if id < 1 && len(ids) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
if id > 0 {
|
||||
ids = append(ids, int(id))
|
||||
}
|
||||
|
||||
// Delete the user(s) from the DB.
|
||||
if err := app.core.DeleteUsers(ids); err != nil {
|
||||
if err := h.app.core.DeleteUsers(ids); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache the API token for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(app.core, app.auth); err != nil {
|
||||
if _, err := cacheUsers(h.app.core, h.app.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// handleGetUserProfile fetches the uesr profile for the currently logged in user.
|
||||
func handleGetUserProfile(c echo.Context) error {
|
||||
var (
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// GetUserProfile fetches the uesr profile for the currently logged in user.
|
||||
func (h *Handlers) GetUserProfile(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Blank out the password hash before responding to the API.
|
||||
user.Password.String = ""
|
||||
@@ -244,12 +227,10 @@ func handleGetUserProfile(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, okResp{user})
|
||||
}
|
||||
|
||||
// handleUpdateUserProfile update's the current user's profile.
|
||||
func handleUpdateUserProfile(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
user = auth.GetUser(c)
|
||||
)
|
||||
// UpdateUserProfile update's the current user's profile.
|
||||
func (h *Handlers) UpdateUserProfile(c echo.Context) error {
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Incoming params.
|
||||
u := auth.User{}
|
||||
@@ -263,19 +244,19 @@ func handleUpdateUserProfile(c echo.Context) error {
|
||||
// Validate fields.
|
||||
if user.PasswordLogin {
|
||||
if !utils.ValidateEmail(email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
}
|
||||
u.Email = null.String{String: email, Valid: true}
|
||||
}
|
||||
|
||||
if u.PasswordLogin && u.Password.String != "" {
|
||||
if !strHasLen(u.Password.String, 8, stdInputMaxLen) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
return echo.NewHTTPError(http.StatusBadRequest, h.app.i18n.Ts("globals.messages.invalidFields", "name", "password"))
|
||||
}
|
||||
}
|
||||
|
||||
// Update the user in the DB.
|
||||
out, err := app.core.UpdateUserProfile(user.ID, u)
|
||||
out, err := h.app.core.UpdateUserProfile(user.ID, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user