mirror of
https://github.com/knadh/listmonk.git
synced 2025-09-12 01:14:50 +08:00
Remove repetitive URL param :id
validation and simplify handlers.
This patch significantly cleans up clunky, repetitive, and pervasive validation logic across HTTP handlers. - Rather than dozens of handlers checking and using strconv to validate ID, the handlers with `:id` are now wrapped in a `hasID()` middleware that does the validation and sets an int `id` in the handler context that the wrapped handlers can now access with `getID()`. - Handlers that handled both single + multi resource requests (eg: GET `/api/lists`) with single/multiple id checking conditions are all now split into separate handlers, eg: `getList()`, `getLists()`.
This commit is contained in:
parent
e6d3aad0a1
commit
0826f401b7
10 changed files with 374 additions and 406 deletions
|
@ -11,27 +11,30 @@ import (
|
|||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// GetBounces handles retrieval of bounce records.
|
||||
func (a *App) GetBounces(c echo.Context) error {
|
||||
// GetBounce handles retrieval of a specific bounce record by ID.
|
||||
func (a *App) GetBounce(c echo.Context) error {
|
||||
// Fetch one bounce from the DB.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id > 0 {
|
||||
out, err := a.core.GetBounce(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
id := getID(c)
|
||||
out, err := a.core.GetBounce(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Query and fetch bounces from the DB.
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// GetBounces handles retrieval of bounce records.
|
||||
func (a *App) GetBounces(c echo.Context) error {
|
||||
var (
|
||||
pg = a.pg.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")
|
||||
|
||||
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
|
||||
// Query and fetch bounces from the DB.
|
||||
res, total, err := a.core.QueryBounces(campID, 0, source, orderBy, order, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -54,12 +57,8 @@ func (a *App) GetBounces(c echo.Context) error {
|
|||
|
||||
// GetSubscriberBounces retrieves a subscriber's bounce records.
|
||||
func (a *App) GetSubscriberBounces(c echo.Context) error {
|
||||
subID, _ := strconv.Atoi(c.Param("id"))
|
||||
if subID < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Query and fetch bounces from the DB.
|
||||
subID := getID(c)
|
||||
out, _, err := a.core.QueryBounces(0, subID, "", "", "", 0, 1000)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -68,32 +67,22 @@ func (a *App) GetSubscriberBounces(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// DeleteBounces handles bounce deletion, either a single one (ID in the URI), or a list.
|
||||
// DeleteBounces handles bounce deletion of a list.
|
||||
func (a *App) DeleteBounces(c echo.Context) error {
|
||||
// Is it an /:id call?
|
||||
var (
|
||||
all, _ = strconv.ParseBool(c.QueryParam("all"))
|
||||
idStr = c.Param("id")
|
||||
all, _ := strconv.ParseBool(c.QueryParam("all"))
|
||||
|
||||
ids = []int{}
|
||||
)
|
||||
if idStr != "" {
|
||||
id, _ := strconv.Atoi(idStr)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
ids = append(ids, id)
|
||||
} else if !all {
|
||||
var ids []int
|
||||
if !all {
|
||||
// There are multiple IDs in the query string.
|
||||
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
res, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidID", "error", err.Error()))
|
||||
}
|
||||
|
||||
if len(i) == 0 {
|
||||
if len(res) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidID"))
|
||||
}
|
||||
ids = i
|
||||
|
||||
ids = res
|
||||
}
|
||||
|
||||
// Delete bounces from the DB.
|
||||
|
@ -104,6 +93,17 @@ func (a *App) DeleteBounces(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteBounce handles bounce deletion of a single bounce record.
|
||||
func (a *App) DeleteBounce(c echo.Context) error {
|
||||
// Delete bounces from the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.DeleteBounces([]int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// BounceWebhook renders the HTML preview of a template.
|
||||
func (a *App) BounceWebhook(c echo.Context) error {
|
||||
// Read the request body instead of using c.Bind() to read to save the entire raw request as meta.
|
||||
|
|
|
@ -109,10 +109,8 @@ func (a *App) GetCampaigns(c echo.Context) error {
|
|||
|
||||
// GetCampaign handles retrieval of campaigns.
|
||||
func (a *App) GetCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
|
@ -136,10 +134,8 @@ func (a *App) GetCampaign(c echo.Context) error {
|
|||
|
||||
// PreviewCampaign renders the HTML preview of a campaign body.
|
||||
func (a *App) PreviewCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeGet, id, c); err != nil {
|
||||
|
@ -185,16 +181,12 @@ func (a *App) PreviewCampaign(c echo.Context) error {
|
|||
|
||||
// CampaignContent handles campaign content (body) format conversions.
|
||||
func (a *App) CampaignContent(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
var camp campContentReq
|
||||
if err := c.Bind(&camp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert formats, eg: markdown to HTML.
|
||||
out, err := camp.ConvertContent(camp.From, camp.To)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
|
@ -251,11 +243,8 @@ func (a *App) CreateCampaign(c echo.Context) error {
|
|||
// UpdateCampaign handles campaign modification.
|
||||
// Campaigns that are done cannot be modified.
|
||||
func (a *App) UpdateCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
|
@ -296,10 +285,8 @@ func (a *App) UpdateCampaign(c echo.Context) error {
|
|||
|
||||
// UpdateCampaignStatus handles campaign status modification.
|
||||
func (a *App) UpdateCampaignStatus(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
|
@ -329,8 +316,9 @@ func (a *App) UpdateCampaignStatus(c echo.Context) error {
|
|||
|
||||
// UpdateCampaignArchive handles campaign status modification.
|
||||
func (a *App) UpdateCampaignArchive(c echo.Context) error {
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if err := a.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -363,10 +351,8 @@ func (a *App) UpdateCampaignArchive(c echo.Context) error {
|
|||
// DeleteCampaign handles campaign deletion.
|
||||
// Only scheduled campaigns that have not started yet can be deleted.
|
||||
func (a *App) DeleteCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
|
@ -417,10 +403,8 @@ func (a *App) GetRunningCampaignStats(c echo.Context) error {
|
|||
// TestCampaign handles the sending of a campaign message to
|
||||
// arbitrary subscribers for testing.
|
||||
func (a *App) TestCampaign(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.errorID"))
|
||||
}
|
||||
// Get the campaign ID.
|
||||
id := getID(c)
|
||||
|
||||
// Check if the user has access to the campaign.
|
||||
if err := a.checkCampaignPerm(auth.PermTypeManage, id, c); err != nil {
|
||||
|
|
142
cmd/handlers.go
142
cmd/handlers.go
|
@ -6,6 +6,7 @@ import (
|
|||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/knadh/listmonk/internal/auth"
|
||||
"github.com/labstack/echo/v4"
|
||||
|
@ -105,24 +106,24 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
|||
g.GET("/api/about", a.GetAboutInfo)
|
||||
|
||||
g.GET("/api/subscribers", pm(a.QuerySubscribers, "subscribers:get_all", "subscribers:get"))
|
||||
g.GET("/api/subscribers/:id", pm(a.GetSubscriber, "subscribers:get_all", "subscribers:get"))
|
||||
g.GET("/api/subscribers/:id/export", pm(a.ExportSubscriberData, "subscribers:get_all", "subscribers:get"))
|
||||
g.GET("/api/subscribers/:id/bounces", pm(a.GetSubscriberBounces, "bounces:get"))
|
||||
g.DELETE("/api/subscribers/:id/bounces", pm(a.DeleteSubscriberBounces, "bounces:manage"))
|
||||
g.GET("/api/subscribers/:id", pm(hasID(a.GetSubscriber), "subscribers:get_all", "subscribers:get"))
|
||||
g.GET("/api/subscribers/:id/export", pm(hasID(a.ExportSubscriberData), "subscribers:get_all", "subscribers:get"))
|
||||
g.GET("/api/subscribers/:id/bounces", pm(hasID(a.GetSubscriberBounces), "bounces:get"))
|
||||
g.DELETE("/api/subscribers/:id/bounces", pm(hasID(a.DeleteSubscriberBounces), "bounces:manage"))
|
||||
g.POST("/api/subscribers", pm(a.CreateSubscriber, "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/:id", pm(a.UpdateSubscriber, "subscribers:manage"))
|
||||
g.POST("/api/subscribers/:id/optin", pm(a.SubscriberSendOptin, "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/:id", pm(hasID(a.UpdateSubscriber), "subscribers:manage"))
|
||||
g.POST("/api/subscribers/:id/optin", pm(hasID(a.SubscriberSendOptin), "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/blocklist", pm(a.BlocklistSubscribers, "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/:id/blocklist", pm(a.BlocklistSubscribers, "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/:id/blocklist", pm(hasID(a.BlocklistSubscriber), "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/lists/:id", pm(a.ManageSubscriberLists, "subscribers:manage"))
|
||||
g.PUT("/api/subscribers/lists", pm(a.ManageSubscriberLists, "subscribers:manage"))
|
||||
g.DELETE("/api/subscribers/:id", pm(a.DeleteSubscribers, "subscribers:manage"))
|
||||
g.DELETE("/api/subscribers/:id", pm(hasID(a.DeleteSubscriber), "subscribers:manage"))
|
||||
g.DELETE("/api/subscribers", pm(a.DeleteSubscribers, "subscribers:manage"))
|
||||
|
||||
g.GET("/api/bounces", pm(a.GetBounces, "bounces:get"))
|
||||
g.GET("/api/bounces/:id", pm(a.GetBounces, "bounces:get"))
|
||||
g.GET("/api/bounces/:id", pm(hasID(a.GetBounce), "bounces:get"))
|
||||
g.DELETE("/api/bounces", pm(a.DeleteBounces, "bounces:manage"))
|
||||
g.DELETE("/api/bounces/:id", pm(a.DeleteBounces, "bounces:manage"))
|
||||
g.DELETE("/api/bounces/:id", pm(hasID(a.DeleteBounce), "bounces:manage"))
|
||||
|
||||
// Subscriber operations based on arbitrary SQL queries.
|
||||
// These aren't very REST-like.
|
||||
|
@ -139,39 +140,39 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
|||
|
||||
// Individual list permissions are applied directly within handleGetLists.
|
||||
g.GET("/api/lists", a.GetLists)
|
||||
g.GET("/api/lists/:id", a.GetList)
|
||||
g.GET("/api/lists/:id", hasID(a.GetList))
|
||||
g.POST("/api/lists", pm(a.CreateList, "lists:manage_all"))
|
||||
g.PUT("/api/lists/:id", a.UpdateList)
|
||||
g.DELETE("/api/lists/:id", a.DeleteLists)
|
||||
g.PUT("/api/lists/:id", hasID(a.UpdateList))
|
||||
g.DELETE("/api/lists/:id", hasID(a.DeleteLists))
|
||||
|
||||
g.GET("/api/campaigns", pm(a.GetCampaigns, "campaigns:get_all", "campaigns:get"))
|
||||
g.GET("/api/campaigns/running/stats", pm(a.GetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
|
||||
g.GET("/api/campaigns/:id", pm(a.GetCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
g.GET("/api/campaigns/:id", pm(hasID(a.GetCampaign), "campaigns:get_all", "campaigns:get"))
|
||||
g.GET("/api/campaigns/analytics/:type", pm(a.GetCampaignViewAnalytics, "campaigns:get_analytics"))
|
||||
g.GET("/api/campaigns/:id/preview", pm(a.PreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/preview", pm(a.PreviewCampaign, "campaigns:get_all", "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/content", pm(a.CampaignContent, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.POST("/api/campaigns/:id/text", pm(a.PreviewCampaign, "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/test", pm(a.TestCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.GET("/api/campaigns/:id/preview", pm(hasID(a.PreviewCampaign), "campaigns:get_all", "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/preview", pm(hasID(a.PreviewCampaign), "campaigns:get_all", "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/content", pm(hasID(a.CampaignContent), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.POST("/api/campaigns/:id/text", pm(hasID(a.PreviewCampaign), "campaigns:get"))
|
||||
g.POST("/api/campaigns/:id/test", pm(hasID(a.TestCampaign), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.POST("/api/campaigns", pm(a.CreateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id", pm(a.UpdateCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/status", pm(a.UpdateCampaignStatus, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/archive", pm(a.UpdateCampaignArchive, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.DELETE("/api/campaigns/:id", pm(a.DeleteCampaign, "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id", pm(hasID(a.UpdateCampaign), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/status", pm(hasID(a.UpdateCampaignStatus), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.PUT("/api/campaigns/:id/archive", pm(hasID(a.UpdateCampaignArchive), "campaigns:manage_all", "campaigns:manage"))
|
||||
g.DELETE("/api/campaigns/:id", pm(hasID(a.DeleteCampaign), "campaigns:manage_all", "campaigns:manage"))
|
||||
|
||||
g.GET("/api/media", pm(a.GetMedia, "media:get"))
|
||||
g.GET("/api/media/:id", pm(a.GetMedia, "media:get"))
|
||||
g.GET("/api/media", pm(a.GetAllMedia, "media:get"))
|
||||
g.GET("/api/media/:id", pm(hasID(a.GetMedia), "media:get"))
|
||||
g.POST("/api/media", pm(a.UploadMedia, "media:manage"))
|
||||
g.DELETE("/api/media/:id", pm(a.DeleteMedia, "media:manage"))
|
||||
g.DELETE("/api/media/:id", pm(hasID(a.DeleteMedia), "media:manage"))
|
||||
|
||||
g.GET("/api/templates", pm(a.GetTemplates, "templates:get"))
|
||||
g.GET("/api/templates/:id", pm(a.GetTemplates, "templates:get"))
|
||||
g.GET("/api/templates/:id/preview", pm(a.PreviewTemplate, "templates:get"))
|
||||
g.POST("/api/templates/preview", pm(a.PreviewTemplate, "templates:get"))
|
||||
g.GET("/api/templates/:id", pm(hasID(a.GetTemplate), "templates:get"))
|
||||
g.GET("/api/templates/:id/preview", pm(hasID(a.PreviewTemplate), "templates:get"))
|
||||
g.POST("/api/templates/preview", pm(a.PreviewTemplateBody, "templates:get"))
|
||||
g.POST("/api/templates", pm(a.CreateTemplate, "templates:manage"))
|
||||
g.PUT("/api/templates/:id", pm(a.UpdateTemplate, "templates:manage"))
|
||||
g.PUT("/api/templates/:id/default", pm(a.TemplateSetDefault, "templates:manage"))
|
||||
g.DELETE("/api/templates/:id", pm(a.DeleteTemplate, "templates:manage"))
|
||||
g.PUT("/api/templates/:id", pm(hasID(a.UpdateTemplate), "templates:manage"))
|
||||
g.PUT("/api/templates/:id/default", pm(hasID(a.TemplateSetDefault), "templates:manage"))
|
||||
g.DELETE("/api/templates/:id", pm(hasID(a.DeleteTemplate), "templates:manage"))
|
||||
|
||||
g.DELETE("/api/maintenance/subscribers/:type", pm(a.GCSubscribers, "settings:maintain"))
|
||||
g.DELETE("/api/maintenance/analytics/:type", pm(a.GCCampaignAnalytics, "settings:maintain"))
|
||||
|
@ -182,20 +183,20 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
|||
g.GET("/api/profile", a.GetUserProfile)
|
||||
g.PUT("/api/profile", a.UpdateUserProfile)
|
||||
g.GET("/api/users", pm(a.GetUsers, "users:get"))
|
||||
g.GET("/api/users/:id", pm(a.GetUsers, "users:get"))
|
||||
g.GET("/api/users/:id", pm(hasID(a.GetUser), "users:get"))
|
||||
g.POST("/api/users", pm(a.CreateUser, "users:manage"))
|
||||
g.PUT("/api/users/:id", pm(a.UpdateUser, "users:manage"))
|
||||
g.PUT("/api/users/:id", pm(hasID(a.UpdateUser), "users:manage"))
|
||||
g.DELETE("/api/users", pm(a.DeleteUsers, "users:manage"))
|
||||
g.DELETE("/api/users/:id", pm(a.DeleteUsers, "users:manage"))
|
||||
g.DELETE("/api/users/:id", pm(hasID(a.DeleteUser), "users:manage"))
|
||||
g.POST("/api/logout", a.Logout)
|
||||
|
||||
g.GET("/api/roles/users", pm(a.GetUserRoles, "roles:get"))
|
||||
g.GET("/api/roles/lists", pm(a.GeListRoles, "roles:get"))
|
||||
g.POST("/api/roles/users", pm(a.CreateUserRole, "roles:manage"))
|
||||
g.POST("/api/roles/lists", pm(a.CreateListRole, "roles:manage"))
|
||||
g.PUT("/api/roles/users/:id", pm(a.UpdateUserRole, "roles:manage"))
|
||||
g.PUT("/api/roles/lists/:id", pm(a.UpdateListRole, "roles:manage"))
|
||||
g.DELETE("/api/roles/:id", pm(a.DeleteRole, "roles:manage"))
|
||||
g.PUT("/api/roles/users/:id", pm(hasID(a.UpdateUserRole), "roles:manage"))
|
||||
g.PUT("/api/roles/lists/:id", pm(hasID(a.UpdateListRole), "roles:manage"))
|
||||
g.DELETE("/api/roles/:id", pm(hasID(a.DeleteRole), "roles:manage"))
|
||||
|
||||
if a.cfg.BounceWebhooksEnabled {
|
||||
// Private authenticated bounce endpoint.
|
||||
|
@ -239,15 +240,15 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
|
|||
// Public subscriber facing views.
|
||||
g.GET("/subscription/form", a.SubscriptionFormPage)
|
||||
g.POST("/subscription/form", a.SubscriptionForm)
|
||||
g.GET("/subscription/:campUUID/:subUUID", noIndex(validateUUID(subscriberExists(a.SubscriptionPage), "campUUID", "subUUID")))
|
||||
g.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(a.SubscriptionPrefs), "campUUID", "subUUID"))
|
||||
g.GET("/subscription/optin/:subUUID", noIndex(validateUUID(subscriberExists(a.OptinPage), "subUUID")))
|
||||
g.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(a.OptinPage), "subUUID"))
|
||||
g.POST("/subscription/export/:subUUID", validateUUID(subscriberExists(a.SelfExportSubscriberData), "subUUID"))
|
||||
g.POST("/subscription/wipe/:subUUID", validateUUID(subscriberExists(a.WipeSubscriberData), "subUUID"))
|
||||
g.GET("/link/:linkUUID/:campUUID/:subUUID", noIndex(validateUUID(a.LinkRedirect, "linkUUID", "campUUID", "subUUID")))
|
||||
g.GET("/campaign/:campUUID/:subUUID", noIndex(validateUUID(a.ViewCampaignMessage, "campUUID", "subUUID")))
|
||||
g.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(a.RegisterCampaignView, "campUUID", "subUUID")))
|
||||
g.GET("/subscription/:campUUID/:subUUID", noIndex(a.hasUUID(a.hasSub(a.SubscriptionPage), "campUUID", "subUUID")))
|
||||
g.POST("/subscription/:campUUID/:subUUID", a.hasUUID(a.hasSub(a.SubscriptionPrefs), "campUUID", "subUUID"))
|
||||
g.GET("/subscription/optin/:subUUID", noIndex(a.hasUUID(a.hasSub(a.OptinPage), "subUUID")))
|
||||
g.POST("/subscription/optin/:subUUID", a.hasUUID(a.hasSub(a.OptinPage), "subUUID"))
|
||||
g.POST("/subscription/export/:subUUID", a.hasUUID(a.hasSub(a.SelfExportSubscriberData), "subUUID"))
|
||||
g.POST("/subscription/wipe/:subUUID", a.hasUUID(a.hasSub(a.WipeSubscriberData), "subUUID"))
|
||||
g.GET("/link/:linkUUID/:campUUID/:subUUID", noIndex(a.hasUUID(a.LinkRedirect, "linkUUID", "campUUID", "subUUID")))
|
||||
g.GET("/campaign/:campUUID/:subUUID", noIndex(a.hasUUID(a.ViewCampaignMessage, "campUUID", "subUUID")))
|
||||
g.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(a.hasUUID(a.RegisterCampaignView, "campUUID", "subUUID")))
|
||||
|
||||
if a.cfg.EnablePublicArchive {
|
||||
g.GET("/archive", a.CampaignArchivesPage)
|
||||
|
@ -326,39 +327,47 @@ func serveCustomAppearance(name string) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// validateUUID middleware validates the UUID string format for a given set of params.
|
||||
func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
|
||||
// hasUUID middleware validates the UUID string format for a given set of params.
|
||||
func (a *App) hasUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
app := c.Get("app").(*App)
|
||||
|
||||
for _, p := range params {
|
||||
if !reUUID.MatchString(c.Param(p)) {
|
||||
return c.Render(http.StatusBadRequest, tplMessage, makeMsgTpl(app.i18n.T("public.errorTitle"), "",
|
||||
app.i18n.T("globals.messages.invalidUUID")))
|
||||
return c.Render(http.StatusBadRequest, tplMessage, makeMsgTpl(a.i18n.T("public.errorTitle"), "",
|
||||
a.i18n.T("globals.messages.invalidUUID")))
|
||||
}
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// subscriberExists middleware checks if a subscriber exists given the UUID
|
||||
// param in a request.
|
||||
func subscriberExists(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
// hasID middleware validates the :id param in the URL and sets its int value in the context.
|
||||
func hasID(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var (
|
||||
app = c.Get("app").(*App)
|
||||
subUUID = c.Param("subUUID")
|
||||
)
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "invalid ID")
|
||||
}
|
||||
|
||||
if _, err := app.core.GetSubscriber(0, subUUID, ""); err != nil {
|
||||
c.Set("id", id)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// hasSub middleware checks if a subscriber exists given the UUID
|
||||
// param in a request.
|
||||
func (a *App) hasSub(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
subUUID := c.Param("subUUID")
|
||||
|
||||
if _, err := a.core.GetSubscriber(0, subUUID, ""); err != nil {
|
||||
if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
|
||||
return c.Render(http.StatusNotFound, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", er.Message.(string)))
|
||||
makeMsgTpl(a.i18n.T("public.notFoundTitle"), "", er.Message.(string)))
|
||||
}
|
||||
|
||||
app.log.Printf("error checking subscriber existence: %v", err)
|
||||
a.log.Printf("error checking subscriber existence: %v", err)
|
||||
return c.Render(http.StatusInternalServerError, tplMessage,
|
||||
makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
|
||||
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.T("public.errorProcessingRequest")))
|
||||
}
|
||||
|
||||
return next(c)
|
||||
|
@ -372,3 +381,8 @@ func noIndex(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
// getID returns the :id param from the URL parsed and stored as an int by the hasID middleware.
|
||||
func getID(c echo.Context) int {
|
||||
return c.Get("id").(int)
|
||||
}
|
||||
|
|
20
cmd/lists.go
20
cmd/lists.go
|
@ -12,10 +12,8 @@ import (
|
|||
|
||||
// GetLists retrieves lists with additional metadata like subscriber counts.
|
||||
func (a *App) GetLists(c echo.Context) error {
|
||||
var (
|
||||
user = auth.GetUser(c)
|
||||
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Get the list IDs (or blanket permission) the user has access to.
|
||||
hasAllPerm, permittedIDs := user.GetPermittedLists(auth.PermTypeGet)
|
||||
|
@ -51,6 +49,8 @@ func (a *App) GetLists(c echo.Context) error {
|
|||
typ = c.FormValue("type")
|
||||
optin = c.FormValue("optin")
|
||||
order = c.FormValue("order")
|
||||
|
||||
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
res, total, err := a.core.QueryLists(query, typ, optin, tags, orderBy, order, hasAllPerm, permittedIDs, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
|
@ -74,12 +74,8 @@ func (a *App) 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, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the list.
|
||||
id := getID(c)
|
||||
if err := user.HasListPerm(auth.PermTypeGet, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -119,12 +115,8 @@ func (a *App) 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, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to the list.
|
||||
id := getID(c)
|
||||
if err := user.HasListPerm(auth.PermTypeManage, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
40
cmd/media.go
40
cmd/media.go
|
@ -5,7 +5,6 @@ import (
|
|||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
|
@ -140,24 +139,14 @@ func (a *App) UploadMedia(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{m})
|
||||
}
|
||||
|
||||
// GetMedia handles retrieval of uploaded media.
|
||||
func (a *App) GetMedia(c echo.Context) error {
|
||||
// Fetch one media item from the DB.
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id > 0 {
|
||||
out, err := a.core.GetMedia(id, "", "", a.media)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// Get the media from the DB.
|
||||
// GetAllMedia handles retrieval of uploaded media.
|
||||
func (a *App) GetAllMedia(c echo.Context) error {
|
||||
var (
|
||||
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
||||
query = c.FormValue("query")
|
||||
|
||||
pg = a.pg.NewFromURL(c.Request().URL.Query())
|
||||
)
|
||||
// Fetch the media items from the DB.
|
||||
res, total, err := a.core.QueryMedia(a.cfg.MediaUpload.Provider, a.media, query, pg.Offset, pg.Limit)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -173,14 +162,23 @@ func (a *App) GetMedia(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// DeleteMedia handles deletion of uploaded media.
|
||||
func (a *App) DeleteMedia(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
// GetMedia handles retrieval of a media item by ID.
|
||||
func (a *App) GetMedia(c echo.Context) error {
|
||||
// Fetch the media item from the DB.
|
||||
id := getID(c)
|
||||
out, err := a.core.GetMedia(id, "", "", a.media)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// DeleteMedia handles deletion of uploaded media.
|
||||
func (a *App) DeleteMedia(c echo.Context) error {
|
||||
|
||||
// Delete the media from the DB. The query returns the filename.
|
||||
id := getID(c)
|
||||
fname, err := a.core.DeleteMedia(id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
25
cmd/roles.go
25
cmd/roles.go
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/listmonk/internal/auth"
|
||||
|
@ -72,9 +71,10 @@ func (a *App) CreateListRole(c echo.Context) error {
|
|||
|
||||
// UpdateUserRole handles role modification.
|
||||
func (a *App) 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 {
|
||||
id := getID(c)
|
||||
|
||||
// ID 1 is reserved for the Super Admin user role.
|
||||
if id == auth.SuperAdminRoleID {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
|
@ -106,9 +106,11 @@ func (a *App) UpdateUserRole(c echo.Context) error {
|
|||
|
||||
// UpdateListRole handles role modification.
|
||||
func (a *App) 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 {
|
||||
// Get the role ID.
|
||||
id := getID(c)
|
||||
|
||||
// ID 1 is reserved for the Super Admin user role.
|
||||
if id == auth.SuperAdminRoleID {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
|
@ -139,10 +141,13 @@ func (a *App) UpdateListRole(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// DeleteRole handles role deletion.
|
||||
// DeleteRole handles (user|list) role deletion.
|
||||
func (a *App) DeleteRole(c echo.Context) error {
|
||||
id, _ := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if id < 2 {
|
||||
// Get the role ID.
|
||||
id := getID(c)
|
||||
|
||||
// ID 1 is reserved for the Super Admin user role.
|
||||
if id == auth.SuperAdminRoleID {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
|
|
|
@ -56,15 +56,10 @@ var (
|
|||
|
||||
// GetSubscriber handles the retrieval of a single subscriber by ID.
|
||||
func (a *App) 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, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Check if the user has access to at least one of the lists on the subscriber.
|
||||
id := getID(c)
|
||||
if err := a.hasSubPerm(user, []int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -212,11 +207,6 @@ func (a *App) 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, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Get and validate fields.
|
||||
req := struct {
|
||||
models.Subscriber
|
||||
|
@ -242,6 +232,7 @@ func (a *App) UpdateSubscriber(c echo.Context) error {
|
|||
listIDs := user.FilterListsByPerm(auth.PermTypeManage, req.Lists)
|
||||
|
||||
// Update the subscriber in the DB.
|
||||
id := getID(c)
|
||||
out, _, err := a.core.UpdateSubscriberWithLists(id, req.Subscriber, listIDs, nil, req.PreconfirmSubs, true)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -252,12 +243,8 @@ func (a *App) UpdateSubscriber(c echo.Context) error {
|
|||
|
||||
// SubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
||||
func (a *App) SubscriberSendOptin(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Fetch the subscriber.
|
||||
id := getID(c)
|
||||
out, err := a.core.GetSubscriber(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -271,38 +258,31 @@ func (a *App) SubscriberSendOptin(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// BlocklistSubscriber handles the blocklisting of a given subscriber.
|
||||
func (a *App) BlocklistSubscriber(c echo.Context) error {
|
||||
// Update the subscribers in the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.BlocklistSubscribers([]int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// 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 (a *App) BlocklistSubscribers(c echo.Context) error {
|
||||
// Is it a /:id call?
|
||||
var (
|
||||
subIDs []int
|
||||
pID = c.Param("id")
|
||||
)
|
||||
if pID != "" {
|
||||
id, _ := strconv.Atoi(pID)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
subIDs = append(subIDs, id)
|
||||
} else {
|
||||
// Multiple IDs.
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(req.SubscriberIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
"No IDs given.")
|
||||
}
|
||||
|
||||
subIDs = req.SubscriberIDs
|
||||
var req subQueryReq
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(req.SubscriberIDs) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", "ids"))
|
||||
}
|
||||
|
||||
// Update the subscribers in the DB.
|
||||
if err := a.core.BlocklistSubscribers(subIDs); err != nil {
|
||||
if err := a.core.BlocklistSubscribers(req.SubscriberIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -367,35 +347,32 @@ func (a *App) ManageSubscriberLists(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteSubscribers handles subscriber deletion.
|
||||
// It takes either an ID in the URI, or a list of IDs in the request body.
|
||||
// DeleteSubscriber handles deletion of a single subscriber.
|
||||
func (a *App) DeleteSubscriber(c echo.Context) error {
|
||||
// Delete the subscribers from the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.DeleteSubscribers([]int{id}, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteSubscribers handles bulk deletion of one or more subscribers.
|
||||
func (a *App) DeleteSubscribers(c echo.Context) error {
|
||||
// Is it an /:id call?
|
||||
var (
|
||||
pID = c.Param("id")
|
||||
subIDs []int
|
||||
)
|
||||
if pID != "" {
|
||||
id, _ := strconv.Atoi(pID)
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
subIDs = append(subIDs, id)
|
||||
} else {
|
||||
// Multiple IDs.
|
||||
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(i) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("subscribers.errorNoIDs"))
|
||||
}
|
||||
subIDs = i
|
||||
// Multiple IDs.
|
||||
ids, err := parseStringIDs(c.Request().URL.Query()["id"])
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("globals.messages.errorInvalidIDs", "error", "ids"))
|
||||
}
|
||||
|
||||
// Delete the subscribers from the DB.
|
||||
if err := a.core.DeleteSubscribers(subIDs, nil); err != nil {
|
||||
if err := a.core.DeleteSubscribers(ids, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -486,12 +463,8 @@ func (a *App) ManageSubscriberListsByQuery(c echo.Context) error {
|
|||
|
||||
// DeleteSubscriberBounces deletes all the bounces on a subscriber.
|
||||
func (a *App) DeleteSubscriberBounces(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the bounces from the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.DeleteSubscriberBounces(id, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -504,14 +477,10 @@ func (a *App) DeleteSubscriberBounces(c echo.Context) error {
|
|||
// a JSON report. This is a privacy feature and depends on the
|
||||
// configuration in a.Constants.Privacy.
|
||||
func (a *App) ExportSubscriberData(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.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".
|
||||
id := getID(c)
|
||||
_, b, err := a.exportSubscriberData(id, "", a.cfg.Privacy.Exportable)
|
||||
if err != nil {
|
||||
a.log.Printf("error exporting subscriber data: %s", err)
|
||||
|
@ -629,26 +598,6 @@ func sanitizeSQLExp(q string) string {
|
|||
return q
|
||||
}
|
||||
|
||||
// getQueryInts parses the list of given query param values into ints.
|
||||
func getQueryInts(param string, qp url.Values) ([]int, error) {
|
||||
var out []int
|
||||
if vals, ok := qp[param]; ok {
|
||||
for _, v := range vals {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
listID, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, listID)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// makeOptinNotifyHook 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()`.
|
||||
|
|
187
cmd/templates.go
187
cmd/templates.go
|
@ -31,23 +31,27 @@ var (
|
|||
regexpTplTag = regexp.MustCompile(`{{(\s+)?template\s+?"content"(\s+)?\.(\s+)?}}`)
|
||||
)
|
||||
|
||||
// GetTemplates handles retrieval of templates.
|
||||
func (a *App) 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 := a.core.GetTemplate(id, noBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// GetTemplate handles the retrieval of a template
|
||||
func (a *App) GetTemplate(c echo.Context) error {
|
||||
// If no_body is true, blank out the body of the template from the response.
|
||||
noBody, _ := strconv.ParseBool(c.QueryParam("no_body"))
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
// Get the template from the DB.
|
||||
id := getID(c)
|
||||
out, err := a.core.GetTemplate(id, noBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Fetch templates from the DB (with or without body).
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// GetTemplates handles retrieval of templates.
|
||||
func (a *App) GetTemplates(c echo.Context) error {
|
||||
// If no_body is true, blank out the body of the template from the response.
|
||||
noBody, _ := strconv.ParseBool(c.QueryParam("no_body"))
|
||||
|
||||
// Fetch templates from the DB.
|
||||
out, err := a.core.GetTemplates("", noBody)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -56,77 +60,45 @@ func (a *App) GetTemplates(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// PreviewTemplate renders the HTML preview of a template.
|
||||
// PreviewTemplate renders the HTML preview of a template in the DB.
|
||||
func (a *App) PreviewTemplate(c echo.Context) error {
|
||||
// Fetch one template from the DB.
|
||||
id := getID(c)
|
||||
tpl, err := a.core.GetTemplate(id, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Render the template.
|
||||
out, err := a.previewTemplate(tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.HTML(http.StatusOK, string(out))
|
||||
}
|
||||
|
||||
// PreviewTemplateBody renders the HTML preview of a template given its type and body.
|
||||
func (a *App) PreviewTemplateBody(c echo.Context) error {
|
||||
tpl := models.Template{
|
||||
Type: c.FormValue("template_type"),
|
||||
Body: c.FormValue("body"),
|
||||
}
|
||||
|
||||
// Body is posted with the request.
|
||||
if tpl.Body != "" {
|
||||
if tpl.Type == "" {
|
||||
tpl.Type = models.TemplateTypeCampaign
|
||||
}
|
||||
|
||||
if tpl.Type == models.TemplateTypeCampaign && !regexpTplTag.MatchString(tpl.Body) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.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, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
t, err := a.core.GetTemplate(id, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tpl = t
|
||||
if tpl.Type == "" {
|
||||
tpl.Type = models.TemplateTypeCampaign
|
||||
}
|
||||
|
||||
// Compile the campaign template.
|
||||
var out []byte
|
||||
if tpl.Type == models.TemplateTypeCampaign {
|
||||
camp := models.Campaign{
|
||||
UUID: dummyUUID,
|
||||
Name: a.i18n.T("templates.dummyName"),
|
||||
Subject: a.i18n.T("templates.dummySubject"),
|
||||
FromEmail: "dummy-campaign@listmonk.app",
|
||||
TemplateBody: tpl.Body,
|
||||
Body: dummyTpl,
|
||||
}
|
||||
if tpl.Type == models.TemplateTypeCampaign && !regexpTplTag.MatchString(tpl.Body) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("templates.placeholderHelp", "placeholder", tplTag))
|
||||
}
|
||||
|
||||
if err := camp.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Render the message body.
|
||||
msg, err := a.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
}
|
||||
out = msg.Body()
|
||||
} else {
|
||||
// Compile transactional template.
|
||||
if err := tpl.Compile(a.manager.GenericTemplateFuncs()); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
m := models.TxMessage{
|
||||
Subject: tpl.Subject,
|
||||
}
|
||||
|
||||
// Render the message.
|
||||
if err := m.Render(dummySubscriber, &tpl); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
out = m.Body
|
||||
// Render the template.
|
||||
out, err := a.previewTemplate(tpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.HTML(http.StatusOK, string(out))
|
||||
|
@ -174,11 +146,6 @@ func (a *App) CreateTemplate(c echo.Context) error {
|
|||
|
||||
// UpdateTemplate handles template modification.
|
||||
func (a *App) UpdateTemplate(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
var o models.Template
|
||||
if err := c.Bind(&o); err != nil {
|
||||
return err
|
||||
|
@ -203,6 +170,7 @@ func (a *App) UpdateTemplate(c echo.Context) error {
|
|||
}
|
||||
|
||||
// Update the template in the DB.
|
||||
id := getID(c)
|
||||
out, err := a.core.UpdateTemplate(id, o.Name, o.Subject, []byte(o.Body))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -219,12 +187,8 @@ func (a *App) UpdateTemplate(c echo.Context) error {
|
|||
|
||||
// TemplateSetDefault handles template modification.
|
||||
func (a *App) TemplateSetDefault(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Update the template in the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.SetDefaultTemplate(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -234,17 +198,13 @@ func (a *App) TemplateSetDefault(c echo.Context) error {
|
|||
|
||||
// DeleteTemplate handles template deletion.
|
||||
func (a *App) DeleteTemplate(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Delete the template from the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.DeleteTemplate(id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete cached template.
|
||||
// Delete cached in-memory template.
|
||||
a.manager.DeleteTpl(id)
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
|
@ -268,3 +228,48 @@ func (a *App) validateTemplate(o models.Template) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// previewTemplate renders the HTML preview of a template.
|
||||
func (a *App) previewTemplate(tpl models.Template) ([]byte, error) {
|
||||
var out []byte
|
||||
if tpl.Type == models.TemplateTypeCampaign {
|
||||
camp := models.Campaign{
|
||||
UUID: dummyUUID,
|
||||
Name: a.i18n.T("templates.dummyName"),
|
||||
Subject: a.i18n.T("templates.dummySubject"),
|
||||
FromEmail: "dummy-campaign@listmonk.app",
|
||||
TemplateBody: tpl.Body,
|
||||
Body: dummyTpl,
|
||||
}
|
||||
|
||||
if err := camp.CompileTemplate(a.manager.TemplateFuncs(&camp)); err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("templates.errorCompiling", "error", err.Error()))
|
||||
}
|
||||
|
||||
// Render the message body.
|
||||
msg, err := a.manager.NewCampaignMessage(&camp, dummySubscriber)
|
||||
if err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest,
|
||||
a.i18n.Ts("templates.errorRendering", "error", err.Error()))
|
||||
}
|
||||
out = msg.Body()
|
||||
} else {
|
||||
// Compile transactional template.
|
||||
if err := tpl.Compile(a.manager.GenericTemplateFuncs()); err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
m := models.TxMessage{
|
||||
Subject: tpl.Subject,
|
||||
}
|
||||
|
||||
// Render the message.
|
||||
if err := m.Render(dummySubscriber, &tpl); err != nil {
|
||||
return nil, echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
out = m.Body
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
|
84
cmd/users.go
84
cmd/users.go
|
@ -3,7 +3,6 @@ package main
|
|||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/knadh/listmonk/internal/auth"
|
||||
|
@ -17,36 +16,30 @@ var (
|
|||
reUsername = regexp.MustCompile(`^[a-zA-Z0-9_\\-\\.]+$`)
|
||||
)
|
||||
|
||||
// GetUsers retrieves users.
|
||||
// GetUser retrieves a single user by ID.
|
||||
func (a *App) GetUser(c echo.Context) error {
|
||||
// Get the user from the DB.
|
||||
id := getID(c)
|
||||
out, err := a.core.GetUser(id, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Blank out the password hash in the response.
|
||||
out.Password = null.String{}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// GetUsers retrieves all users.
|
||||
func (a *App) GetUsers(c echo.Context) error {
|
||||
var (
|
||||
single = false
|
||||
userID, _ = strconv.Atoi(c.Param("id"))
|
||||
)
|
||||
if userID > 0 {
|
||||
single = true
|
||||
}
|
||||
|
||||
if single {
|
||||
// Get the user from the DB.
|
||||
out, err := a.core.GetUser(userID, "", "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Blank out the password hash respond to the API.
|
||||
out.Password = null.String{}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
}
|
||||
|
||||
// Get all users from the DB.
|
||||
out, err := a.core.GetUsers()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Blank out password hashes before respond to the API.
|
||||
// Blank out the password hash in the response.
|
||||
for n := range out {
|
||||
out[n].Password = null.String{}
|
||||
}
|
||||
|
@ -56,7 +49,7 @@ func (a *App) GetUsers(c echo.Context) error {
|
|||
|
||||
// CreateUser handles user creation.
|
||||
func (a *App) CreateUser(c echo.Context) error {
|
||||
var u = auth.User{}
|
||||
var u auth.User
|
||||
if err := c.Bind(&u); err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -95,7 +88,7 @@ func (a *App) CreateUser(c echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Blank out the password hash before responding to the API.
|
||||
// Blank out the password hash in the response.
|
||||
if user.Type != auth.UserTypeAPI {
|
||||
user.Password = null.String{}
|
||||
}
|
||||
|
@ -110,11 +103,6 @@ func (a *App) CreateUser(c echo.Context) error {
|
|||
|
||||
// UpdateUser handles user modification.
|
||||
func (a *App) UpdateUser(c echo.Context) error {
|
||||
id, _ := strconv.Atoi(c.Param("id"))
|
||||
if id < 1 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
|
||||
// Incoming params.
|
||||
var u auth.User
|
||||
if err := c.Bind(&u); err != nil {
|
||||
|
@ -133,6 +121,8 @@ func (a *App) UpdateUser(c echo.Context) error {
|
|||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", "username"))
|
||||
}
|
||||
|
||||
// Get the user ID.
|
||||
id := getID(c)
|
||||
if u.Type != auth.UserTypeAPI {
|
||||
if !utils.ValidateEmail(email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", "email"))
|
||||
|
@ -178,7 +168,7 @@ func (a *App) UpdateUser(c echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Blank out the password hash before responding to the API.
|
||||
// Blank out the password hash in the response.
|
||||
user.Password = null.String{}
|
||||
|
||||
// Cache the API token for in-memory, off-DB /api/* request auth.
|
||||
|
@ -189,18 +179,28 @@ func (a *App) UpdateUser(c echo.Context) error {
|
|||
return c.JSON(http.StatusOK, okResp{user})
|
||||
}
|
||||
|
||||
// DeleteUser handles the deletion of a single user by ID.
|
||||
func (a *App) DeleteUser(c echo.Context) error {
|
||||
// Delete the user(s) from the DB.
|
||||
id := getID(c)
|
||||
if err := a.core.DeleteUsers([]int{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Cache the API token for in-memory, off-DB /api/* request auth.
|
||||
if _, err := cacheUsers(a.core, a.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{true})
|
||||
}
|
||||
|
||||
// DeleteUsers handles user deletion, either a single one (ID in the URI), or a list.
|
||||
func (a *App) DeleteUsers(c echo.Context) error {
|
||||
var (
|
||||
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
ids []int
|
||||
)
|
||||
if id < 1 && len(ids) == 0 {
|
||||
ids, err := getQueryInts("id", c.QueryParams())
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("globals.messages.invalidID"))
|
||||
}
|
||||
if id > 0 {
|
||||
ids = append(ids, int(id))
|
||||
}
|
||||
|
||||
// Delete the user(s) from the DB.
|
||||
if err := a.core.DeleteUsers(ids); err != nil {
|
||||
|
@ -220,7 +220,7 @@ func (a *App) GetUserProfile(c echo.Context) error {
|
|||
// Get the authenticated user.
|
||||
user := auth.GetUser(c)
|
||||
|
||||
// Blank out the password hash before responding to the API.
|
||||
// Blank out the password hash in the response.
|
||||
user.Password.String = ""
|
||||
user.Password.Valid = false
|
||||
|
||||
|
@ -261,7 +261,7 @@ func (a *App) UpdateUserProfile(c echo.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// Blank out the password hash before responding to the API.
|
||||
// Blank out the password hash in the response.
|
||||
out.Password = null.String{}
|
||||
|
||||
return c.JSON(http.StatusOK, okResp{out})
|
||||
|
|
21
cmd/utils.go
21
cmd/utils.go
|
@ -3,6 +3,7 @@ package main
|
|||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"slices"
|
||||
|
@ -91,3 +92,23 @@ func generateRandomString(n int) (string, error) {
|
|||
func strHasLen(str string, min, max int) bool {
|
||||
return len(str) >= min && len(str) <= max
|
||||
}
|
||||
|
||||
// getQueryInts parses the list of given query param values into ints.
|
||||
func getQueryInts(param string, qp url.Values) ([]int, error) {
|
||||
var out []int
|
||||
if vals, ok := qp[param]; ok {
|
||||
for _, v := range vals {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
listID, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, listID)
|
||||
}
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue