diff --git a/cmd/init.go b/cmd/init.go
index d47c505..40b5784 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 98cf57b..561aaa9 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 0985d23..4f19cc2 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 : '-' }}
+
+
{{ props.row.subscriptionMeta.optinIp }}
+
+
+
+ {{ $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 96251a6..af3c944 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 260d193..f8e5b45 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', '""'),