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:
Kailash Nadh
2025-04-05 14:29:48 +05:30
parent 007f4de850
commit 00c858fc49
23 changed files with 1102 additions and 1476 deletions

View File

@@ -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})

View File

@@ -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{

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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})
}

View File

@@ -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.")
}

View File

@@ -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()})
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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})
}

View File

@@ -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"

View File

@@ -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
}

View File

@@ -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)))
}
}
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}