Merge pull request #1 from knadh/master

update
This commit is contained in:
TomBoss 2021-02-15 19:06:05 +01:00 committed by GitHub
commit b4fea57543
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 495 additions and 490 deletions

View File

@ -118,15 +118,14 @@ func handleViewCampaignMessage(c echo.Context) error {
} }
// Get the subscriber. // Get the subscriber.
var sub models.Subscriber sub, err := getSubscriber(0, subUUID, "", app)
if err := app.queries.GetSubscriber.Get(&sub, 0, subUUID); err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return c.Render(http.StatusNotFound, tplMessage, return c.Render(http.StatusNotFound, tplMessage,
makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", makeMsgTpl(app.i18n.T("public.notFoundTitle"), "",
app.i18n.T("public.errorFetchingEmail"))) app.i18n.T("public.errorFetchingEmail")))
} }
app.log.Printf("error fetching campaign subscriber: %v", err)
return c.Render(http.StatusInternalServerError, tplMessage, return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", makeMsgTpl(app.i18n.T("public.errorTitle"), "",
app.i18n.Ts("public.errorFetchingCampaign"))) app.i18n.Ts("public.errorFetchingCampaign")))
@ -324,14 +323,18 @@ func handleSubscriptionForm(c echo.Context) error {
// Insert the subscriber into the DB. // Insert the subscriber into the DB.
req.Status = models.SubscriberStatusEnabled req.Status = models.SubscriberStatusEnabled
req.ListUUIDs = pq.StringArray(req.SubListUUIDs) req.ListUUIDs = pq.StringArray(req.SubListUUIDs)
if _, err := insertSubscriber(req.SubReq, app); err != nil && err != errSubscriberExists { _, _, hasOptin, err := insertSubscriber(req.SubReq, app)
if err != nil {
return c.Render(http.StatusInternalServerError, tplMessage, return c.Render(http.StatusInternalServerError, tplMessage,
makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", err.(*echo.HTTPError).Message))) makeMsgTpl(app.i18n.T("public.errorTitle"), "", fmt.Sprintf("%s", err.(*echo.HTTPError).Message)))
} }
return c.Render(http.StatusOK, tplMessage, msg := "public.subConfirmed"
makeMsgTpl(app.i18n.T("public.subTitle"), "", if hasOptin {
app.i18n.Ts("public.subConfirmed"))) msg = "public.subOptinPending"
}
return c.Render(http.StatusOK, tplMessage, makeMsgTpl(app.i18n.T("public.subTitle"), "", app.i18n.Ts(msg)))
} }
// handleLinkRedirect redirects a link UUID to its original underlying link // handleLinkRedirect redirects a link UUID to its original underlying link

View File

@ -79,7 +79,11 @@ func handleGetSubscriber(c echo.Context) error {
id, _ = strconv.Atoi(c.Param("id")) id, _ = strconv.Atoi(c.Param("id"))
) )
sub, err := getSubscriber(id, app) if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
sub, err := getSubscriber(id, "", "", app)
if err != nil { if err != nil {
return err return err
} }
@ -273,14 +277,13 @@ func handleCreateSubscriber(c echo.Context) error {
} }
// Insert the subscriber into the DB. // Insert the subscriber into the DB.
sub, err := insertSubscriber(req, app) sub, isNew, _, err := insertSubscriber(req, app)
if err != nil { if err != nil {
if err == errSubscriberExists {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
return err return err
} }
if !isNew {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
}
return c.JSON(http.StatusOK, okResp{sub}) return c.JSON(http.StatusOK, okResp{sub})
} }
@ -321,11 +324,11 @@ func handleUpdateSubscriber(c echo.Context) error {
} }
// Send a confirmation e-mail (if there are any double opt-in lists). // Send a confirmation e-mail (if there are any double opt-in lists).
sub, err := getSubscriber(int(id), app) sub, err := getSubscriber(int(id), "", "", app)
if err != nil { if err != nil {
return err return err
} }
_ = sendOptinConfirmation(sub, []int64(req.Lists), app) _, _ = sendOptinConfirmation(sub, []int64(req.Lists), app)
return c.JSON(http.StatusOK, okResp{sub}) return c.JSON(http.StatusOK, okResp{sub})
} }
@ -335,7 +338,6 @@ func handleSubscriberSendOptin(c echo.Context) error {
var ( var (
app = c.Get("app").(*App) app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id")) id, _ = strconv.Atoi(c.Param("id"))
out models.Subscribers
) )
if id < 1 { if id < 1 {
@ -343,19 +345,15 @@ func handleSubscriberSendOptin(c echo.Context) error {
} }
// Fetch the subscriber. // Fetch the subscriber.
err := app.queries.GetSubscriber.Select(&out, id, nil) out, err := getSubscriber(id, "", "", app)
if err != nil { if err != nil {
app.log.Printf("error fetching subscriber: %v", err) app.log.Printf("error fetching subscriber: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching", app.i18n.Ts("globals.messages.errorFetching",
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err))) "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
} }
if len(out) == 0 {
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
}
if err := sendOptinConfirmation(out[0], nil, app); err != nil { if _, err := sendOptinConfirmation(out, nil, app); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.T("subscribers.errorSendingOptin")) app.i18n.T("subscribers.errorSendingOptin"))
} }
@ -619,56 +617,53 @@ func handleExportSubscriberData(c echo.Context) error {
return c.Blob(http.StatusOK, "application/json", b) return c.Blob(http.StatusOK, "application/json", b)
} }
// insertSubscriber inserts a subscriber and returns the ID. // insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, error) { // it was a new subscriber, and the second bool indicates if the subscriber was sent an optin confirmation.
func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, bool, bool, error) {
uu, err := uuid.NewV4() uu, err := uuid.NewV4()
if err != nil { if err != nil {
return req.Subscriber, err return req.Subscriber, false, false, err
} }
req.UUID = uu.String() req.UUID = uu.String()
err = app.queries.InsertSubscriber.Get(&req.ID, isNew := true
if err = app.queries.InsertSubscriber.Get(&req.ID,
req.UUID, req.UUID,
req.Email, req.Email,
strings.TrimSpace(req.Name), strings.TrimSpace(req.Name),
req.Status, req.Status,
req.Attribs, req.Attribs,
req.Lists, req.Lists,
req.ListUUIDs) req.ListUUIDs); err != nil {
if err != nil {
if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" { if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
return req.Subscriber, errSubscriberExists isNew = false
} } else {
// return req.Subscriber, errSubscriberExists
app.log.Printf("error inserting subscriber: %v", err) app.log.Printf("error inserting subscriber: %v", err)
return req.Subscriber, echo.NewHTTPError(http.StatusInternalServerError, return req.Subscriber, false, false, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorCreating", app.i18n.Ts("globals.messages.errorCreating",
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err))) "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
} }
}
// Fetch the subscriber's full data. // Fetch the subscriber's full data. If the subscriber already existed and wasn't
sub, err := getSubscriber(req.ID, app) // created, the id will be empty. Fetch the details by e-mail then.
sub, err := getSubscriber(req.ID, "", strings.ToLower(req.Email), app)
if err != nil { if err != nil {
return sub, err return sub, false, false, err
} }
// Send a confirmation e-mail (if there are any double opt-in lists). // Send a confirmation e-mail (if there are any double opt-in lists).
_ = sendOptinConfirmation(sub, []int64(req.Lists), app) num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
return sub, nil return sub, isNew, num > 0, nil
} }
// getSubscriber gets a single subscriber by ID. // getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
func getSubscriber(id int, app *App) (models.Subscriber, error) { // Only one of these params should have a value.
var ( func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
out models.Subscribers var out models.Subscribers
)
if id < 1 { if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
return models.Subscriber{},
echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
if err := app.queries.GetSubscriber.Select(&out, id, nil); err != nil {
app.log.Printf("error fetching subscriber: %v", err) app.log.Printf("error fetching subscriber: %v", err)
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError, return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching", app.i18n.Ts("globals.messages.errorFetching",
@ -733,8 +728,9 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
} }
// sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber // sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
// if at least one of the given listIDs is set to optin=double // if at least one of the given listIDs is set to optin=double. It returns the number of
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) error { // opt-in lists that were found.
func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
var lists []models.List var lists []models.List
// Fetch double opt-in lists from the given list IDs. // Fetch double opt-in lists from the given list IDs.
@ -742,12 +738,12 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil, if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil { pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err)) app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
return err return 0, err
} }
// None. // None.
if len(lists) == 0 { if len(lists) == 0 {
return nil return 0, nil
} }
var ( var (
@ -764,9 +760,9 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
if err := app.sendNotification([]string{sub.Email}, if err := app.sendNotification([]string{sub.Email},
app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil { app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
app.log.Printf("error sending opt-in e-mail: %s", err) app.log.Printf("error sending opt-in e-mail: %s", err)
return err return 0, err
} }
return nil return len(lists), nil
} }
// sanitizeSQLExp does basic sanitisation on arbitrary // sanitizeSQLExp does basic sanitisation on arbitrary

View File

@ -5,7 +5,7 @@
<b-tag v-for="l in selectedItems" <b-tag v-for="l in selectedItems"
:key="l.id" :key="l.id"
:class="l.subscriptionStatus" :class="l.subscriptionStatus"
:closable="!disabled && l.subscriptionStatus !== 'unsubscribed'" :closable="true"
:data-id="l.id" :data-id="l.id"
@close="removeList(l.id)"> @close="removeList(l.id)">
{{ l.name }} <sup>{{ l.subscriptionStatus }}</sup> {{ l.name }} <sup>{{ l.subscriptionStatus }}</sup>

View File

@ -259,6 +259,7 @@
"public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.", "public.privacyWipeHelp": "Delete all your subscriptions and related data from the database permanently.",
"public.sub": "Subscribe", "public.sub": "Subscribe",
"public.subConfirmed": "Subscribed successfully.", "public.subConfirmed": "Subscribed successfully.",
"public.subOptinPending": "An e-mail has been sent to you to confirm your subscription(s).",
"public.subConfirmedTitle": "Confirmed", "public.subConfirmedTitle": "Confirmed",
"public.subName": "Name (optional)", "public.subName": "Name (optional)",
"public.subNotFound": "Subscription not found.", "public.subNotFound": "Subscription not found.",

View File

@ -189,7 +189,7 @@
"import.mode": "Mode", "import.mode": "Mode",
"import.overwrite": "Écraser ?", "import.overwrite": "Écraser ?",
"import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?", "import.overwriteHelp": "Remplacer le nom et les attributs des abonnés existants ?",
"import.recordsCount": "{num} \/ {total} enregistrements", "import.recordsCount": "{num} / {total} enregistrements",
"import.stopImport": "Arrêter l'importation", "import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner", "import.subscribe": "S'abonner",
"import.title": "Importer des abonnés", "import.title": "Importer des abonnés",
@ -292,7 +292,7 @@
"settings.media.provider": "Fournisseur", "settings.media.provider": "Fournisseur",
"settings.media.s3.bucket": "Bucket", "settings.media.s3.bucket": "Bucket",
"settings.media.s3.bucketPath": "Chemin du bucket", "settings.media.s3.bucketPath": "Chemin du bucket",
"settings.media.s3.bucketPathHelp": "Chemin à l'intérieur du bucket pour télécharger les fichiers. La valeur par défaut est \/", "settings.media.s3.bucketPathHelp": "Chemin à l'intérieur du bucket pour télécharger les fichiers. La valeur par défaut est /",
"settings.media.s3.bucketType": "Type de bucket", "settings.media.s3.bucketType": "Type de bucket",
"settings.media.s3.bucketTypePrivate": "Privé", "settings.media.s3.bucketTypePrivate": "Privé",
"settings.media.s3.bucketTypePublic": "Publique", "settings.media.s3.bucketTypePublic": "Publique",
@ -305,13 +305,13 @@
"settings.media.upload.path": "Chemin du téléchargement", "settings.media.upload.path": "Chemin du téléchargement",
"settings.media.upload.pathHelp": "Chemin vers le répertoire où les médias seront téléchargés.", "settings.media.upload.pathHelp": "Chemin vers le répertoire où les médias seront téléchargés.",
"settings.media.upload.uri": "URI de téléchargement", "settings.media.upload.uri": "URI de téléchargement",
"settings.media.upload.uriHelp": "URI de téléchargement qui sera visible du monde extérieur. Le média téléchargé dans le chemin du téléchargement sera accessible publiquement sous {root_url}, par exemple, https:\/\/listmonk.votresite.com\/uploads.", "settings.media.upload.uriHelp": "URI de téléchargement qui sera visible du monde extérieur. Le média téléchargé dans le chemin du téléchargement sera accessible publiquement sous {root_url}, par exemple, https://listmonk.votresite.com/uploads.",
"settings.messengers.maxConns": "Nb. connexions max.", "settings.messengers.maxConns": "Nb. connexions max.",
"settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.", "settings.messengers.maxConnsHelp": "Nombre maximum de connexions simultanées au serveur.",
"settings.messengers.messageDiscard": "Annuler les modifications ?", "settings.messengers.messageDiscard": "Annuler les modifications ?",
"settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...", "settings.messengers.messageSaved": "Paramètres sauvegardés. Rechargement de l'application...",
"settings.messengers.name": "Messagers", "settings.messengers.name": "Messagers",
"settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique \/ tiret.", "settings.messengers.nameHelp": "Par exemple : my-sms. Alphanumérique / tiret.",
"settings.messengers.password": "Mot de passe", "settings.messengers.password": "Mot de passe",
"settings.messengers.retries": "Tentatives", "settings.messengers.retries": "Tentatives",
"settings.messengers.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.", "settings.messengers.retriesHelp": "Nombre de tentatives en cas d'échec d'un message.",

View File

@ -1,8 +1,13 @@
-- subscribers -- subscribers
-- name: get-subscriber -- name: get-subscriber
-- Get a single subscriber by id or UUID. -- Get a single subscriber by id or UUID or email.
SELECT * FROM subscribers WHERE CASE WHEN $1 > 0 THEN id = $1 ELSE uuid = $2 END; SELECT * FROM subscribers WHERE
CASE
WHEN $1 > 0 THEN id = $1
WHEN $2 != '' THEN uuid = $2::UUID
WHEN $3 != '' THEN email = $3
END;
-- name: subscriber-exists -- name: subscriber-exists
-- Check if a subscriber exists by id or UUID. -- Check if a subscriber exists by id or UUID.