From ad80c716f9de71e5cda19c3181ba7938a701c9ea Mon Sep 17 00:00:00 2001 From: Kailash Nadh Date: Sat, 22 Jul 2023 13:28:45 +0530 Subject: [PATCH] Add new privacy option 'Record opt-in IP' to record IP address of optin confirmation. - Add new 'Subscriptions' table on the subscriber list form that shows subs, IP, and other data. - Add new `meta` JSONB field to `subscriber_lsts` table. Closes #1329. --- cmd/init.go | 1 + cmd/public.go | 11 ++++- frontend/src/views/SubscriberForm.vue | 66 +++++++++++++------------ frontend/src/views/settings/privacy.vue | 6 +++ i18n/ca.json | 2 + i18n/cs-cz.json | 2 + i18n/cy.json | 2 + i18n/de.json | 2 + i18n/en.json | 2 + i18n/es.json | 2 + i18n/fi.json | 2 + i18n/fr.json | 2 + i18n/hu.json | 2 + i18n/it.json | 2 + i18n/jp.json | 2 + i18n/ml.json | 2 + i18n/nl.json | 2 + i18n/pl.json | 2 + i18n/pt-BR.json | 2 + i18n/pt.json | 2 + i18n/ro.json | 2 + i18n/ru.json | 2 + i18n/se.json | 2 + i18n/sk.json | 2 + i18n/tr.json | 2 + i18n/vi.json | 2 + i18n/zh-CN.json | 2 + i18n/zh-TW.json | 2 + internal/core/subscribers.go | 8 ++- internal/migrations/v2.5.0.go | 7 ++- models/models.go | 5 +- models/settings.go | 1 + queries.sql | 8 ++- schema.sql | 2 + 34 files changed, 122 insertions(+), 41 deletions(-) diff --git a/cmd/init.go b/cmd/init.go index d47c5058..40b57849 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -69,6 +69,7 @@ type constants struct { AllowBlocklist bool `koanf:"allow_blocklist"` AllowExport bool `koanf:"allow_export"` AllowWipe bool `koanf:"allow_wipe"` + RecordOptinIP bool `koanf:"record_optin_ip"` Exportable map[string]bool `koanf:"-"` DomainBlocklist []string `koanf:"-"` } `koanf:"privacy"` diff --git a/cmd/public.go b/cmd/public.go index 98cf57b0..561aaa9a 100644 --- a/cmd/public.go +++ b/cmd/public.go @@ -374,7 +374,16 @@ func handleOptinPage(c echo.Context) error { // Confirm. if confirm { - if err := app.core.ConfirmOptionSubscription(subUUID, out.ListUUIDs); err != nil { + meta := models.JSON{} + if app.constants.Privacy.RecordOptinIP { + if h := c.Request().Header.Get("X-Forwarded-For"); h != "" { + meta["optin_ip"] = h + } else if h := c.Request().RemoteAddr; h != "" { + meta["optin_ip"] = strings.Split(h, ":")[0] + } + } + + if err := app.core.ConfirmOptionSubscription(subUUID, out.ListUUIDs, meta); err != nil { app.log.Printf("error unsubscribing: %v", err) return c.Render(http.StatusInternalServerError, tplMessage, makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.Ts("public.errorProcessingRequest"))) diff --git a/frontend/src/views/SubscriberForm.vue b/frontend/src/views/SubscriberForm.vue index 0985d238..4f19cc28 100644 --- a/frontend/src/views/SubscriberForm.vue +++ b/frontend/src/views/SubscriberForm.vue @@ -75,39 +75,41 @@ -
+
{{ $tc('globals.terms.subscriptions', 2) }} ({{ data.lists.length }})
-
- - -
- - {{ props.row.name }} - -
- - - {{ ' ' }} - {{ $t(`lists.optins.${props.row.optin}`) }} - {{ ' ' }} -
-
- - {{ props.row.optin === 'double' ? props.row.subscriptionStatus : '-' }} - - - {{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }} - - - {{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }} - -
-
+ + +
+ + {{ props.row.name }} + +
+ + + {{ ' ' }} + {{ $t(`lists.optins.${props.row.optin}`) }} + {{ ' ' }} +
+
+ + {{ props.row.optin === 'double' ? props.row.subscriptionStatus : '-' }} + + + + {{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }} + + + {{ $utils.niceDate(props.row.subscriptionCreatedAt, true) }} + +
diff --git a/frontend/src/views/settings/privacy.vue b/frontend/src/views/settings/privacy.vue index 96251a69..af3c944d 100644 --- a/frontend/src/views/settings/privacy.vue +++ b/frontend/src/views/settings/privacy.vue @@ -36,6 +36,12 @@ name="privacy.allow_wipe" /> + + + + 0 THEN id = $1 ELSE uuid = $2 END ) -SELECT lists.*, subscriber_lists.status as subscription_status, subscriber_lists.created_at as subscription_created_at +SELECT lists.*, + subscriber_lists.status as subscription_status, + subscriber_lists.created_at as subscription_created_at, + subscriber_lists.meta as subscription_meta FROM lists LEFT JOIN subscriber_lists ON (subscriber_lists.list_id = lists.id AND subscriber_lists.subscriber_id = (SELECT id FROM sub)) WHERE CASE WHEN $3 = TRUE THEN TRUE ELSE subscriber_lists.status IS NOT NULL END @@ -214,7 +218,7 @@ WITH subID AS ( listIDs AS ( SELECT id FROM lists WHERE uuid = ANY($2::UUID[]) ) -UPDATE subscriber_lists SET status='confirmed', updated_at=NOW() +UPDATE subscriber_lists SET status='confirmed', meta=meta || $3, updated_at=NOW() WHERE subscriber_id = (SELECT id FROM subID) AND list_id = ANY(SELECT id FROM listIDs); -- name: unsubscribe-subscribers-from-lists diff --git a/schema.sql b/schema.sql index 260d1937..f8e5b451 100644 --- a/schema.sql +++ b/schema.sql @@ -43,6 +43,7 @@ DROP TABLE IF EXISTS subscriber_lists CASCADE; CREATE TABLE subscriber_lists ( subscriber_id INTEGER REFERENCES subscribers(id) ON DELETE CASCADE ON UPDATE CASCADE, list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE, + meta JSONB NOT NULL DEFAULT '{}', status subscription_status NOT NULL DEFAULT 'unconfirmed', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), @@ -225,6 +226,7 @@ INSERT INTO settings (key, value) VALUES ('privacy.allow_preferences', 'true'), ('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'), ('privacy.domain_blocklist', '[]'), + ('privacy.record_optin_ip', 'false'), ('security.enable_captcha', 'false'), ('security.captcha_key', '""'), ('security.captcha_secret', '""'),