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:
Kailash Nadh 2025-04-06 14:01:21 +05:30
parent e6d3aad0a1
commit 0826f401b7
10 changed files with 374 additions and 406 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()`.

View file

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

View file

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

View file

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