From 1198f6f3afdd9051b41af80436d9e723788d70df Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Wed, 13 Nov 2024 00:28:46 +0530
Subject: [PATCH 001/145] Update release link on static site homepage.
---
docs/site/data/github.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/site/data/github.json b/docs/site/data/github.json
index 60162d273..99a4fb61f 100644
--- a/docs/site/data/github.json
+++ b/docs/site/data/github.json
@@ -1 +1 @@
-{"version":"v4.0.1","date":"2024-10-28T07:56:09Z","url":"https://github.com/knadh/listmonk/releases/tag/v4.0.1","assets":[{"name":"darwin","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_darwin_amd64.tar.gz"},{"name":"freebsd","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_freebsd_amd64.tar.gz"},{"name":"linux","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_linux_amd64.tar.gz"},{"name":"netbsd","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_netbsd_amd64.tar.gz"},{"name":"openbsd","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_openbsd_amd64.tar.gz"},{"name":"windows","url":"https://github.com/knadh/listmonk/releases/download/v4.0.1/listmonk_4.0.1_windows_amd64.tar.gz"}]}
+{"version":"v4.1.0","date":"2024-11-12T18:49:52Z","url":"https://github.com/knadh/listmonk/releases/tag/v4.1.0","assets":[{"name":"darwin","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_darwin_amd64.tar.gz"},{"name":"freebsd","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_freebsd_amd64.tar.gz"},{"name":"linux","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_linux_amd64.tar.gz"},{"name":"netbsd","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_netbsd_amd64.tar.gz"},{"name":"openbsd","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_openbsd_amd64.tar.gz"},{"name":"windows","url":"https://github.com/knadh/listmonk/releases/download/v4.1.0/listmonk_4.1.0_windows_amd64.tar.gz"}]}
From 392bc87225266edddf9328105fb2eb2de06da6ea Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Fri, 15 Nov 2024 12:02:09 +0530
Subject: [PATCH 002/145] Add curl example to subscriber import docs.
---
docs/docs/content/apis/import.md | 25 +++++++++++++++++++++----
1 file changed, 21 insertions(+), 4 deletions(-)
diff --git a/docs/docs/content/apis/import.md b/docs/docs/content/apis/import.md
index 92b3d5e0b..4345e1ce0 100644
--- a/docs/docs/content/apis/import.md
+++ b/docs/docs/content/apis/import.md
@@ -11,7 +11,7 @@ ______________________________________________________________________
#### GET /api/import/subscribers
-Retrieve the status of an import.
+Retrieve the status of an ongoing import.
##### Example Request
@@ -36,7 +36,7 @@ ______________________________________________________________________
#### GET /api/import/subscribers/logs
-Retrieve logs related to imports.
+Retrieve logs from an ongoing import.
##### Example Request
@@ -63,9 +63,26 @@ Send a CSV (optionally ZIP compressed) file to import subscribers. Use a multipa
| Name | Type | Required | Description |
|:-------|:------------|:---------|:-----------------------------------------|
| params | JSON string | Yes | Stringified JSON with import parameters. |
-| file | File | Yes | File for upload. |
+| file | file | Yes | File for upload. |
-**`params`** (JSON string)
+
+#### `params` (JSON string)
+| Name | Type | Required | Description |
+|:----------|:---------|:---------|:-----------------------------------------------------------------------------------------------------------------------------------|
+| mode | string | Yes | `subscribe` or `blocklist` |
+| delim | string | Yes | Single character indicating delimiter used in the CSV file, eg: `,` |
+| lists | []number | Yes | Single character indicating delimiter used in the CSV file, eg: `,` |
+| overwrite | bool | Yes | Whether to overwrite the subscriber parameters including subscriptions or ignore records that are already present in the database. |
+
+##### Example Request
+
+```shell
+curl -u "username:password" -X POST 'http://localhost:9000/api/import/subscribers' \
+ -F 'params={"mode":"subscribe", "subscription_status":"confirmed", "delim":",", "lists":[1, 2], "overwrite": true}' \
+ -F "file=@/path/to/subs.csv"
+```
+
+##### Example Response
```json
{
From 1451c4b1f1feb00fb8fedda784e75cf5ddeb5769 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Fri, 15 Nov 2024 12:18:44 +0530
Subject: [PATCH 003/145] Improve API auth/permission docs and examples.
---
docs/docs/content/apis/apis.md | 34 ++++++++++++++++++++++---
docs/docs/content/apis/bounces.md | 2 +-
docs/docs/content/apis/campaigns.md | 20 +++++++--------
docs/docs/content/apis/import.md | 8 +++---
docs/docs/content/apis/lists.md | 8 +++---
docs/docs/content/apis/media.md | 6 ++---
docs/docs/content/apis/templates.md | 12 ++++-----
docs/docs/content/apis/transactional.md | 4 +--
8 files changed, 60 insertions(+), 34 deletions(-)
diff --git a/docs/docs/content/apis/apis.md b/docs/docs/content/apis/apis.md
index 4e6af46ab..ad1761491 100644
--- a/docs/docs/content/apis/apis.md
+++ b/docs/docs/content/apis/apis.md
@@ -2,13 +2,33 @@
All features that are available on the listmonk dashboard are also available as REST-like HTTP APIs that can be interacted with directly. Request and response bodies are JSON. This allows easy scripting of listmonk and integration with other systems, for instance, synchronisation with external subscriber databases.
-API requests require BasicAuth authentication with the admin credentials.
+!!! note
+ If you come across API calls that are yet to be documented, please consider contributing to docs.
-> The API section is a work in progress. There may be API calls that are yet to be documented. Please consider contributing to docs.
-## OpenAPI (Swagger) spec
+## Auth
+HTTP API requests support BasicAuth and a Authorization `token` headers. API users and tokens with the required permissions can be created and managed on the admin UI (Admin -> Users).
-The auto-generated OpenAPI (Swagger) specification site for the APIs are available at [**listmonk.app/docs/swagger**](https://listmonk.app/docs/swagger/)
+##### BasicAuth example
+```shell
+curl -u "api_user:token" http://localhost:9000/api/lists
+```
+
+##### Authorization token example
+```shell
+curl -H "Authorization: token api_user:token" http://localhost:9000/api/lists
+```
+
+## Permissions
+**User role**: Permissions allowed for a user are defined as a *User role* (Admin -> User roles) and then attached to a user.
+
+**List role**: Read / write permissions per-list can be defined as a *List role* (Admin -> User roles) and then attached to a user.
+
+In a *User role*, `lists:get_all` or `lists:manage_all` permission supercede and override any list specific permissions for a user defined in a *List role*.
+
+To manage lists and subscriber list subscriptions via API requests, ensure that the appropriate permissions are attached to the API user.
+
+______________________________________________________________________
## Response structure
@@ -57,3 +77,9 @@ All timestamp fields are in the format `2019-01-01T09:00:00.000000+05:30`. The s
| 502 | The backend OMS is down and the API is unable to communicate with it |
| 503 | Service unavailable; the API is down |
| 504 | Gateway timeout; the API is unreachable |
+
+
+## OpenAPI (Swagger) spec
+
+The auto-generated OpenAPI (Swagger) specification site for the APIs are available at [**listmonk.app/docs/swagger**](https://listmonk.app/docs/swagger/)
+
diff --git a/docs/docs/content/apis/bounces.md b/docs/docs/content/apis/bounces.md
index 6c4568b92..bcc13305a 100644
--- a/docs/docs/content/apis/bounces.md
+++ b/docs/docs/content/apis/bounces.md
@@ -27,7 +27,7 @@ Retrieve the bounce records.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/bounces?campaign_id=1&page=1&per_page=2' \
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/bounces?campaign_id=1&page=1&per_page=2' \
-H 'accept: application/json' -H 'Content-Type: application/x-www-form-urlencoded' \
--data '{"source":"demo","order_by":"created_at","order":"asc"}'
```
diff --git a/docs/docs/content/apis/campaigns.md b/docs/docs/content/apis/campaigns.md
index ffa8faf6e..06140e117 100644
--- a/docs/docs/content/apis/campaigns.md
+++ b/docs/docs/content/apis/campaigns.md
@@ -23,7 +23,7 @@ Retrieve all campaigns.
##### Example Request
```shell
- curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns?page=1&per_page=100'
+ curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns?page=1&per_page=100'
```
##### Parameters
@@ -98,7 +98,7 @@ Retrieve a specific campaign.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/1'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns/1'
```
##### Example Response
@@ -153,7 +153,7 @@ Preview a specific campaign.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/1/preview'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns/1/preview'
```
##### Example Response
@@ -178,7 +178,7 @@ Retrieve stats of specified campaigns.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/running/stats?campaign_id=1'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns/running/stats?campaign_id=1'
```
##### Example Response
@@ -208,7 +208,7 @@ Retrieve stats of specified campaigns.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/analytics/views?id=1&from=2024-08-04&to=2024-08-12'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns/analytics/views?id=1&from=2024-08-04&to=2024-08-12'
```
##### Example Response
@@ -248,7 +248,7 @@ curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/analytic
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/analytics/links?id=1&from=2024-08-04T18%3A30%3A00.624Z&to=2024-08-12T18%3A29%3A00.624Z'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/campaigns/analytics/links?id=1&from=2024-08-04T18%3A30%3A00.624Z&to=2024-08-12T18%3A29%3A00.624Z'
```
##### Example Response
@@ -303,7 +303,7 @@ Create a new campaign.
##### Example request
```shell
-curl -u "username:password" 'http://localhost:9000/api/campaigns' -X POST -H 'Content-Type: application/json;charset=utf-8' --data-raw '{"name":"Test campaign","subject":"Hello, world","lists":[1],"from_email":"listmonk ","content_type":"richtext","messenger":"email","type":"regular","tags":["test"],"template_id":1}'
+curl -u "api_user:token" 'http://localhost:9000/api/campaigns' -X POST -H 'Content-Type: application/json;charset=utf-8' --data-raw '{"name":"Test campaign","subject":"Hello, world","lists":[1],"from_email":"listmonk ","content_type":"richtext","messenger":"email","type":"regular","tags":["test"],"template_id":1}'
```
##### Example response
@@ -394,7 +394,7 @@ Change status of a campaign.
##### Example Request
```shell
-curl -u "username:password" -X PUT 'http://localhost:9000/api/campaigns/1/status' \
+curl -u "api_user:token" -X PUT 'http://localhost:9000/api/campaigns/1/status' \
--header 'Content-Type: application/json' \
--data-raw '{"status":"scheduled"}'
```
@@ -457,7 +457,7 @@ Publish campaign to public archive.
```shell
-curl -u "username:password" -X PUT 'http://localhost:8080/api/campaigns/33/archive'
+curl -u "api_user:token" -X PUT 'http://localhost:8080/api/campaigns/33/archive'
--header 'Content-Type: application/json'
--data-raw '{"archive":true,"archive_template_id":1,"archive_meta":{},"archive_slug":"my-newsletter-old-edition"}'
```
@@ -490,7 +490,7 @@ Delete a campaign.
##### Example Request
```shell
-curl -u "username:password" -X DELETE 'http://localhost:9000/api/campaigns/34'
+curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/campaigns/34'
```
##### Example Response
diff --git a/docs/docs/content/apis/import.md b/docs/docs/content/apis/import.md
index 4345e1ce0..e565e6a50 100644
--- a/docs/docs/content/apis/import.md
+++ b/docs/docs/content/apis/import.md
@@ -16,7 +16,7 @@ Retrieve the status of an ongoing import.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/import/subscribers'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/import/subscribers'
```
##### Example Response
@@ -41,7 +41,7 @@ Retrieve logs from an ongoing import.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/import/subscribers/logs'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/import/subscribers/logs'
```
##### Example Response
@@ -77,7 +77,7 @@ Send a CSV (optionally ZIP compressed) file to import subscribers. Use a multipa
##### Example Request
```shell
-curl -u "username:password" -X POST 'http://localhost:9000/api/import/subscribers' \
+curl -u "api_user:token" -X POST 'http://localhost:9000/api/import/subscribers' \
-F 'params={"mode":"subscribe", "subscription_status":"confirmed", "delim":",", "lists":[1, 2], "overwrite": true}' \
-F "file=@/path/to/subs.csv"
```
@@ -102,7 +102,7 @@ Stop and delete an ongoing import.
##### Example Request
```shell
-curl -u "username:password" -X DELETE 'http://localhost:9000/api/import/subscribers'
+curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/import/subscribers'
```
##### Example Response
diff --git a/docs/docs/content/apis/lists.md b/docs/docs/content/apis/lists.md
index 4db390691..f89837242 100644
--- a/docs/docs/content/apis/lists.md
+++ b/docs/docs/content/apis/lists.md
@@ -30,7 +30,7 @@ Retrieve lists.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/lists?page=1&per_page=100'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/lists?page=1&per_page=100'
```
##### Example Response
@@ -108,7 +108,7 @@ Retrieve a specific list.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/lists/5'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/lists/5'
```
##### Example Response
@@ -148,7 +148,7 @@ Create a new list.
##### Example Request
```shell
-curl -u "username:password" -X POST 'http://localhost:9000/api/lists'
+curl -u "api_user:token" -X POST 'http://localhost:9000/api/lists'
```
##### Example Response
@@ -190,7 +190,7 @@ Update a list.
##### Example Request
```shell
-curl -u "username:password" -X PUT 'http://localhost:9000/api/lists/5' \
+curl -u "api_user:token" -X PUT 'http://localhost:9000/api/lists/5' \
--form 'name=modified test list' \
--form 'type=private'
```
diff --git a/docs/docs/content/apis/media.md b/docs/docs/content/apis/media.md
index 3fffa0823..144e843ef 100644
--- a/docs/docs/content/apis/media.md
+++ b/docs/docs/content/apis/media.md
@@ -16,7 +16,7 @@ Get an uploaded media file.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/media' \
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/media' \
--header 'Content-Type: multipart/form-data; boundary=--------------------------093715978792575906250298'
```
@@ -87,7 +87,7 @@ Upload a media file.
##### Example Request
```shell
-curl -u "username:password" -X POST 'http://localhost:9000/api/media' \
+curl -u "api_user:token" -X POST 'http://localhost:9000/api/media' \
--header 'Content-Type: multipart/form-data; boundary=--------------------------183679989870526937212428' \
--form 'file=@/path/to/image.jpg'
```
@@ -122,7 +122,7 @@ Delete an uploaded media file.
##### Example Request
```shell
-curl -u "username:password" -X DELETE 'http://localhost:9000/api/media/1'
+curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/media/1'
```
##### Example Response
diff --git a/docs/docs/content/apis/templates.md b/docs/docs/content/apis/templates.md
index 3984f1867..0d2a0eb23 100644
--- a/docs/docs/content/apis/templates.md
+++ b/docs/docs/content/apis/templates.md
@@ -20,7 +20,7 @@ Retrieve all templates.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/templates'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/templates'
```
##### Example Response
@@ -56,7 +56,7 @@ Retrieve a specific template.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/templates/1'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/templates/1'
```
##### Example Response
@@ -90,7 +90,7 @@ Retrieve the HTML preview of a template.
##### Example Request
```shell
-curl -u "username:password" -X GET 'http://localhost:9000/api/templates/1/preview'
+curl -u "api_user:token" -X GET 'http://localhost:9000/api/templates/1/preview'
```
##### Example Response
@@ -128,7 +128,7 @@ Create a template.
##### Example Request
```shell
-curl -u "username:password" -X POST 'http://localhost:9000/api/templates' \
+curl -u "api_user:token" -X POST 'http://localhost:9000/api/templates' \
-H 'Content-Type: application/json' \
-d '{
"name": "New template",
@@ -179,7 +179,7 @@ Set a template as the default.
##### Example Request
```shell
-curl -u "username:password" -X PUT 'http://localhost:9000/api/templates/1/default'
+curl -u "api_user:token" -X PUT 'http://localhost:9000/api/templates/1/default'
```
##### Example Response
@@ -213,7 +213,7 @@ Delete a template.
##### Example Request
```shell
-curl -u "username:password" -X DELETE 'http://localhost:9000/api/templates/35'
+curl -u "api_user:token" -X DELETE 'http://localhost:9000/api/templates/35'
```
##### Example Response
diff --git a/docs/docs/content/apis/transactional.md b/docs/docs/content/apis/transactional.md
index 130fa756e..80207ec73 100644
--- a/docs/docs/content/apis/transactional.md
+++ b/docs/docs/content/apis/transactional.md
@@ -28,7 +28,7 @@ Allows sending transactional messages to one or more subscribers via a preconfig
##### Example
```shell
-curl -u "username:password" "http://localhost:9000/api/tx" -X POST \
+curl -u "api_user:token" "http://localhost:9000/api/tx" -X POST \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
@@ -55,7 +55,7 @@ ______________________________________________________________________
To include file attachments in a transactional message, use the `multipart/form-data` Content-Type. Use `data` param for the parameters described above as a JSON object. Include any number of attachments via the `file` param.
```shell
-curl -u "username:password" "http://localhost:9000/api/tx" -X POST \
+curl -u "api_user:token" "http://localhost:9000/api/tx" -X POST \
-F 'data=\"{
\"subscriber_email\": \"user@test.com\",
\"template_id\": 4
From ee9677cd8e06523c4efa30006ef6d41403ce6740 Mon Sep 17 00:00:00 2001
From: Ulf Seltmann
Date: Fri, 15 Nov 2024 08:53:47 +0100
Subject: [PATCH 004/145] removes example properties from attribs property and
allow additional properties (#2154)
---
docs/swagger/collections.yaml | 42 ++++-------------------------------
1 file changed, 4 insertions(+), 38 deletions(-)
diff --git a/docs/swagger/collections.yaml b/docs/swagger/collections.yaml
index bf416160d..f6b155cd5 100644
--- a/docs/swagger/collections.yaml
+++ b/docs/swagger/collections.yaml
@@ -3448,13 +3448,7 @@ components:
type: string
attribs:
type: object
- properties:
- city:
- type: string
- good:
- type: boolean
- type:
- type: string
+ additionalProperties: true
status:
type: string
created_at:
@@ -3513,13 +3507,7 @@ components:
type: string
attribs:
type: object
- properties:
- city:
- type: string
- good:
- type: boolean
- type:
- type: string
+ additionalProperties: true
status:
type: string
lists:
@@ -3567,18 +3555,7 @@ components:
type: boolean
attribs:
type: object
- properties:
- city:
- type: string
- projects:
- type: integer
- stack:
- type: object
- properties:
- languages:
- type: array
- items:
- type: string
+ additionalProperties: true
UpdateSubscriber:
type: object
@@ -3601,18 +3578,7 @@ components:
type: boolean
attribs:
type: object
- properties:
- city:
- type: string
- projects:
- type: integer
- stack:
- type: object
- properties:
- languages:
- type: array
- items:
- type: string
+ additionalProperties: true
SubscriberQueryRequest:
type: object
From 882c49f1dd5e7984b2127e5ac7bfa23fca8fff33 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 19 Nov 2024 10:14:02 +0530
Subject: [PATCH 005/145] Bump cross-spawn from 7.0.3 to 7.0.6 in /frontend
(#2169)
Bumps [cross-spawn](https://github.com/moxystudio/node-cross-spawn) from 7.0.3 to 7.0.6.
- [Changelog](https://github.com/moxystudio/node-cross-spawn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/moxystudio/node-cross-spawn/compare/v7.0.3...v7.0.6)
---
updated-dependencies:
- dependency-name: cross-spawn
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
frontend/yarn.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 7588fe526..3f228dbb2 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -912,9 +912,9 @@ core-util-is@1.0.2:
integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==
cross-spawn@^7.0.0, cross-spawn@^7.0.2:
- version "7.0.3"
- resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
- integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
From fce46b2103b121746044990afdf8fedee4e62f18 Mon Sep 17 00:00:00 2001
From: Dylan Cetin <74064578+dylancetin@users.noreply.github.com>
Date: Mon, 25 Nov 2024 07:49:12 +0300
Subject: [PATCH 006/145] fix: update OpenAPI specs for routes and correct docs
for campaign endpoints (#2179)
* Fix API Docs for campaign routes' params
1. Added no_body for some. These existed in OpenApi spec but was not present in the docs
2. GET /api/campaigns/analytics/{type} fixed params description for "from" and "to"
* OpenApi Specs GET routes and File Type Fix
This commit fixes some routes' openapi specs by following lismonk.app/docs/
1. GET Routes can't have a body. All params spesified in the body is moved up to query params
2. On the route /import/subscribers POST fixed content type to Multipart, added example params and fixed file type to string/binary
3. And added required: false to many
---
docs/docs/content/apis/campaigns.md | 6 +-
docs/swagger/collections.yaml | 243 +++++++++++++++++++---------
2 files changed, 169 insertions(+), 80 deletions(-)
diff --git a/docs/docs/content/apis/campaigns.md b/docs/docs/content/apis/campaigns.md
index 06140e117..1c7d7bbd6 100644
--- a/docs/docs/content/apis/campaigns.md
+++ b/docs/docs/content/apis/campaigns.md
@@ -37,6 +37,7 @@ Retrieve all campaigns.
| tags | []string | | Tags to filter campaigns. Repeat in the query for multiple values. |
| page | number | | Page number for paginated results. |
| per_page | number | | Results per page. Set as 'all' for all results. |
+| no_body | boolean | | When set to true, returns response without body content. |
##### Example Response
@@ -94,6 +95,7 @@ Retrieve a specific campaign.
| Name | Type | Required | Description |
|:------------|:----------|:---------|:-------------|
| campaign_id | number | Yes | Campaign ID. |
+| no_body | boolean | | When set to true, returns response without body content. |
##### Example Request
@@ -201,8 +203,8 @@ Retrieve stats of specified campaigns.
|:------------|:----------|:---------|:----------------------------------------------|
| id |number\[\] | Yes | Campaign IDs to get stats for. |
| type |string | Yes | Analytics type: views, links, clicks, bounces |
-| from |string | Yes | Campaign IDs to get stats for. |
-| to |string | Yes | Campaign IDs to get stats for. |
+| from |string | Yes | Start value of date range. |
+| to |string | Yes | End value of date range. |
##### Example Request
diff --git a/docs/swagger/collections.yaml b/docs/swagger/collections.yaml
index f6b155cd5..cec5fc78a 100644
--- a/docs/swagger/collections.yaml
+++ b/docs/swagger/collections.yaml
@@ -256,21 +256,56 @@ paths:
parameters:
- in: query
name: page
- description: number of records to skip
+ description: Page number for paginated results.
+ required: false
schema:
type: integer
format: int32
- in: query
name: per_page
- description: max number of records to return per page
+ description: Number of items per page. Use an integer for specific page size or 'all' to retrieve all results
+ required: false
schema:
- type: integer
- format: int32
+ oneOf:
+ - type: integer
+ description: Number of items to return per page
+ - type: string
+ enum: ["all"]
+ description: Return all results without pagination
- in: query
name: query
description: query subscribers with an SQL expression.
+ required: false
+ schema:
+ type: string
+ - in: query
+ name: order_by
+ description: Result sorting field. Options are name, status, created_at, updated_at
+ required: false
schema:
type: string
+ enum: ["name", "status", "created_at", "updated_at"]
+ - in: query
+ name: order
+ description: ASC|DESC Sort by ascending or descending order.
+ required: false
+ schema:
+ type: string
+ enum: ["ASC", "DESC"]
+ - in: query
+ name: subscription_status
+ description: Subscription status to filter by if there are one or more list_ids.
+ required: false
+ schema:
+ type: string
+ - in: query
+ name: list_id
+ description: ID of lists to filter by. Repeat in the query for multiple values.
+ required: false
+ schema:
+ type: array
+ items:
+ type: integer
responses:
"200":
@@ -719,36 +754,45 @@ paths:
operationId: getBounces
tags:
- Bounces
- requestBody:
- description: output parameters form
- required: true
- content:
- application/x-www-form-urlencoded:
- schema:
- type: object
- properties:
- source:
- type: string
- order_by:
- type: string
- order:
- type: string
parameters:
- in: query
name: campaign_id
- description: bounce record retrieval of particular campaign
+ description: Numeric identifier for retrieving bounce records associated with a specific campaign
schema:
type: integer
- in: query
name: page
- description: total number of pages
+ description: Page number for paginated results. Start from 1 for the first page
schema:
type: integer
- in: query
name: per_page
- description: number of items per page
+ description: Number of items per page. Use an integer for specific page size or 'all' to retrieve all results
schema:
- type: integer
+ oneOf:
+ - type: integer
+ description: Number of items to return per page
+ - type: string
+ enum:
+ - "all"
+ description: Return all results without pagination
+ - in: query
+ name: source
+ description: Filter bounce records by their source of origin
+ schema:
+ type: string
+ - in: query
+ name: order_by
+ description: Specifies the field by which to sort the bounce records. Available options are 'email', 'campaign_name', 'source', and 'created_at'
+ schema:
+ type: string
+ enum: ["email", "campaign_name", "source", "created_at"]
+ - in: query
+ name: order
+ description: Determines the sort order of results. Use 'asc' for ascending or 'desc' for descending order
+ schema:
+ type: string
+ enum: ["asc", "desc"]
responses:
"200":
description: list of bounce records
@@ -856,32 +900,54 @@ paths:
- in: query
name: page
description: total number of pages
+ required: false
schema:
type: integer
- in: query
name: per_page
description: number of items per page
+ required: false
schema:
- type: integer
- requestBody:
- required: true
- description: output parameters form
- content:
- application/x-www-form-urlencoded:
- schema:
- type: object
- properties:
- query:
- description: Optional string to search a list by name.
- type: string
- order_by:
- description: Field to sort results by. name|status|created_at|updated_at
- type: string
- order:
- description: ASC|DESC Sort by ascending or descending order.
- type: string
- minimal:
- type: boolean
+ oneOf:
+ - type: integer
+ description: Number of items to return per page
+ - type: string
+ enum: ["all"]
+ description: Return all results without pagination
+ - in: query
+ name: query
+ description: Optional string to search a list by name.
+ required: false
+ schema:
+ type: string
+ - in: query
+ name: order_by
+ description: Field to sort results by. name|status|created_at|updated_at
+ required: false
+ schema:
+ type: string
+ enum: ["name", "status", "created_at", "updated_at"]
+ - in: query
+ name: order
+ description: ASC|DESC Sort by ascending or descending order.
+ required: false
+ schema:
+ type: string
+ enum: ["ASC", "DESC"]
+ - in: query
+ name: minimal
+ description: When set to true, returns response without body content
+ required: false
+ schema:
+ type: boolean
+ - in: query
+ name: tag
+ description: Tags to filter lists. Repeat in the query for multiple values.
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
responses:
"200":
description: list of metadata
@@ -1027,14 +1093,18 @@ paths:
requestBody:
description: uploads and bulk imports of compressed CSV files
content:
- application/json:
+ multipart/form-data:
schema:
type: object
properties:
params:
type: string
+ description: JSON string containing import parameters for more detail https://listmonk.app/docs/apis/import/#params-json-string
+ example: '{"mode":"subscribe", "subscription_status":"confirmed", "delim":",", "lists":[1, 2], "overwrite": true}'
file:
type: string
+ format: binary
+ description: File for upload.
responses:
"200":
description: updated import status
@@ -1086,45 +1156,64 @@ paths:
parameters:
- in: query
name: status
- description: status flag of campaign
+ description: Filter campaigns by status. Multiple status values can be specified by repeating the parameter
+ required: false
schema:
type: array
items:
type: string
- enum: [scheduled, running, paused, cancelled]
+ enum: ["scheduled", "running", "paused", "cancelled"]
- in: query
name: no_body
- description: boolean flag for response with/without body
+ description: When set to true, returns response without body content
+ required: false
schema:
type: boolean
- in: query
name: page
- description: total number of pages
+ description: Page number for paginated results.
+ required: false
schema:
type: integer
- in: query
name: per_page
- description: number of items per page
+ description: Number of items per page. Use an integer for specific page size or 'all' to retrieve all results
+ required: false
schema:
- type: integer
- requestBody:
- required: true
- description: output parameters form
- content:
- application/x-www-form-urlencoded:
- schema:
- type: object
- properties:
- query:
- description: Optional string to search a list by name.
- type: string
- order_by:
- description: Field to sort results by. name|status|created_at|updated_at
- type: string
- order:
- description: ASC|DESC Sort by ascending or descending order.
- type: string
-
+ oneOf:
+ - type: integer
+ description: Number of items to return per page
+ - type: string
+ enum: ["all"]
+ description: Return all results without pagination
+ - in: query
+ name: tags
+ description: Filter campaigns by tags. Multiple tags can be specified by repeating the parameter
+ required: false
+ schema:
+ type: array
+ items:
+ type: string
+ - in: query
+ name: order
+ description: Determines the sort order of results. ASC for ascending, DESC for descending order
+ required: false
+ schema:
+ type: string
+ enum: ["ASC", "DESC"]
+ - in: query
+ name: order_by
+ description: Specifies the field by which to sort the campaigns. Available options are 'name', 'status', 'created_at', and 'updated_at'
+ required: false
+ schema:
+ type: string
+ enum: ["name", "status", "created_at", "updated_at"]
+ - in: query
+ name: query
+ description: SQL query expression to filter campaigns by custom criteria
+ required: false
+ schema:
+ type: string
tags:
- Campaigns
responses:
@@ -1262,6 +1351,13 @@ paths:
operationId: getRunningCampaignStats
tags:
- Campaigns
+ parameters:
+ - in: query
+ name: campaign_id
+ description: Campaign IDs to get stats for.
+ required: true
+ schema:
+ type: number
responses:
"200":
description: list of stats for given set of campaign ids
@@ -1295,6 +1391,7 @@ paths:
description: start value of date range
schema:
type: string
+ format: date
- in: query
required: true
name: to
@@ -1304,6 +1401,7 @@ paths:
- in: query
name: id
description: campaign id/s to retrive view counts
+ required: true
schema:
type: string
responses:
@@ -1332,17 +1430,6 @@ paths:
description: The id value of the campaign you want to get the preview of
schema:
type: integer
- requestBody:
- required: true
- description: template id
- content:
- application/x-www-form-urlencoded:
- schema:
- type: object
- properties:
- template_id:
- description: template id
- type: integer
responses:
"200":
description: HTML Preview of requested campaign
From 3f5bad1a9a9db08494400d639c6f58faf497f7c1 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Mon, 25 Nov 2024 10:20:08 +0530
Subject: [PATCH 007/145] Fix incorrent curl command in docs.
---
docs/docs/content/apis/bounces.md | 6 ++---
docs/docs/content/apis/lists.md | 2 +-
docs/docs/content/apis/media.md | 2 +-
docs/docs/content/apis/subscribers.md | 32 +++++++++++++--------------
docs/docs/content/bounces.md | 2 +-
5 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/docs/docs/content/apis/bounces.md b/docs/docs/content/apis/bounces.md
index bcc13305a..9823c992a 100644
--- a/docs/docs/content/apis/bounces.md
+++ b/docs/docs/content/apis/bounces.md
@@ -94,7 +94,7 @@ To delete all bounces.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces?all=true'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces?all=true'
```
##### Example Response
@@ -120,7 +120,7 @@ To delete multiple bounce records.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces?id=840965&id=840168&id=840879'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces?id=840965&id=840168&id=840879'
```
##### Example Response
@@ -140,7 +140,7 @@ To delete specific bounce id.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces/840965'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/bounces/840965'
```
##### Example Response
diff --git a/docs/docs/content/apis/lists.md b/docs/docs/content/apis/lists.md
index f89837242..ef62da22b 100644
--- a/docs/docs/content/apis/lists.md
+++ b/docs/docs/content/apis/lists.md
@@ -229,7 +229,7 @@ Delete a specific subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/lists/1'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/lists/1'
```
##### Example Response
diff --git a/docs/docs/content/apis/media.md b/docs/docs/content/apis/media.md
index 144e843ef..6f2654547 100644
--- a/docs/docs/content/apis/media.md
+++ b/docs/docs/content/apis/media.md
@@ -51,7 +51,7 @@ Retrieve a specific media.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/media/7'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/media/7'
```
##### Example Response
diff --git a/docs/docs/content/apis/subscribers.md b/docs/docs/content/apis/subscribers.md
index 707483552..2ac6529ff 100644
--- a/docs/docs/content/apis/subscribers.md
+++ b/docs/docs/content/apis/subscribers.md
@@ -40,15 +40,15 @@ Retrieve all subscribers.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers?page=1&per_page=100'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers?page=1&per_page=100'
```
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers?list_id=1&list_id=2&page=1&per_page=100'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers?list_id=1&list_id=2&page=1&per_page=100'
```
```shell
-curl -u curl -u 'api_username:access_token' -X GET 'http://localhost:9000/api/subscribers' \
+curl -u 'api_username:access_token' -X GET 'http://localhost:9000/api/subscribers' \
--url-query 'page=1' \
--url-query 'per_page=100' \
--url-query "query=subscribers.name LIKE 'Test%' AND subscribers.attribs->>'city' = 'Bengaluru'"
@@ -147,7 +147,7 @@ Retrieve a specific subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1'
```
##### Example Response
@@ -199,7 +199,7 @@ Export a specific subscriber data that gives profile, list subscriptions, campai
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1/export'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1/export'
```
##### Example Response
@@ -248,7 +248,7 @@ Get a specific subscriber bounce records.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1/bounces'
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/1/bounces'
```
##### Example Response
@@ -312,7 +312,7 @@ Create a new subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers' -H 'Content-Type: application/json' \
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers' -H 'Content-Type: application/json' \
--data '{"email":"subsriber@domain.com","name":"The Subscriber","status":"enabled","lists":[1],"attribs":{"city":"Bengaluru","projects":3,"stack":{"languages":["go","python"]}}}'
```
@@ -347,7 +347,7 @@ Sends optin confirmation email to subscribers.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/11/optin' -H 'Content-Type: application/json' \
+curl -u 'api_username:access_token' 'http://localhost:9000/api/subscribers/11/optin' -H 'Content-Type: application/json' \
--data {}
```
@@ -414,7 +414,7 @@ Modify subscriber list memberships.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X PUT 'http://localhost:9000/api/subscribers/lists' \
+curl -u 'api_username:access_token' -X PUT 'http://localhost:9000/api/subscribers/lists' \
-H 'Content-Type: application/json' \
--data-raw '{"ids": [1, 2, 3], "action": "add", "target_list_ids": [4, 5, 6], "status": "confirmed"}'
```
@@ -450,7 +450,7 @@ Blocklist a specific subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X PUT 'http://localhost:9000/api/subscribers/9/blocklist'
+curl -u 'api_username:access_token' -X PUT 'http://localhost:9000/api/subscribers/9/blocklist'
```
##### Example Response
@@ -476,7 +476,7 @@ Blocklist multiple subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X PUT 'http://localhost:8080/api/subscribers/blocklist' -H 'Content-Type: application/json' --data-raw '{"ids":[2,1]}'
+curl -u 'api_username:access_token' -X PUT 'http://localhost:8080/api/subscribers/blocklist' -H 'Content-Type: application/json' --data-raw '{"ids":[2,1]}'
```
##### Example Response
@@ -505,7 +505,7 @@ Blocklist subscribers based on SQL expression.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X POST 'http://localhost:9000/api/subscribers/query/blocklist' \
+curl -u 'api_username:access_token' -X POST 'http://localhost:9000/api/subscribers/query/blocklist' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"subscribers.name LIKE \'John Doe\' AND subscribers.attribs->>'\''city'\'' = '\''Bengaluru'\''"}'
```
@@ -533,7 +533,7 @@ Delete a specific subscriber.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers/9'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers/9'
```
##### Example Response
@@ -559,7 +559,7 @@ Delete a subscriber's bounce records
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers/9/bounces'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers/9/bounces'
```
##### Example Response
@@ -585,7 +585,7 @@ Delete one or more subscribers.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers?id=10&id=11'
+curl -u 'api_username:access_token' -X DELETE 'http://localhost:9000/api/subscribers?id=10&id=11'
```
##### Example Response
@@ -613,7 +613,7 @@ Delete subscribers based on SQL expression.
##### Example Request
```shell
-curl -u curl -u 'api_username:access_token' -X POST 'http://localhost:9000/api/subscribers/query/delete' \
+curl -u 'api_username:access_token' -X POST 'http://localhost:9000/api/subscribers/query/delete' \
-H 'Content-Type: application/json' \
--data-raw '{"query":"subscribers.name LIKE \'John Doe\' AND subscribers.attribs->>'\''city'\'' = '\''Bengaluru'\''"}'
```
diff --git a/docs/docs/content/bounces.md b/docs/docs/content/bounces.md
index e67e895fc..84d76418e 100644
--- a/docs/docs/content/bounces.md
+++ b/docs/docs/content/bounces.md
@@ -33,7 +33,7 @@ The bounce webhook API can be used to record bounce events with custom scripting
```shell
-curl -u curl -u 'api_username:access_token' -X POST 'http://localhost:9000/webhooks/bounce' \
+curl -u 'api_username:access_token' -X POST 'http://localhost:9000/webhooks/bounce' \
-H "Content-Type: application/json" \
--data '{"email": "user1@mail.com", "campaign_uuid": "9f86b50d-5711-41c8-ab03-bc91c43d711b", "source": "api", "type": "hard", "meta": "{\"additional\": \"info\"}}'
From cb99d600aa3af21425a79862eb404ba20b625249 Mon Sep 17 00:00:00 2001
From: centja1
Date: Tue, 3 Dec 2024 02:10:31 -0600
Subject: [PATCH 008/145] Fix loading of messengers from serverConfig on line
629 (#2187)
---
frontend/src/views/Campaign.vue | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/views/Campaign.vue b/frontend/src/views/Campaign.vue
index 1898f6ceb..d8f5f91ae 100644
--- a/frontend/src/views/Campaign.vue
+++ b/frontend/src/views/Campaign.vue
@@ -626,7 +626,7 @@ export default Vue.extend({
},
messengers() {
- return ['email', ...this.serverConfig.messengers.map((m) => m.name)];
+ return [...this.serverConfig.messengers];
},
},
From 1c33d32b9e66063c14b289241ff3ddf81822fbdd Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Wed, 4 Dec 2024 21:16:48 +0530
Subject: [PATCH 009/145] Remove redundant event from bounces UI. Closes #1850.
---
frontend/src/views/Bounces.vue | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frontend/src/views/Bounces.vue b/frontend/src/views/Bounces.vue
index 54d16d010..c426e0337 100644
--- a/frontend/src/views/Bounces.vue
+++ b/frontend/src/views/Bounces.vue
@@ -21,8 +21,7 @@
$buefy.toast.open(`Expanded ${row.user.first_name}`)" paginated backend-pagination
- pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="bounces.perPage"
+ paginated backend-pagination pagination-position="both" @page-change="onPageChange" :current-page="queryParams.page" :per-page="bounces.perPage"
:total="bounces.total" backend-sorting @sort="onSort">
From a1291114d9b14d965fab8b914dae956570afc57f Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Wed, 4 Dec 2024 21:52:37 +0530
Subject: [PATCH 010/145] Replace broken `indent` JS lib with `js-beautify`.
Closes #2182.
---
frontend/package.json | 2 +-
frontend/src/components/Editor.vue | 13 +-
frontend/yarn.lock | 2867 ++++++++++++++--------------
3 files changed, 1462 insertions(+), 1420 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index e79b8e78e..0229e0d6b 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,7 +17,7 @@
"chart.js": "^4.4.1",
"codeflask": "^1.4.1",
"dayjs": "^1.11.10",
- "indent.js": "^0.3.5",
+ "js-beautify": "^1.15.1",
"prismjs": "^1.29.0",
"qs": "^6.10.1",
"textversionjs": "^1.1.3",
diff --git a/frontend/src/components/Editor.vue b/frontend/src/components/Editor.vue
index 565066477..da8ce4d5d 100644
--- a/frontend/src/components/Editor.vue
+++ b/frontend/src/components/Editor.vue
@@ -84,8 +84,8 @@
-
+
diff --git a/i18n/bg.json b/i18n/bg.json
index daea40909..ced8a1a59 100644
--- a/i18n/bg.json
+++ b/i18n/bg.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Разрешаване на абонатите да променят предпочитанията си, като например техните имена и множество абонаменти за списъци.",
"settings.privacy.allowWipe": "Разрешаване на изтриване",
"settings.privacy.allowWipeHelp": "Разрешаване на абонатите да изтриват себе си, включително техните абонаменти и всички други данни от базата данни. Прегледите на кампаниите и кликовете върху връзките също се премахват, докато броят на прегледите и кликовете остава (без абонат, свързан с тях), така че статистиката и анализите да не бъдат засегнати.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Черен списък на домейни",
"settings.privacy.domainBlocklistHelp": "Имейл адреси с тези домейни не могат да се абонират. Въведете по един домейн на ред, напр.: somesite.com",
"settings.privacy.individualSubTracking": "Индивидуално проследяване на абонати",
diff --git a/i18n/ca.json b/i18n/ca.json
index 6fc0d2075..74bf6d130 100644
--- a/i18n/ca.json
+++ b/i18n/ca.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permet als subscriptors fer canvis de les preferències tals com els seus noms o la subscripció a múltiples llistes.",
"settings.privacy.allowWipe": "Permet l'esborrat permanent",
"settings.privacy.allowWipeHelp": "Permet als subscriptors esborrar-se, incloses les seves subscripcions i totes les altres dades de la base de dades. Les visualitzacions de campanya i els clics als enllaços també s'eliminen mentre es mantenen les visualitzacions i els recomptes de clics (sense subscriptors associats a ells) de manera que les estadístiques i els indicadors no es veuran afectats.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Llista de dominis bloquejats",
"settings.privacy.domainBlocklistHelp": "No es permet la subscripció a les adreces de correu electrònic amb aquests dominis. Introduïu un domini per línia, per exemple: somesite.com",
"settings.privacy.individualSubTracking": "Seguiment individual de subscriptors",
diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json
index d20ec1e06..23caed5cd 100644
--- a/i18n/cs-cz.json
+++ b/i18n/cs-cz.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Povolit přihlášeným změnu předvoleb jako jsou jména a přihlášení k více seznamům.",
"settings.privacy.allowWipe": "Umožnit vymazání",
"settings.privacy.allowWipeHelp": "Umožnit odběratelům odstranit sebe včetně svých odběrů a všech ostatních dat z databáze. Pohledy na kampaně a klepnutí na odkazy se rovněž odeberou, zatímco pohledy a počty klepnutí se zachovají (aniž by měly přidruženého odběratele), takže statistiky a analýzy nebudou ovlivněny.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Seznam blokovaných domén",
"settings.privacy.domainBlocklistHelp": "E-mailové adresy z těchto domén se nemohou přihlásit k odběru. Uveďte jednu doménu na řádek, eg: somesite.com",
"settings.privacy.individualSubTracking": "Sledování jednotlivých odběratelů",
diff --git a/i18n/cy.json b/i18n/cy.json
index d8a4d0440..51c59de0a 100644
--- a/i18n/cy.json
+++ b/i18n/cy.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Caniatáu i danysgrifwyr newid dewisiadau fel eu henw a pha restrau maent wedi tanysgrifio iddynt.",
"settings.privacy.allowWipe": "Caniatáu sgubo",
"settings.privacy.allowWipeHelp": "Caniatáu i danysgrifwyr ddileu eu hunain",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Rhestr rhwystro parthau",
"settings.privacy.domainBlocklistHelp": "Nid oes gan gyfeiriadau e-bost yn y parthau hyn yr hawl i danysgrifio. Rhowch un parth i bob llinell",
"settings.privacy.individualSubTracking": "Olrhain tanysgrifwyr unigol",
diff --git a/i18n/da.json b/i18n/da.json
index 97b623f11..257ac6cfc 100644
--- a/i18n/da.json
+++ b/i18n/da.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Tillad abonnenter at ændre præferencer såsom deres navne og abonnementer på flere lister.",
"settings.privacy.allowWipe": "Tillad aftørring",
"settings.privacy.allowWipeHelp": "Tillad abonnenter at slette sig selv, herunder deres abonnementer og alle andre data fra databasen. Kampagnevisninger og klik på link fjernes også, mens visninger og klikantal forbliver (uden abonnent tilknyttet dem), så statistik og analyser ikke påvirkes.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domæne blokeringsliste",
"settings.privacy.domainBlocklistHelp": "E-mail-adresser med disse domæner må ikke abonnere. Indtast et domæne pr. linje, f.eks.: somesite.com",
"settings.privacy.individualSubTracking": "Sporing af individuelle abonnenter",
diff --git a/i18n/de.json b/i18n/de.json
index 049a969e6..de52ddf26 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Erlaube den Abonnenten, ihre Einstellungen zu ändern, wie z. B. ihren Namen und mehrere Listenabonnements.",
"settings.privacy.allowWipe": "Löschen aktivieren",
"settings.privacy.allowWipeHelp": "Erlaube Abonnenten alle Daten, welche über sie gespeichert sind zu löschen. Dies beinhaltet auch Klicks und Anzeigen, verändert allerdings nicht die Gesamtzahl. Statistiken bleiben auch unverändert.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domain-Sperrliste",
"settings.privacy.domainBlocklistHelp": "E-Mail Adressen dieser Domains sind vom Abonnieren ausgeschlossen. Eine Domain pro Zeile, z.B. somesite.com",
"settings.privacy.individualSubTracking": "Einzelabonnenten Tracking",
diff --git a/i18n/el.json b/i18n/el.json
index 05ea481d0..80c940a76 100644
--- a/i18n/el.json
+++ b/i18n/el.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Να επιτρέπεται στους συνδρομητές να αλλάξουν τις προτιμήσεις τους, όπως τα ονόματά τους και τις συνδρομές σε πολλαπλές λίστες.",
"settings.privacy.allowWipe": "Να επιτρέπεται η ολική εκκαθάριση",
"settings.privacy.allowWipeHelp": "Να επιτρέπεται στους συνδρομητές να διαγράφουν τους εαυτούς τους, συμπεριλαμβανομένων των εγγραφών τους και όλων των άλλων δεδομένων από τη βάση δεδομένων. Οι προβολές εκστρατειών και τα κλικ σε συνδέσμους διαγράφονται επίσης, ενώ οι καταγραφές του πλήθους των προβολές και των κλικ παραμένουν (χωρίς να συνδέεται με αυτά κανένας συνδρομητής), ώστε να μην επηρεάζονται τα στατιστικά και τα αναλυτικά στοιχεία.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Λίστα αποκλεισμένων domain",
"settings.privacy.domainBlocklistHelp": "Οι διευθύνσεις ηλεκτρονικού ταχυδρομείου σε αυτά τα domain δεν μπορούν να εγγραφούν. Εισάγετε ένα domain ανά γραμμή, π.χ.: somesite.com",
"settings.privacy.individualSubTracking": "Παρακολούθηση μεμονωμένων συνδρομητών",
diff --git a/i18n/en.json b/i18n/en.json
index dd4511737..426c5ce52 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -506,7 +506,9 @@
"settings.privacy.allowWipe": "Allow wiping",
"settings.privacy.allowWipeHelp": "Allow subscribers to delete themselves including their subscriptions and all other data from the database. Campaign views and link clicks are also removed while views and click counts remain (with no subscriber associated to them) so that stats and analytics are not affected.",
"settings.privacy.domainBlocklist": "Domain blocklist",
- "settings.privacy.domainBlocklistHelp": "E-mail addresses with these domains are disallowed from subscribing. Enter one domain per line, eg: somesite.com",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainBlocklistHelp": "E-mail addresses with these domains are disallowed from subscribing. Enter one domain per line, eg: example.com",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.individualSubTracking": "Individual subscriber tracking",
"settings.privacy.individualSubTrackingHelp": "Track subscriber-level campaign views and clicks. When disabled, view and click tracking continue without being linked to individual subscribers.",
"settings.privacy.listUnsubHeader": "Include `List-Unsubscribe` header",
diff --git a/i18n/eo.json b/i18n/eo.json
index a90b6ac5c..60e336bcd 100644
--- a/i18n/eo.json
+++ b/i18n/eo.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permet als subscriptors fer canvis de les preferències tals com els seus noms o la subscripció a múltiples llistes.",
"settings.privacy.allowWipe": "Permet l'esborrat permanent",
"settings.privacy.allowWipeHelp": "Permet als subscriptors esborrar-se, incloses les seves subscripcions i totes les altres dades de la base de dades. Les visualitzacions de campanya i els clics als enllaços també s'eliminen mentre es mantenen les visualitzacions i els recomptes de clics (sense subscriptors associats a ells) de manera que les estadístiques i els indicadors no es veuran afectats.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Llista de dominis bloquejats",
"settings.privacy.domainBlocklistHelp": "No es permet la subscripció a les adreces de correu electrònic amb aquests dominis. Introduïu un domini per línia, per exemple: somesite.com",
"settings.privacy.individualSubTracking": "Seguiment individual de subscriptors",
diff --git a/i18n/es.json b/i18n/es.json
index ee68f5e70..0ca6acb3d 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permitir a las cuentas suscritas realizar cambios como nombre o pertenencia a diferentes listas.",
"settings.privacy.allowWipe": "Permitir limpieza de datos",
"settings.privacy.allowWipeHelp": "Permitir a los suscriptores eliminarse incluyendo sus suscripciones y todos sus datos de la base de datos. Las vistas de las campañas y los vínculos cliqueados también son eliminados mientras que las vistas y el conteo de clics se mantienen. (sin suscriptores asociados a ellos) de manera que las estadísticas y el análisis no se vea afectado.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Listado de dominios bloqueados",
"settings.privacy.domainBlocklistHelp": "Los correos electrónicos de estos dominios estan desabilitados para suscribirse. Introduzca un dominio por línea, por ejemplo: unsitio.com",
"settings.privacy.individualSubTracking": "Seguimiento de suscriptor inválido.",
diff --git a/i18n/fi.json b/i18n/fi.json
index c8070fe0c..b888a4f82 100644
--- a/i18n/fi.json
+++ b/i18n/fi.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Salli tilaajien muuttaa asetuksia, kuten nimiä ja tilauslistoja.",
"settings.privacy.allowWipe": "Salli poistaminen",
"settings.privacy.allowWipeHelp": "Salli tilaajien poistaa itsensä sisältäen tilaukset ja kaikki muut tiedot tietokannasta. Kampanjan katselut ja linkkiklikkaukset poistuvat myös, kun näkymät ja klikki- tai näyttömäärät säilyvät (ilman tilaajaa niihin nimettynä), jotta tilastotiedot ja analytiikka eivät häiriinny.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Verkkotunnus-estolista",
"settings.privacy.domainBlocklistHelp": "Tilaajien sähköpostiosoitteet näistä verkkotunnuksista estetään liittymästä postituslistoille. Lisää yksi verkkotunnus per rivi, esim: esimerkki.fi",
"settings.privacy.individualSubTracking": "Yksittäinen tilaajatason seuranta",
diff --git a/i18n/fr-CA.json b/i18n/fr-CA.json
index 78e42b37a..41497e1f4 100644
--- a/i18n/fr-CA.json
+++ b/i18n/fr-CA.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permettre aux abonnés de modifier leurs préférences, comme leur nom et l'abonnement à plusieurs listes.",
"settings.privacy.allowWipe": "Autoriser la suppression des données par les abonné·es",
"settings.privacy.allowWipeHelp": "Autoriser les abonné·es à supprimer leurs abonnements et toutes les autres données de la base de données. Les vues de campagne et les clics sur les liens sont également supprimés, tandis que le compteur de vues et de nombre de clics globaux restent inchangés (aucun·e abonné·e ne leur est associé) afin que les statistiques et les analyses ne soient pas affectées.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domaine bloqué",
"settings.privacy.domainBlocklistHelp": "Les adresses courriels avec ces domaines ne sont pas autorisées à s'abonner. Entrer un domaine par ligne, exple : somesite.com",
"settings.privacy.individualSubTracking": "Suivi individuel des abonné·es (vérifiez si la légalislation l'autorise)",
diff --git a/i18n/fr.json b/i18n/fr.json
index 9e0c3a517..8c7b95199 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permettre aux abonnés de modifier leurs préférences, comme leur nom et l'abonnement à plusieurs listes.",
"settings.privacy.allowWipe": "Autoriser la suppression des données par les abonné·es",
"settings.privacy.allowWipeHelp": "Autoriser les abonné·es à supprimer leurs abonnements et toutes les autres données de la base de données. Les vues de campagne et les clics sur les liens sont également supprimés, tandis que le compteur de vues et de nombre de clics globaux restent inchangés (aucun·e abonné·e ne leur est associé) afin que les statistiques et les analyses ne soient pas affectées.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domaine bloqué",
"settings.privacy.domainBlocklistHelp": "Les adresses e-mail avec ces domaines ne sont pas autorisées à s'abonner. Entrer un domaine par ligne, exple : somesite.com",
"settings.privacy.individualSubTracking": "Suivi individuel des abonné·es (vérifiez si la légalislation l'autorise)",
diff --git a/i18n/he.json b/i18n/he.json
index 0ba096d46..09c4ebab1 100644
--- a/i18n/he.json
+++ b/i18n/he.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "ניתן למנויים לשתף פעולה בשינוי בחירות כמו שמותיהם ורישומי המנויים הרבים.",
"settings.privacy.allowWipe": "אישור מחיקה",
"settings.privacy.allowWipeHelp": "ניתן למנויים למחוק את עצמם כולל מינויים וכל הנתונים הקשורים להם ממסד הנתונים. תוספות חישוב גם מסירות הודעות וחיצונית בזמו שנשארו (ללא subscriber משוייך אליהם) בזמן מדידת נתונים כדי שלא יתפקעו נתונים וניתוחים.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "רשימת החסימה",
"settings.privacy.domainBlocklistHelp": "כתובות דואר אלקטרוני באמצעות שמן נאסר על הרשות להרשים. שמות התחומים יבשים על כל שורה. לדוגמה: somesite.com",
"settings.privacy.individualSubTracking": "מעקב אישי של המנויים",
diff --git a/i18n/hu.json b/i18n/hu.json
index a58b990e2..1d6e0bc3b 100644
--- a/i18n/hu.json
+++ b/i18n/hu.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "A tagok módosíthatják tagságukat (nevüket, listáikat, stb.).",
"settings.privacy.allowWipe": "Tagság törlése",
"settings.privacy.allowWipeHelp": "A tagok törölhetik midnen adatukat az adatbázisból. A megtekintések és kattintások száma megmarad (nem tagokkal társítva), így ez a kimutatásokat nem érinti.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domain tiltólista",
"settings.privacy.domainBlocklistHelp": "A felsorolt domainekhez tartozó e-mail címekkel nem lehet feliratkozni. Soronként egy domaint adjon meg, pl.: teszt.hu",
"settings.privacy.individualSubTracking": "Megtekintések és kattintások tagokhoz kötése",
diff --git a/i18n/it.json b/i18n/it.json
index d0123fcf8..5265daa2e 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Consenti agli iscritti di modificare le preferenze come il loro nome e le sottoscrizioni a più liste.",
"settings.privacy.allowWipe": "Autorizza la cancellazione",
"settings.privacy.allowWipeHelp": "Autorizza gli iscritti a cancellare le loro iscrizioni e tutti gli altri dati dal database. Le visualizzazioni della campagna e i clic sui link verranno anch'essi cancellati, mentre i contatori globali delle visualizzazioni e del numero di clic restano invariati (nessun iscritto vi è associato) in modo che le statistiche non siano compromesse.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Dominio della lista di blocco",
"settings.privacy.domainBlocklistHelp": "Le caselle di posta di questi domini sono vietate dalla iscrizione. Inserire un dominio per riga, ad esempio: pincopallino.com",
"settings.privacy.individualSubTracking": "Follow-up individuale degli abbonati",
diff --git a/i18n/jp.json b/i18n/jp.json
index 32601b0ec..83fb990b7 100644
--- a/i18n/jp.json
+++ b/i18n/jp.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "加入者に個人設定変更(名前やサブスクリプション状態)を許可する。",
"settings.privacy.allowWipe": "ワイプを許可する",
"settings.privacy.allowWipeHelp": "加入者サブスクリプション含むすべてのデータを含めて、データベースから自身を削除することを許可する。キャンペーンビューとリンククリックも削除されるが、統計と分析に影響が出ないよう、ビューとクリックカウントは残る (加入者を持たない状態)。",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "ドメインブロックリスト",
"settings.privacy.domainBlocklistHelp": "これらのドメインを持つメールアドレスは加入することができません。各行に一つドメインを入れてください。例: somesite.com",
"settings.privacy.individualSubTracking": "加入者個別追跡",
diff --git a/i18n/ml.json b/i18n/ml.json
index 58bffe641..172355e78 100644
--- a/i18n/ml.json
+++ b/i18n/ml.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "വരിക്കാരെ അവരുടെ പേരുകളും ഒന്നിലധികം ലിസ്റ്റ് സബ്സ്ക്രിപ്ഷനുകളും പോലുള്ള മുൻഗണനകൾ മാറ്റാൻ അനുവദിക്കുക.",
"settings.privacy.allowWipe": "വിവരങ്ങൾ എന്നന്നേയ്ക്കുമായി ഇല്ലാതാക്കുന്നത് അനുവദിക്കുക",
"settings.privacy.allowWipeHelp": "ഉപഭോക്താക്കളെ അവരുടെ വരിക്കാരായിട്ടുള്ള ലിസ്റ്റുകളും മറ്റു വിവരങ്ങളും ഡാറ്റാബേസിൽ നിന്നും ഇല്ലാതാക്കാൻ അനുവദിക്കുക.ക്യാമ്പെയ്ൻ കാഴ്ചകളും കണ്ണികളിന്മേലുള്ള ക്ലിക്കുകളുടെ വിവരങ്ങളും ഇല്ലാതാക്കുമെങ്കിലും കാഴ്ചകളുടെയും കണ്ണിയിലുള്ള ക്ലിക്കുകളുടെ (ഉപഭോക്തൃ വിവരങ്ങളില്ലാതെ) എണ്ണവും നിലനിൽക്കും. അതിനാൽ സ്ഥിതിവിവരക്കണക്കുകളെയും വിശകലനങ്ങളെയും ബാധിക്കില്ല.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "ഡൊമെയ്ൻ ബ്ലോക്ക്ലിസ്റ്റ്",
"settings.privacy.domainBlocklistHelp": "ഈ ഡൊമെയ്നുകളുള്ള ഇമെയിൽ വിലാസങ്ങൾ സബ്സ്ക്രൈബുചെയ്യുന്നതിൽ നിന്ന് അനുവദനീയമല്ല. ഓരോ വരിയിലും ഒരു ഡൊമെയ്ൻ നൽകുക. ഉദാ: somesite.com",
"settings.privacy.individualSubTracking": "വ്യക്തിഗത വരിക്കാരെ പിൻതുടരുക",
diff --git a/i18n/nl.json b/i18n/nl.json
index d536acb38..030e06378 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Abonnees toestaan om voorkeuren zoals hun naam en meerdere lijstabonnementen te wijzigen.",
"settings.privacy.allowWipe": "Data wipe toestaan",
"settings.privacy.allowWipeHelp": "Abonnees toelaten zichzelf, al hun inschrijvingen en alle andere data over hun te verwijderen uit de database. Views en klikken op links van campagnes worden verwijderd, maar het aantal views en kliks blijft hetzelfde zodat statistieken niet veranderen.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domein blocklist",
"settings.privacy.domainBlocklistHelp": "E-mail adressen met deze domeinen kunnen zich niet inschrijven. Geef een domein in per lijn, bv.: somesite.com",
"settings.privacy.individualSubTracking": "Individuele abonnees volgen",
diff --git a/i18n/no.json b/i18n/no.json
index efa718869..903f84f58 100644
--- a/i18n/no.json
+++ b/i18n/no.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Tillat abonnenter å endre preferanser, for eksempel navn og hvilke lister de er abonnert på.",
"settings.privacy.allowWipe": "Tillat sletting",
"settings.privacy.allowWipeHelp": "Tillat abonnenter å slette seg selv, inkludert abonnementer og all annen data fra databasen. Kampanjevisninger og lenkeklikk fjernes også, mens statistikk og analyse forblir (uten tilknytning til abonnenter).",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Blokkerte domener",
"settings.privacy.domainBlocklistHelp": "E-postadresser med disse domenene er ikke tillatt å abonnere. Skriv inn ett domene per linje, f.eks. somesite.com",
"settings.privacy.individualSubTracking": "Individuell abonnentsporing",
diff --git a/i18n/pl.json b/i18n/pl.json
index 38692245a..b78712b9e 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Zezwól subskrybentom na zmianę ustawień takich jak imię czy subskrybowane listy",
"settings.privacy.allowWipe": "Zezwól na czyszczenie danych",
"settings.privacy.allowWipeHelp": "Czy zezwolić subskrybentom na usuwanie ich samych razem z wszystkimi ich danymi? Wyświetlenia i liczba kliknięć zostaną zachowane, ale zostaną z nich usunięte informacje kto wykonał tę akcję.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Lista zablokowanych domen",
"settings.privacy.domainBlocklistHelp": "Adresy e-mail z tymi domenami nie mogą subskrybować. Wprowadź jedną domenę w każdym wierszu, np.: domena.com",
"settings.privacy.individualSubTracking": "Śledzenie indywidualnych subskrybentów",
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index d32f8ba5b..eb06df4c5 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permita que os assinantes alterem as preferências, como seus nomes e assinaturas de várias listas.",
"settings.privacy.allowWipe": "Permitir limpeza",
"settings.privacy.allowWipeHelp": "Permitir que os assinantes se excluam incluindo suas inscrições e todos os outros dados da base de dados. Visualizações da campanha e cliques de links também são removidos enquanto o total de visualizações e cliques permanecem (com nenhum inscrito associado a eles) para que as estatísticas e análises não sejam afetadas.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Blocklist de domínios",
"settings.privacy.domainBlocklistHelp": "Endereços de e-mail com estes domínios serão proibidos de se cadastrarem. Um domínio por linha, ex: somesite.com",
"settings.privacy.individualSubTracking": "Rastreamento individual de inscrito",
diff --git a/i18n/pt.json b/i18n/pt.json
index 6ac631a11..13ecbe208 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permitir que os subscritores alterem as suas preferências, como o seu nome e a sua subscrição às diversas listas.",
"settings.privacy.allowWipe": "Permitir eliminação de dados",
"settings.privacy.allowWipeHelp": "Permitir aos subscritores eliminar todos os seus dados, incluindo as suas subscrições, da base de dados. Visualizações de campanhas e cliques em links também são removidos enquanto visualizações e contagem de clicks permanecem (sem nenhum subscritor associado) para que as estatísticas não sejam afetadas.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Lista de domínios bloqueados",
"settings.privacy.domainBlocklistHelp": "Endereços de email com estes domínios não podem efetuar subscrições. Insira um domínio por linha, e.g. somesite.com",
"settings.privacy.individualSubTracking": "Tracking individual de subscritores",
diff --git a/i18n/ro.json b/i18n/ro.json
index f3ae12ee1..88ea7dd57 100644
--- a/i18n/ro.json
+++ b/i18n/ro.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Permiteți abonaților să-și schimbe preferințele, cum ar fi numele lor și abonările la mai multe liste.",
"settings.privacy.allowWipe": "Permiteți accesul la audio",
"settings.privacy.allowWipeHelp": "Permite abonaților să se șteargă, inclusiv abonamentele lor și toate celelalte date din baza de date. Vizualizările campaniei și clicurile pe linkuri sunt, de asemenea, eliminate, în timp ce numărul de vizualizări și clicuri rămâne (fără niciun abonat asociat acestora), astfel încât statisticile și analizele să nu fie afectate.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Nu am găsit date despre domeniul {domain}.",
"settings.privacy.domainBlocklistHelp": "Adresele de poștă electronică cu aceste domenii nu sunt permise de la abonare. Introduceți un domeniu pe linie, de exemplu: somesite.com",
"settings.privacy.individualSubTracking": "În acest hub nu sunt disponibile date despre abonați",
diff --git a/i18n/ru.json b/i18n/ru.json
index 15d3baba1..29ead24c4 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Разрешить подписчикам изменять настройки, такие как их имена и подписки на несколько списков.",
"settings.privacy.allowWipe": "Разрешить удаление",
"settings.privacy.allowWipeHelp": "Разрешить подписчикам удалять себя, включая их подписки и все другие данные из базы данных. Просмотры кампаний и клики по ссылкам также удаляются, в то время как количество просмотров и кликов остаётся (без связи с подписчиком), чтобы не повлиять на статистику и аналитику.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Чёрный список доменов",
"settings.privacy.domainBlocklistHelp": "Адреса электронной почты с этими доменами не могут подписываться. Введите по одному домену на строку, например: somesite.com",
"settings.privacy.individualSubTracking": "Индивидуальное отслеживание подписчиков",
diff --git a/i18n/se.json b/i18n/se.json
index f1d612a6a..6e64c6f3e 100644
--- a/i18n/se.json
+++ b/i18n/se.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Ska prenumeranter kunna ändra preferenser som deras namn och flera lista-prenumerationer.",
"settings.privacy.allowWipe": "Tillåt att radera",
"settings.privacy.allowWipeHelp": "Ska prenumeranter kunna radera sig själva, inklusive deras prenumerationer och all annan data från databasen. Kampanjvisningar och länkklickar tas också bort, medan visnings- och klickräkningar förblir (utan någon prenumerant kopplad till dem) för att statistik och analys inte påverkas.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Domänblocklista",
"settings.privacy.domainBlocklistHelp": "E-postadresser med dessa domäner är inte tillåtna att prenumerera. Ange en domän per rad, t.ex: exempsite.com",
"settings.privacy.individualSubTracking": "Individuell prenumerationsövervakning",
diff --git a/i18n/sk.json b/i18n/sk.json
index 1297820b3..bc59749f7 100644
--- a/i18n/sk.json
+++ b/i18n/sk.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Povoliť prihláseným zmenu predvolieb ako sú meno a prihlásenie k viacerým zoznamom.",
"settings.privacy.allowWipe": "Povoliť vymazanie",
"settings.privacy.allowWipeHelp": "Dovolí odberateľom odstrániť svoje odbery a všetky súvisiace údaje z databázy. Pozretia kampaní a kliknutia na odkazy se tiež odstránia, pozretia a počty kliknutí sa zachovajú (ale nebudú mať odberateľa), takže štatistiky a analýzy nebudú ovplyvnené.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Zoznam blokovaných domén",
"settings.privacy.domainBlocklistHelp": "E-mailové adresy z týchto domén sa nemôžu prihlásiť na odber. Uveďte jednu doménu na riadok, napr: somesite.com",
"settings.privacy.individualSubTracking": "Sledovanie jednotlivých odberateľov",
diff --git a/i18n/sl.json b/i18n/sl.json
index d68b2290a..d0d38328e 100644
--- a/i18n/sl.json
+++ b/i18n/sl.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Dovoli naročnikom, da spremenijo nastavitve, kot so njihova imena in naročnine na več seznamov.",
"settings.privacy.allowWipe": "Dovoli brisanje",
"settings.privacy.allowWipeHelp": "Dovoli naročnikom, da se izbrišejo, vključno s svojimi naročninami in vsemi drugimi podatki iz zbirke podatkov. Odstranjeni so tudi ogledi oglaševalske akcije in kliki povezav, medtem ko število ogledov in klikov ostane (brez povezanih naročnikov), tako da statistika in analitika ni prizadeta.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Seznam blokiranih domen",
"settings.privacy.domainBlocklistHelp": "Na e-poštne naslove s temi domenami ni dovoljeno naročanje. V vsako vrstico vnesite eno domeno, npr. somesite.com",
"settings.privacy.individualSubTracking": "Sledenje posameznim naročnikom",
diff --git a/i18n/tr.json b/i18n/tr.json
index 44cfc16e5..b2a1313f7 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Abonelerin adları ve çoklu liste abonelikleri gibi tercihlerini değiştirmelerine izin verin.",
"settings.privacy.allowWipe": "Silmek için izin ver",
"settings.privacy.allowWipeHelp": "Abonelerin, abonelikleri ve veritabanındaki diğer tüm veriler dahil olmak üzere kendilerini silmesine izin verin. Kampanya görüntülemeleri ve bağlantı tıklamaları da, görünümler ve tıklama sayıları kalır (bunlarla ilişkilendirilmiş abone olmadan), böylece istatistikler ve analizler etkilenmez.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Alan adı engelleme listesi",
"settings.privacy.domainBlocklistHelp": "Bu alan adlarına sahip e-posta adreslerinin abone olmasına izin verilmez. Her satıra bir alan adı girin, örneğin: somesite.com",
"settings.privacy.individualSubTracking": "Bireysel üye takibi",
diff --git a/i18n/uk.json b/i18n/uk.json
index 37dac31f1..03a8992a1 100644
--- a/i18n/uk.json
+++ b/i18n/uk.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Дозволити підписни_цям налаштовувати свої імена й перемикати стан підписок.",
"settings.privacy.allowWipe": "Дозволити стирання",
"settings.privacy.allowWipeHelp": "Дозволити підписни_цям видаляти себе, свої підписки й пов'язані дані з бази. Перегляди кампаній і переходи за посиланнями відв'язуються від підписни_ці, тобто кількість у статистиці й аналітиці залишається без змін.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Блокування доменів",
"settings.privacy.domainBlocklistHelp": "Адресам е-пошти з цих доменів заборонено підписуватись. Уводьте кожен домен з нового рядка, наприклад: example.org",
"settings.privacy.individualSubTracking": "Відстежувати окремих підписни_ць",
diff --git a/i18n/vi.json b/i18n/vi.json
index 50668dc7c..03d4e31d8 100644
--- a/i18n/vi.json
+++ b/i18n/vi.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "Cho phép người đăng ký thay đổi tùy chọn như tên và đăng ký danh sách đa nguyên.",
"settings.privacy.allowWipe": "Cho phép xóa",
"settings.privacy.allowWipeHelp": "Cho phép người đăng ký tự xóa bao gồm đăng ký của họ và tất cả dữ liệu khác khỏi cơ sở dữ liệu. Lượt xem chiến dịch và lượt nhấp vào liên kết cũng bị xóa trong khi lượt xem và số lượt nhấp vẫn còn (không có người đăng ký nào được liên kết với chúng) để số liệu thống kê và phân tích không bị ảnh hưởng.",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "Danh sách chặn tên miền",
"settings.privacy.domainBlocklistHelp": "Địa chỉ email với các miền này không được phép đăng ký. Nhập một tên miền trên mỗi dòng, ví dụ: somesite.com",
"settings.privacy.individualSubTracking": "Theo dõi người đăng ký cá nhân",
diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json
index 07e56ad49..adf7298d6 100644
--- a/i18n/zh-CN.json
+++ b/i18n/zh-CN.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "允许订阅者更改首选项,例如他们的姓名和多个列表订阅。",
"settings.privacy.allowWipe": "允许擦除",
"settings.privacy.allowWipeHelp": "允许订阅者删除自己,包括他们的订阅和数据库中的所有其他数据。广告系列浏览量和链接点击量也会被删除,而浏览量和点击量仍然存在(没有与之关联的订阅者),因此统计数据和分析不会受到影响。",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "域阻止列表",
"settings.privacy.domainBlocklistHelp": "不允许订阅具有这些域的电子邮件地址。每行输入一个域,例如:somesite.com",
"settings.privacy.individualSubTracking": "个人订户跟踪",
diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json
index 9df580807..1cd1b7551 100644
--- a/i18n/zh-TW.json
+++ b/i18n/zh-TW.json
@@ -505,6 +505,8 @@
"settings.privacy.allowPrefsHelp": "允許訂閱者更改偏好,例如他們的名字和多個訂閱清單。",
"settings.privacy.allowWipe": "允許清除",
"settings.privacy.allowWipeHelp": "允許訂閱者刪除自己,包括他們的訂閱和資料庫中的所有其他數據資料。廣告瀏覽量和連結點擊次數也會被刪除,而瀏覽量和點擊量仍然存在(只是沒有與之關聯的訂閱者),因此統計數據和分析不會受到影響。",
+ "settings.privacy.domainAllowlist": "Domain allowlist",
+ "settings.privacy.domainAllowlistHelp": "Only e-mail addresses with these domains are allowed to subscribe. Enter one domain per line, eg: example.com, *.example.com",
"settings.privacy.domainBlocklist": "網域封鎖清單",
"settings.privacy.domainBlocklistHelp": "不允許使用這些網域的電子郵件進行訂閱。每行輸入一個網域,例如:somesite.com",
"settings.privacy.individualSubTracking": "個人訂閱用戶追蹤",
diff --git a/internal/migrations/v5.0.0.go b/internal/migrations/v5.0.0.go
index a143b3c84..7b3a450b4 100644
--- a/internal/migrations/v5.0.0.go
+++ b/internal/migrations/v5.0.0.go
@@ -37,5 +37,12 @@ func V5_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger
return err
}
+ // Insert new preference settings.
+ if _, err := db.Exec(`
+ INSERT INTO settings (key, value) VALUES('privacy.domain_allowlist', '[]') ON CONFLICT DO NOTHING;
+ `); err != nil {
+ return err
+ }
+
return nil
}
diff --git a/internal/subimporter/importer.go b/internal/subimporter/importer.go
index 973ce678b..41164b970 100644
--- a/internal/subimporter/importer.go
+++ b/internal/subimporter/importer.go
@@ -52,11 +52,17 @@ const (
// Importer represents the bulk CSV subscriber import system.
type Importer struct {
- opt Options
- db *sql.DB
- i18n *i18n.I18n
- domainBlocklist map[string]bool
+ opt Options
+ db *sql.DB
+ i18n *i18n.I18n
+
+ domainBlocklist map[string]struct{}
hasBlocklistWildcards bool
+ hasBlocklist bool
+
+ domainAllowlist map[string]struct{}
+ hasAllowlistWildcards bool
+ hasAllowlist bool
stop chan bool
status Status
@@ -70,8 +76,8 @@ type Options struct {
UpdateListDateStmt *sql.Stmt
NotifCB models.AdminNotifCallback
- // Lookup table for blocklisted domains.
DomainBlocklist []string
+ DomainAllowlist []string
}
// Session represents a single import session.
@@ -136,23 +142,23 @@ func New(opt Options, db *sql.DB, i *i18n.I18n) *Importer {
opt: opt,
db: db,
i18n: i,
- domainBlocklist: make(map[string]bool, len(opt.DomainBlocklist)),
+ domainBlocklist: make(map[string]struct{}, len(opt.DomainBlocklist)),
+ domainAllowlist: make(map[string]struct{}, len(opt.DomainAllowlist)),
status: Status{Status: StatusNone, logBuf: bytes.NewBuffer(nil)},
stop: make(chan bool, 1),
}
// Domain blocklist.
- for _, d := range opt.DomainBlocklist {
- im.domainBlocklist[d] = true
+ mp, hasWildcards := makeDomainMap(opt.DomainBlocklist)
+ im.domainBlocklist = mp
+ im.hasBlocklistWildcards = hasWildcards
+ im.hasBlocklist = len(mp) > 0
- // Domains with *. as the subdomain prefix, strip that
- // and add the full domain to the blocklist as well.
- // eg: *.example.com => example.com
- if strings.Contains(d, "*.") {
- im.hasBlocklistWildcards = true
- im.domainBlocklist[strings.TrimPrefix(d, "*.")] = true
- }
- }
+ // Domain allowlist.
+ mp, hasWildcards = makeDomainMap(opt.DomainAllowlist)
+ im.domainAllowlist = mp
+ im.hasAllowlistWildcards = hasWildcards
+ im.hasAllowlist = len(mp) > 0
return &im
}
@@ -603,29 +609,24 @@ func (im *Importer) SanitizeEmail(email string) (string, error) {
// Check if the e-mail's domain is blocklisted. The e-mail domain and blocklist config
// are always lowercase.
- d := strings.Split(em.Address, "@")
- if len(d) == 2 {
- domain := d[1]
-
- // Check the domain as-is.
- if _, ok := im.domainBlocklist[domain]; ok {
- return "", errors.New(im.i18n.T("subscribers.domainBlocklisted"))
+ if im.hasAllowlist || im.hasBlocklist {
+ d := strings.Split(em.Address, "@")
+ if len(d) != 2 {
+ return em.Address, nil
}
- // If there are wildcards in the blocklist and the email domain has a subdomain, check that.
- if im.hasBlocklistWildcards && strings.Count(domain, ".") > 1 {
- parts := strings.Split(domain, ".")
-
- // Replace the first part of the subdomain with * and check if that exists in the blocklist.
- // Eg: test.mail.example.com => *.mail.example.com
- parts[0] = "*"
- domain = strings.Join(parts, ".")
+ domain := d[1]
- if _, ok := im.domainBlocklist[domain]; ok {
+ // If there's an allowlist, check if the domain is in it. Checking blocklist after that is moot.
+ if im.hasAllowlist {
+ if !im.checkInList(domain, im.hasAllowlistWildcards, im.domainAllowlist) {
+ return "", errors.New(im.i18n.T("subscribers.domainBlocklisted"))
+ }
+ } else if im.hasBlocklist {
+ if im.checkInList(domain, im.hasBlocklistWildcards, im.domainBlocklist) {
return "", errors.New(im.i18n.T("subscribers.domainBlocklisted"))
}
}
-
}
return em.Address, nil
@@ -659,6 +660,30 @@ func (im *Importer) ValidateFields(s SubReq) (SubReq, error) {
return s, nil
}
+// Check the domain against the given map of domains (block/allowlist).
+func (im *Importer) checkInList(domain string, hasWildcards bool, mp map[string]struct{}) bool {
+ // Check the domain as-is.
+ if _, ok := mp[domain]; ok {
+ return true
+ }
+
+ // If there are wildcards in the list and the email domain has a subdomain, check that.
+ if hasWildcards && strings.Count(domain, ".") > 1 {
+ parts := strings.Split(domain, ".")
+
+ // Replace the first part of the subdomain with * and check if that exists in the list.
+ // Eg: test.mail.example.com => *.mail.example.com
+ parts[0] = "*"
+ domain = strings.Join(parts, ".")
+
+ if _, ok := mp[domain]; ok {
+ return true
+ }
+ }
+
+ return false
+}
+
// mapCSVHeaders takes a list of headers obtained from a CSV file, a map of known headers,
// and returns a new map with each of the headers in the known map mapped by the position (0-n)
// in the given CSV list.
@@ -702,3 +727,23 @@ func countLines(r io.Reader) (int, error) {
}
}
}
+
+func makeDomainMap(domains []string) (map[string]struct{}, bool) {
+ var (
+ out = make(map[string]struct{}, len(domains))
+ hasWildCards = false
+ )
+ for _, d := range domains {
+ out[d] = struct{}{}
+
+ // Domains with *. as the subdomain prefix, strip that
+ // and add the full domain to the blocklist as well.
+ // eg: *.example.com => example.com
+ if strings.Contains(d, "*.") {
+ hasWildCards = true
+ out[strings.TrimPrefix(d, "*.")] = struct{}{}
+ }
+ }
+
+ return out, hasWildCards
+}
diff --git a/models/settings.go b/models/settings.go
index fd7ba1bdf..d0cb8a36c 100644
--- a/models/settings.go
+++ b/models/settings.go
@@ -35,6 +35,7 @@ type Settings struct {
PrivacyExportable []string `json:"privacy.exportable"`
PrivacyRecordOptinIP bool `json:"privacy.record_optin_ip"`
DomainBlocklist []string `json:"privacy.domain_blocklist"`
+ DomainAllowlist []string `json:"privacy.domain_allowlist"`
SecurityEnableCaptcha bool `json:"security.enable_captcha"`
SecurityCaptchaKey string `json:"security.captcha_key"`
diff --git a/schema.sql b/schema.sql
index 9f2a81a6e..bf973faf3 100644
--- a/schema.sql
+++ b/schema.sql
@@ -249,6 +249,7 @@ INSERT INTO settings (key, value) VALUES
('privacy.allow_preferences', 'true'),
('privacy.exportable', '["profile", "subscriptions", "campaign_views", "link_clicks"]'),
('privacy.domain_blocklist', '[]'),
+ ('privacy.domain_allowlist', '[]'),
('privacy.record_optin_ip', 'false'),
('security.enable_captcha', 'false'),
('security.captcha_key', '""'),
From d1c964da4f02d4d93eae9ed79811609ed2b86581 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 30 Mar 2025 00:18:23 +0530
Subject: [PATCH 071/145] Bump vite from 5.4.12 to 5.4.15 in /frontend (#2379)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.12 to 5.4.15.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.15/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.15/packages/vite)
---
updated-dependencies:
- dependency-name: vite
dependency-type: direct:development
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
frontend/package.json | 2 +-
frontend/yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index 8392fa738..3ef71aab6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -39,7 +39,7 @@
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-vue": "^9.19.2",
"sass": "^1.34.0",
- "vite": "^5.4.12",
+ "vite": "^5.4.15",
"vue-eslint-parser": "^9.3.2",
"vue-template-compiler": "^2.6.12"
},
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index c8b2d3b38..b0e0b023e 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -3591,10 +3591,10 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
-vite@^5.4.12:
- version "5.4.12"
- resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.12.tgz#627d12ff06de3942557dfe8632fd712a12a072c7"
- integrity sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==
+vite@^5.4.15:
+ version "5.4.15"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.15.tgz#2941547f10ebb4bf9b0fa0da863c06711eb7e5e5"
+ integrity sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==
dependencies:
esbuild "^0.21.3"
postcss "^8.4.43"
From b18c7ad0fbe09c6568e267e7d8e13ba1ced1861f Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Sun, 30 Mar 2025 10:53:41 +0530
Subject: [PATCH 072/145] Fix incorrect loading spinner on the Lists UI. Closes
#1822.
---
frontend/src/api/index.js | 2 +-
frontend/src/constants.js | 5 +++++
frontend/src/views/Lists.vue | 4 ++--
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js
index 5f4f11a65..8abb08b91 100644
--- a/frontend/src/api/index.js
+++ b/frontend/src/api/index.js
@@ -127,7 +127,7 @@ export const queryLists = (params) => http.get(
'/api/lists',
{
params: (!params ? { per_page: 'all' } : params),
- loading: models.lists,
+ loading: models.listsFull,
},
);
diff --git a/frontend/src/constants.js b/frontend/src/constants.js
index 712afbb9b..7aff23d32 100644
--- a/frontend/src/constants.js
+++ b/frontend/src/constants.js
@@ -2,7 +2,12 @@ export const models = Object.freeze({
serverConfig: 'serverConfig',
lang: 'lang',
dashboard: 'dashboard',
+ // This loading state is used across all contexts where lists are loaded
+ // via the instant "minimal" API.
lists: 'lists',
+ // This is used only on the lists page where lists are loaded with full
+ // context (subscriber counts), which can be slow and expensive.
+ listsFull: 'listsFull',
subscribers: 'subscribers',
campaigns: 'campaigns',
templates: 'templates',
diff --git a/frontend/src/views/Lists.vue b/frontend/src/views/Lists.vue
index 2feff9e64..d586a16fe 100644
--- a/frontend/src/views/Lists.vue
+++ b/frontend/src/views/Lists.vue
@@ -16,7 +16,7 @@
-
@@ -142,7 +142,7 @@
-
+
From 92e5d630cac08bad48540f775ce38988e344e4f6 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Sun, 30 Mar 2025 11:21:50 +0530
Subject: [PATCH 073/145] Fix various static-check/idiom warnings.
---
cmd/admin.go | 2 +-
cmd/auth.go | 2 +-
cmd/bounce.go | 1 -
cmd/handlers.go | 34 +++++-----------------------------
cmd/i18n.go | 14 +++++++-------
cmd/init.go | 4 ++--
cmd/install.go | 4 ++--
cmd/main.go | 4 ++--
cmd/manager_store.go | 3 ---
cmd/notifications.go | 7 -------
cmd/subscribers.go | 20 ++------------------
cmd/upgrade.go | 4 ++--
cmd/users.go | 2 +-
cmd/utils.go | 28 ----------------------------
14 files changed, 25 insertions(+), 104 deletions(-)
diff --git a/cmd/admin.go b/cmd/admin.go
index 2541dbfb5..e42074874 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -37,7 +37,7 @@ func handleGetServerConfig(c echo.Context) error {
}
// Language list.
- langList, err := getI18nLangList(app.constants.Lang, app)
+ langList, err := getI18nLangList(app)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
fmt.Sprintf("Error loading language list: %v", err))
diff --git a/cmd/auth.go b/cmd/auth.go
index 2d368ba7d..de7cf1060 100644
--- a/cmd/auth.go
+++ b/cmd/auth.go
@@ -316,7 +316,7 @@ func doLogin(c echo.Context) error {
}
// Resist potential constant-time-comparison attacks with a min response time.
- if ms := time.Now().Sub(start).Milliseconds(); ms < 100 {
+ if ms := time.Since(start).Milliseconds(); ms < 100 {
time.Sleep(time.Duration(ms))
}
diff --git a/cmd/bounce.go b/cmd/bounce.go
index 33b7b5491..6afcc1323 100644
--- a/cmd/bounce.go
+++ b/cmd/bounce.go
@@ -161,7 +161,6 @@ func handleBounceWebhook(c echo.Context) error {
app.log.Printf("error processing SNS (SES) subscription: %v", err)
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
}
- break
// Bounce notification.
case "Notification":
diff --git a/cmd/handlers.go b/cmd/handlers.go
index 0c38adb2a..c123325a8 100644
--- a/cmd/handlers.go
+++ b/cmd/handlers.go
@@ -8,7 +8,6 @@ import (
"regexp"
"github.com/knadh/listmonk/internal/auth"
- "github.com/knadh/paginator"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
@@ -17,11 +16,6 @@ const (
// stdInputMaxLen is the maximum allowed length for a standard input field.
stdInputMaxLen = 2000
- sortAsc = "asc"
- sortDesc = "desc"
-
- basicAuthd = "basicauthd"
-
// URIs.
uriAdmin = "/admin"
)
@@ -30,25 +24,8 @@ type okResp struct {
Data interface{} `json:"data"`
}
-// pagination represents a query's pagination (limit, offset) related values.
-type pagination struct {
- PerPage int `json:"per_page"`
- Page int `json:"page"`
- Offset int `json:"offset"`
- Limit int `json:"limit"`
-}
-
var (
- reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
- reLangCode = regexp.MustCompile("[^a-zA-Z_0-9\\-]")
-
- paginate = paginator.New(paginator.Opt{
- DefaultPerPage: 20,
- MaxPerPage: 50,
- NumPageNums: 10,
- PageParam: "page",
- PerPageParam: "per_page",
- })
+ reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
)
// registerHandlers registers HTTP handlers.
@@ -352,9 +329,8 @@ func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
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(app.i18n.T("public.errorTitle"), "",
+ app.i18n.T("globals.messages.invalidUUID")))
}
}
return next(c)
@@ -363,7 +339,7 @@ func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
// subscriberExists middleware checks if a subscriber exists given the UUID
// param in a request.
-func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
+func subscriberExists(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
var (
app = c.Get("app").(*App)
@@ -386,7 +362,7 @@ func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc
}
// noIndex adds the HTTP header requesting robots to not crawl the page.
-func noIndex(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
+func noIndex(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set("X-Robots-Tag", "noindex")
return next(c)
diff --git a/cmd/i18n.go b/cmd/i18n.go
index 8b9dc74e9..261c79ea1 100644
--- a/cmd/i18n.go
+++ b/cmd/i18n.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
+ "regexp"
"sort"
"github.com/knadh/listmonk/internal/i18n"
@@ -21,6 +22,8 @@ type i18nLangRaw struct {
Name string `json:"_.name"`
}
+var reLangCode = regexp.MustCompile(`[^a-zA-Z_0-9\\-]`)
+
// handleGetI18nLang returns the JSON language pack given the language code.
func handleGetI18nLang(c echo.Context) error {
app := c.Get("app").(*App)
@@ -39,7 +42,7 @@ func handleGetI18nLang(c echo.Context) error {
}
// getI18nLangList returns the list of available i18n languages.
-func getI18nLangList(lang string, app *App) ([]i18nLang, error) {
+func getI18nLangList(app *App) ([]i18nLang, error) {
list, err := app.fs.Glob("/i18n/*.json")
if err != nil {
return nil, err
@@ -52,15 +55,12 @@ func getI18nLangList(lang string, app *App) ([]i18nLang, error) {
return out, fmt.Errorf("error reading lang file: %s: %v", l, err)
}
- var lang i18nLangRaw
- if err := json.Unmarshal(b.ReadBytes(), &lang); err != nil {
+ var r i18nLangRaw
+ if err := json.Unmarshal(b.ReadBytes(), &r); err != nil {
return out, fmt.Errorf("error parsing lang file: %s: %v", l, err)
}
- out = append(out, i18nLang{
- Code: lang.Code,
- Name: lang.Name,
- })
+ out = append(out, i18nLang(r))
}
sort.SliceStable(out, func(i, j int) bool {
diff --git a/cmd/init.go b/cmd/init.go
index c9067528e..50932504c 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -317,7 +317,7 @@ func initDB() *sqlx.DB {
}
// readQueries reads named SQL queries from the SQL queries file into a query map.
-func readQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem) goyesql.Queries {
+func readQueries(sqlFile string, fs stuffbin.FileSystem) goyesql.Queries {
// Load SQL queries.
qB, err := fs.Read(sqlFile)
if err != nil {
@@ -673,7 +673,7 @@ func initMediaStore() media.Store {
// initNotifTemplates compiles and returns e-mail notification templates that are
// used for sending ad-hoc notifications to admins and subscribers.
-func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18n, cs *constants) *notifTpls {
+func initNotifTemplates(fs stuffbin.FileSystem, i *i18n.I18n, cs *constants) *notifTpls {
tpls, err := stuffbin.ParseTemplatesGlob(initTplFuncs(i, cs), fs, "/static/email-templates/*.html")
if err != nil {
lo.Fatalf("error parsing e-mail notif templates: %v", err)
diff --git a/cmd/install.go b/cmd/install.go
index 3d61ca2a6..f313e43e0 100644
--- a/cmd/install.go
+++ b/cmd/install.go
@@ -15,7 +15,7 @@ import (
// install runs the first time setup of setting up the database.
func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempotent bool) {
- qMap := readQueries(queryFilePath, db, fs)
+ qMap := readQueries(queryFilePath, fs)
fmt.Println("")
if !idempotent {
@@ -244,7 +244,7 @@ func recordMigrationVersion(ver string, db *sqlx.DB) error {
func newConfigFile(path string) error {
if _, err := os.Stat(path); !os.IsNotExist(err) {
- return fmt.Errorf("%s exists. Remove it to generate a new one.", path)
+ return fmt.Errorf("%s exists. Remove it to generate a new one", path)
}
// Initialize the static file system into which all
diff --git a/cmd/main.go b/cmd/main.go
index e7de40926..6e538bbb9 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -157,7 +157,7 @@ func init() {
checkUpgrade(db)
// Read the SQL queries from the queries file.
- qMap := readQueries(queryFilePath, db, fs)
+ qMap := readQueries(queryFilePath, fs)
// Load settings from DB.
if q, ok := qMap["get-settings"]; ok {
@@ -223,7 +223,7 @@ func main() {
// for new user setup.
app.needsUserSetup = !hasUsers
- app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.i18n, app.constants)
+ app.notifTpls = initNotifTemplates(fs, app.i18n, app.constants)
initTxTemplates(app.manager, app)
if ko.Bool("bounce.enabled") {
diff --git a/cmd/manager_store.go b/cmd/manager_store.go
index e0d82fc23..72c3c5dac 100644
--- a/cmd/manager_store.go
+++ b/cmd/manager_store.go
@@ -1,8 +1,6 @@
package main
import (
- "net/http"
-
"github.com/gofrs/uuid/v5"
"github.com/knadh/listmonk/internal/core"
"github.com/knadh/listmonk/internal/manager"
@@ -17,7 +15,6 @@ type store struct {
queries *models.Queries
core *core.Core
media media.Store
- h *http.Client
}
type runningCamp struct {
diff --git a/cmd/notifications.go b/cmd/notifications.go
index eece80ed2..5ae296e3f 100644
--- a/cmd/notifications.go
+++ b/cmd/notifications.go
@@ -20,13 +20,6 @@ var (
reTitle = regexp.MustCompile(`(?s)(.+?)`)
)
-// notifData represents params commonly used across different notification
-// templates.
-type notifData struct {
- RootURL string
- LogoURL string
-}
-
// sendNotification sends out an e-mail notification to admins.
func (app *App) sendNotification(toEmails []string, subject, tplName string, data interface{}, headers textproto.MIMEHeader) error {
if len(toEmails) == 0 {
diff --git a/cmd/subscribers.go b/cmd/subscribers.go
index 4d775a0f6..ef9983ab8 100644
--- a/cmd/subscribers.go
+++ b/cmd/subscribers.go
@@ -3,7 +3,6 @@ package main
import (
"encoding/csv"
"encoding/json"
- "errors"
"fmt"
"net/http"
"net/textproto"
@@ -34,16 +33,6 @@ type subQueryReq struct {
All bool `json:"all"`
}
-// subProfileData represents a subscriber's collated data in JSON
-// for export.
-type subProfileData struct {
- Email string `db:"email" json:"-"`
- Profile json.RawMessage `db:"profile" json:"profile,omitempty"`
- Subscriptions json.RawMessage `db:"subscriptions" json:"subscriptions,omitempty"`
- CampaignViews json.RawMessage `db:"campaign_views" json:"campaign_views,omitempty"`
- LinkClicks json.RawMessage `db:"link_clicks" json:"link_clicks,omitempty"`
-}
-
// subOptin contains the data that's passed to the double opt-in e-mail template.
type subOptin struct {
models.Subscriber
@@ -60,10 +49,6 @@ var (
UUID: dummyUUID,
Attribs: models.JSON{"city": "Bengaluru"},
}
-
- subQuerySortFields = []string{"email", "name", "created_at", "updated_at"}
-
- errSubscriberExists = errors.New("subscriber already exists")
)
// handleGetSubscriber handles the retrieval of a single subscriber by ID.
@@ -175,7 +160,7 @@ loop:
if err != nil {
return err
}
- if out == nil || len(out) == 0 {
+ if len(out) == 0 {
break
}
@@ -416,8 +401,7 @@ func handleDeleteSubscribers(c echo.Context) error {
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
}
if len(i) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
+ return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
}
subIDs = i
}
diff --git a/cmd/upgrade.go b/cmd/upgrade.go
index 414c5178c..c74f2c288 100644
--- a/cmd/upgrade.go
+++ b/cmd/upgrade.go
@@ -115,7 +115,7 @@ func checkUpgrade(db *sqlx.DB) {
// getPendingMigrations gets the pending migrations by comparing the last
// recorded migration in the DB against all migrations listed in `migrations`.
func getPendingMigrations(db *sqlx.DB) (string, []migFunc, error) {
- lastVer, err := getLastMigrationVersion()
+ lastVer, err := getLastMigrationVersion(db)
if err != nil {
return "", nil, err
}
@@ -135,7 +135,7 @@ func getPendingMigrations(db *sqlx.DB) (string, []migFunc, error) {
// getLastMigrationVersion returns the last migration semver recorded in the DB.
// If there isn't any, `v0.0.0` is returned.
-func getLastMigrationVersion() (string, error) {
+func getLastMigrationVersion(db *sqlx.DB) (string, error) {
var v string
if err := db.Get(&v, `
SELECT COALESCE(
diff --git a/cmd/users.go b/cmd/users.go
index 9c259cce8..0ca78c02e 100644
--- a/cmd/users.go
+++ b/cmd/users.go
@@ -15,7 +15,7 @@ import (
)
var (
- reUsername = regexp.MustCompile("^[a-zA-Z0-9_\\-\\.]+$")
+ reUsername = regexp.MustCompile(`^[a-zA-Z0-9_\\-\\.]+$`)
)
// handleGetUsers retrieves users.
diff --git a/cmd/utils.go b/cmd/utils.go
index 7c48ab660..ebbf7e46b 100644
--- a/cmd/utils.go
+++ b/cmd/utils.go
@@ -1,16 +1,12 @@
package main
import (
- "bytes"
"crypto/rand"
"fmt"
"path/filepath"
"regexp"
"strconv"
"strings"
-
- "golang.org/x/text/cases"
- "golang.org/x/text/language"
)
var (
@@ -99,27 +95,3 @@ func generateRandomString(n int) (string, error) {
func strHasLen(str string, min, max int) bool {
return len(str) >= min && len(str) <= max
}
-
-// strSliceContains checks if a string is present in the string slice.
-func strSliceContains(str string, sl []string) bool {
- for _, s := range sl {
- if s == str {
- return true
- }
- }
-
- return false
-}
-
-func trimNullBytes(b []byte) string {
- return string(bytes.Trim(b, "\x00"))
-}
-
-func titleCase(input string) string {
- parts := strings.Fields(input)
- for n, p := range parts {
- parts[n] = cases.Title(language.Und).String(p)
- }
-
- return strings.Join(parts, " ")
-}
From fbc27ae4b256423d23b4a571b5be45da26c339b5 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Mon, 31 Mar 2025 12:56:43 +0530
Subject: [PATCH 074/145] Refactor UI time diff display function to prefix '-'
on past dates.
---
frontend/src/utils.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/frontend/src/utils.js b/frontend/src/utils.js
index 68c05a12a..7d7a9dd03 100644
--- a/frontend/src/utils.js
+++ b/frontend/src/utils.js
@@ -68,7 +68,15 @@ export default class Utils {
return out;
};
- duration = (start, end) => dayjs(end).from(dayjs(start), true);
+ duration = (start, end) => {
+ const a = dayjs(start);
+ const b = dayjs(end);
+
+ // Get the duration string, eg: "2 days".
+ const diff = b.from(a, true);
+
+ return `${b.isBefore(a) ? '-' : ''}${diff}`;
+ };
// Simple, naive, e-mail address check.
validateEmail = (e) => e.match(reEmail);
From a5f8b28cb157fa73195dba3b99d357b5e4c2deb9 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Mon, 31 Mar 2025 13:00:51 +0530
Subject: [PATCH 075/145] Fix inconsistent behaviour in campaign scheduling on
the UI.
- Fix status/button state management issues when `Send at` was toggled
under various scenarios.
- Allow paused campaigns to be edited and turned into scheduled campaigns.
- Add Cypress UI tests for unscheduling.
---
frontend/cypress/e2e/campaigns.cy.js | 12 ++++++++++
frontend/src/assets/style.scss | 4 ++++
frontend/src/views/Campaign.vue | 35 +++++++++++++++++-----------
frontend/src/views/Campaigns.vue | 16 ++++++-------
i18n/en.json | 2 +-
internal/core/campaigns.go | 2 +-
queries.sql | 10 +++++++-
7 files changed, 56 insertions(+), 25 deletions(-)
diff --git a/frontend/cypress/e2e/campaigns.cy.js b/frontend/cypress/e2e/campaigns.cy.js
index a3f254448..9a531d94b 100644
--- a/frontend/cypress/e2e/campaigns.cy.js
+++ b/frontend/cypress/e2e/campaigns.cy.js
@@ -114,6 +114,18 @@ describe('Campaigns', () => {
cy.get('tbody td[data-label=Status] .tag.scheduled');
});
+ it('Unschedules campaign', () => {
+ cy.get('td[data-label=Status] a').eq(1).click();
+ cy.wait(250);
+ cy.get('button[data-cy=btn-unschedule]').click();
+ cy.get('.modal button.is-primary:eq(0)').click();
+ cy.wait(250);
+ cy.visit('/admin/campaigns');
+
+ // Check if the status label has the inner text `Draft`.
+ cy.get('td[data-label=Status] .tag.draft').should('have.length', 1);
+ });
+
it('Switches formats', () => {
cy.resetDB();
cy.loginAndVisit('/admin/campaigns');
diff --git a/frontend/src/assets/style.scss b/frontend/src/assets/style.scss
index 1856e7aae..0bcd02f92 100644
--- a/frontend/src/assets/style.scss
+++ b/frontend/src/assets/style.scss
@@ -126,6 +126,10 @@ section {
background-color: $primary;
}
+.has-text-primary {
+ color: $primary !important;
+}
+
.box {
background: $white;
box-shadow: 2px 2px 0 #f3f3f3;
diff --git a/frontend/src/views/Campaign.vue b/frontend/src/views/Campaign.vue
index 6ff49a170..b37df27ba 100644
--- a/frontend/src/views/Campaign.vue
+++ b/frontend/src/views/Campaign.vue
@@ -44,8 +44,8 @@
-
+
{{ $t('campaigns.unSchedule') }}
@@ -127,8 +127,8 @@
-
@@ -469,11 +469,6 @@ export default Vue.extend({
}
return f;
});
-
- if (data.sendAt !== null) {
- this.form.sendLater = true;
- this.form.sendAtDate = dayjs(data.sendAt).toDate();
- }
});
},
@@ -553,11 +548,16 @@ export default Vue.extend({
typMsg = 'campaigns.started';
}
+ if (!this.form.sendAtDate) {
+ this.form.sendLater = false;
+ }
+
// This promise is used by startCampaign to first save before starting.
return new Promise((resolve) => {
this.$api.updateCampaign(this.data.id, data).then((d) => {
this.data = d;
this.form.archiveSlug = d.archiveSlug;
+
this.$utils.toast(this.$t(typMsg, { name: d.name }));
resolve();
});
@@ -613,7 +613,6 @@ export default Vue.extend({
unscheduleCampaign() {
this.$api.changeCampaignStatus(this.data.id, 'draft').then((d) => {
this.data = d;
- this.form.archiveSlug = d.archiveSlug;
});
},
},
@@ -627,15 +626,15 @@ export default Vue.extend({
},
canSchedule() {
- return this.data.status === 'draft' && this.data.sendAt;
+ return (this.data.status === 'draft' || this.data.status === 'paused') && (this.form.sendLater && this.form.sendAtDate);
},
canUnSchedule() {
- return this.data.status === 'scheduled' && this.data.sendAt;
+ return this.data.status === 'scheduled';
},
canStart() {
- return this.data.status === 'draft' || this.data.status === 'paused';
+ return (this.data.status === 'draft' || this.data.status === 'paused') && !this.form.sendLater;
},
canArchive() {
@@ -671,6 +670,16 @@ export default Vue.extend({
selectedLists() {
this.form.lists = this.selectedLists;
},
+
+ 'data.sendAt': function () {
+ if (this.data.sendAt !== null) {
+ this.form.sendLater = true;
+ this.form.sendAtDate = dayjs(this.data.sendAt).toDate();
+ } else {
+ this.form.sendLater = false;
+ this.form.sendAtDate = null;
+ }
+ },
},
mounted() {
diff --git a/frontend/src/views/Campaigns.vue b/frontend/src/views/Campaigns.vue
index 02b84965c..efdd4c453 100644
--- a/frontend/src/views/Campaigns.vue
+++ b/frontend/src/views/Campaigns.vue
@@ -52,16 +52,14 @@
-
-
-
-
- {{ $utils.duration(new Date(), props.row.sendAt, true) }}
-
-
- {{ $utils.niceDate(props.row.sendAt, true) }}
+
+
+
+ {{ $utils.duration(new Date(), props.row.sendAt, true) }}
+
-
+ {{ $utils.niceDate(props.row.sendAt, true) }}
+
diff --git a/i18n/en.json b/i18n/en.json
index 426c5ce52..788021811 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -61,7 +61,7 @@
"campaigns.notFound": "Campaign not found.",
"campaigns.onlyActiveCancel": "Only active campaigns can be cancelled.",
"campaigns.onlyActivePause": "Only active campaigns can be paused.",
- "campaigns.onlyDraftAsScheduled": "Only draft campaigns can be scheduled.",
+ "campaigns.onlyDraftAsScheduled": "Only draft or paused campaigns can be scheduled.",
"campaigns.onlyPausedDraft": "Only paused campaigns and drafts can be started.",
"campaigns.onlyScheduledAsDraft": "Only scheduled campaigns can be saved as drafts.",
"campaigns.pause": "Pause",
diff --git a/internal/core/campaigns.go b/internal/core/campaigns.go
index cf3fbbd39..c586ae237 100644
--- a/internal/core/campaigns.go
+++ b/internal/core/campaigns.go
@@ -255,7 +255,7 @@ func (c *Core) UpdateCampaignStatus(id int, status string) (models.Campaign, err
errMsg = c.i18n.T("campaigns.onlyScheduledAsDraft")
}
case models.CampaignStatusScheduled:
- if cm.Status != models.CampaignStatusDraft {
+ if cm.Status != models.CampaignStatusDraft && cm.Status != models.CampaignStatusPaused {
errMsg = c.i18n.T("campaigns.onlyDraftAsScheduled")
}
if !cm.SendAt.Valid {
diff --git a/queries.sql b/queries.sql
index c6ac6cbad..a0ce33413 100644
--- a/queries.sql
+++ b/queries.sql
@@ -880,7 +880,15 @@ UPDATE campaigns SET
WHERE id=$1;
-- name: update-campaign-status
-UPDATE campaigns SET status=$2, updated_at=NOW() WHERE id = $1;
+UPDATE campaigns SET
+ status=(
+ CASE
+ WHEN send_at IS NOT NULL AND $2 = 'running' THEN 'scheduled'
+ ELSE $2::campaign_status
+ END
+ ),
+ updated_at=NOW()
+WHERE id = $1;
-- name: update-campaign-archive
UPDATE campaigns SET
From a271bf54d5055c9b462d5716129aa98614da03a1 Mon Sep 17 00:00:00 2001
From: Kailash Nadh
Date: Sun, 30 Mar 2025 15:25:28 +0530
Subject: [PATCH 076/145] Introduce per-campaign filter permissions. Closes
#2325.
This patch introduces new `campaigns:get_all` and `campaigns:manage_all`
permissions which alter the behaviour of the the old `campaigns:get` and
`campaigns:manage` permissions. This is a subtle breaking behavioural change.
Old:
- `campaigns:get` -> View all campaigns irrespective of a user's list
permissions.
- `campaigns:manage` -> Manage all campaigns irrespective of a user's list
permissions.
New:
- `campaigns:get_all` -> View all campaigns irrespective of a user's list
permissions.
- `campaigns:manage_all` -> Manage all campaigns irrespective of a user's list
permissions.
- `campaigns:get` -> View only the campaigns that have at least one list to
which which a user has get or manage access.
- `campaigns:manage` -> Manage only the campaigns that have at list one list
to which a user has get or manage access.
In addition, this patch refactors and cleans up certain permission related
logic and functions.
---
cmd/campaigns.go | 137 +++++++++++++++++++----
cmd/handlers.go | 26 ++---
cmd/init.go | 2 +-
cmd/lists.go | 23 ++--
cmd/subscribers.go | 2 +-
frontend/src/views/Campaign.vue | 8 +-
internal/auth/auth.go | 7 +-
internal/core/campaigns.go | 16 ++-
internal/core/users.go | 12 +-
internal/migrations/v5.0.0.go | 9 ++
models/models.go | 124 ---------------------
models/permissions.go | 2 +
models/queries.go | 1 +
models/users.go | 190 ++++++++++++++++++++++++++++++++
permissions.json | 4 +-
queries.sql | 18 ++-
16 files changed, 392 insertions(+), 189 deletions(-)
create mode 100644 models/users.go
diff --git a/cmd/campaigns.go b/cmd/campaigns.go
index 0514dd79b..69d378205 100644
--- a/cmd/campaigns.go
+++ b/cmd/campaigns.go
@@ -13,6 +13,7 @@ import (
"strings"
"time"
+ "github.com/knadh/listmonk/internal/auth"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
@@ -52,8 +53,9 @@ var (
// handleGetCampaigns handles retrieval of campaigns.
func handleGetCampaigns(c echo.Context) error {
var (
- app = c.Get("app").(*App)
- pg = app.paginator.NewFromURL(c.Request().URL.Query())
+ app = c.Get("app").(*App)
+ user = c.Get(auth.UserKey).(models.User)
+ pg = app.paginator.NewFromURL(c.Request().URL.Query())
status = c.QueryParams()["status"]
tags = c.QueryParams()["tag"]
@@ -63,17 +65,31 @@ func handleGetCampaigns(c echo.Context) error {
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
)
- res, total, err := app.core.QueryCampaigns(query, status, tags, orderBy, order, pg.Offset, pg.Limit)
+ var (
+ hasAllPerm = user.HasPerm(models.PermCampaignsGetAll)
+ permittedLists []int
+ )
+
+ if !hasAllPerm {
+ // Either the user has campaigns:get_all permissions and can view all campaigns,
+ // or the campaigns are filtered by the lists the user has get|manage access to.
+ hasAllPerm, permittedLists = user.GetPermittedLists(true, true)
+ }
+
+ // Query and retrieve the campaigns.
+ res, total, err := app.core.QueryCampaigns(query, status, tags, orderBy, order, hasAllPerm, permittedLists, pg.Offset, pg.Limit)
if err != nil {
return err
}
+ // Remove the body from the response if requested.
if noBody {
- for i := 0; i < len(res); i++ {
+ for i := range res {
res[i].Body = ""
}
}
+ // Paginate the response.
var out models.PageResults
if len(res) == 0 {
out.Results = []models.Campaign{}
@@ -93,11 +109,22 @@ func handleGetCampaigns(c echo.Context) error {
// handleGetCampaign handles retrieval of campaigns.
func handleGetCampaign(c echo.Context) error {
var (
- app = c.Get("app").(*App)
+ app = c.Get("app").(*App)
+
id, _ = strconv.Atoi(c.Param("id"))
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
)
+ if id < 1 {
+ return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
+ }
+
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, true, c); err != nil {
+ return err
+ }
+
+ // Get the campaign from the DB.
out, err := app.core.GetCampaign(id, "", "")
if err != nil {
return err
@@ -113,7 +140,8 @@ func handleGetCampaign(c echo.Context) error {
// handlePreviewCampaign renders the HTML preview of a campaign body.
func handlePreviewCampaign(c echo.Context) error {
var (
- app = c.Get("app").(*App)
+ app = c.Get("app").(*App)
+
id, _ = strconv.Atoi(c.Param("id"))
tplID, _ = strconv.Atoi(c.FormValue("template_id"))
)
@@ -122,6 +150,12 @@ func handlePreviewCampaign(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, true, c); err != nil {
+ return err
+ }
+
+ // Fetch the campaign body from the DB.
camp, err := app.core.GetCampaignForPreview(id, tplID)
if err != nil {
return err
@@ -243,6 +277,12 @@ func handleUpdateCampaign(c echo.Context) error {
}
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, false, c); err != nil {
+ return err
+ }
+
+ // Retrieve the campaign from the DB.
cm, err := app.core.GetCampaign(id, "", "")
if err != nil {
return err
@@ -285,20 +325,26 @@ func handleUpdateCampaignStatus(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
- var o struct {
- Status string `json:"status"`
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, false, c); err != nil {
+ return err
}
- if err := c.Bind(&o); err != nil {
+ req := struct {
+ Status string `json:"status"`
+ }{}
+ if err := c.Bind(&req); err != nil {
return err
}
- out, err := app.core.UpdateCampaignStatus(id, o.Status)
+ // Update the campaign status in the DB.
+ out, err := app.core.UpdateCampaignStatus(id, req.Status)
if err != nil {
return err
}
- if o.Status == models.CampaignStatusPaused || o.Status == models.CampaignStatusCancelled {
+ // If the campaign is being stopped, send the signal to the manager to stop it in flight.
+ if req.Status == models.CampaignStatusPaused || req.Status == models.CampaignStatusCancelled {
app.manager.StopCampaign(id)
}
@@ -312,14 +358,17 @@ func handleUpdateCampaignArchive(c echo.Context) error {
id, _ = strconv.Atoi(c.Param("id"))
)
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, false, c); err != nil {
+ return err
+ }
+
req := struct {
Archive bool `json:"archive"`
TemplateID int `json:"archive_template_id"`
Meta models.JSON `json:"archive_meta"`
ArchiveSlug string `json:"archive_slug"`
}{}
-
- // Get and validate fields.
if err := c.Bind(&req); err != nil {
return err
}
@@ -351,6 +400,12 @@ func handleDeleteCampaign(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
}
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, false, c); err != nil {
+ return err
+ }
+
+ // Delete the campaign from the DB.
if err := app.core.DeleteCampaign(id); err != nil {
return err
}
@@ -401,17 +456,23 @@ func handleGetRunningCampaignStats(c echo.Context) error {
// arbitrary subscribers for testing.
func handleTestCampaign(c echo.Context) error {
var (
- app = c.Get("app").(*App)
- campID, _ = strconv.Atoi(c.Param("id"))
- tplID, _ = strconv.Atoi(c.FormValue("template_id"))
- req campaignReq
+ app = c.Get("app").(*App)
+
+ id, _ = strconv.Atoi(c.Param("id"))
+ tplID, _ = strconv.Atoi(c.FormValue("template_id"))
)
- if campID < 1 {
+ if id < 1 {
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
}
+ // Check if the user has access to the campaign.
+ if err := checkCampaignPerm(id, false, c); err != nil {
+ return err
+ }
+
// Get and validate fields.
+ var req campaignReq
if err := c.Bind(&req); err != nil {
return err
}
@@ -437,7 +498,7 @@ func handleTestCampaign(c echo.Context) error {
}
// The campaign.
- camp, err := app.core.GetCampaignForPreview(campID, tplID)
+ camp, err := app.core.GetCampaignForPreview(id, tplID)
if err != nil {
return err
}
@@ -644,3 +705,41 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
o.Body = b.String()
return o, nil
}
+
+// checkCampaignPerm checks if the user has get or manage access to the given campaign.
+func checkCampaignPerm(id int, isGet bool, c echo.Context) error {
+ var (
+ app = c.Get("app").(*App)
+ user = c.Get(auth.UserKey).(models.User)
+ )
+
+ perm := models.PermCampaignsGet
+ if isGet {
+ // It's a get request and there's a blanket get all permission.
+ if user.HasPerm(models.PermCampaignsGetAll) {
+ return nil
+ }
+ } else {
+ // It's a manage request and there's a blanket manage_all permission.
+ if user.HasPerm(models.PermCampaignsManageAll) {
+ return nil
+ }
+
+ perm = models.PermCampaignsManage
+ }
+
+ // There are no *_all campaign permissions. Instead, check if the user access
+ // blanket get_all/manage_all list permissions. If yes, then the user can access
+ // all campaigns. If there are no *_all permissions, then ensure that the
+ // campaign belongs to the lists that the user has access to.
+ if hasAllPerm, permittedListIDs := user.GetPermittedLists(true, true); !hasAllPerm {
+ if ok, err := app.core.CampaignHasLists(id, permittedListIDs); err != nil {
+ return err
+ } else if !ok {
+ return echo.NewHTTPError(http.StatusForbidden,
+ app.i18n.Ts("globals.messages.permissionDenied", "name", perm))
+ }
+ }
+
+ return nil
+}
diff --git a/cmd/handlers.go b/cmd/handlers.go
index c123325a8..6ee1944bc 100644
--- a/cmd/handlers.go
+++ b/cmd/handlers.go
@@ -138,20 +138,20 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
api.PUT("/api/lists/:id", listPerm(handleUpdateList))
api.DELETE("/api/lists/:id", listPerm(handleDeleteLists))
- api.GET("/api/campaigns", pm(handleGetCampaigns, "campaigns:get"))
- api.GET("/api/campaigns/running/stats", pm(handleGetRunningCampaignStats, "campaigns:get"))
- api.GET("/api/campaigns/:id", pm(handleGetCampaign, "campaigns:get"))
+ api.GET("/api/campaigns", pm(handleGetCampaigns, "campaigns:get_all", "campaigns:get"))
+ api.GET("/api/campaigns/running/stats", pm(handleGetRunningCampaignStats, "campaigns:get_all", "campaigns:get"))
+ api.GET("/api/campaigns/:id", pm(handleGetCampaign, "campaigns:get_all", "campaigns:get"))
api.GET("/api/campaigns/analytics/:type", pm(handleGetCampaignViewAnalytics, "campaigns:get_analytics"))
- api.GET("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get"))
- api.POST("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get"))
- api.POST("/api/campaigns/:id/content", pm(handleCampaignContent, "campaigns:manage"))
- api.POST("/api/campaigns/:id/text", pm(handlePreviewCampaign, "campaigns:manage"))
- api.POST("/api/campaigns/:id/test", pm(handleTestCampaign, "campaigns:manage"))
- api.POST("/api/campaigns", pm(handleCreateCampaign, "campaigns:manage"))
- api.PUT("/api/campaigns/:id", pm(handleUpdateCampaign, "campaigns:manage"))
- api.PUT("/api/campaigns/:id/status", pm(handleUpdateCampaignStatus, "campaigns:manage"))
- api.PUT("/api/campaigns/:id/archive", pm(handleUpdateCampaignArchive, "campaigns:manage"))
- api.DELETE("/api/campaigns/:id", pm(handleDeleteCampaign, "campaigns:manage"))
+ api.GET("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
+ api.POST("/api/campaigns/:id/preview", pm(handlePreviewCampaign, "campaigns:get_all", "campaigns:get"))
+ api.POST("/api/campaigns/:id/content", pm(handleCampaignContent, "campaigns:manage_all", "campaigns:manage"))
+ api.POST("/api/campaigns/:id/text", pm(handlePreviewCampaign, "campaigns:get"))
+ api.POST("/api/campaigns/:id/test", pm(handleTestCampaign, "campaigns:manage_all", "campaigns:manage"))
+ api.POST("/api/campaigns", pm(handleCreateCampaign, "campaigns:manage_all", "campaigns:manage"))
+ api.PUT("/api/campaigns/:id", pm(handleUpdateCampaign, "campaigns:manage_all", "campaigns:manage"))
+ api.PUT("/api/campaigns/:id/status", pm(handleUpdateCampaignStatus, "campaigns:manage_all", "campaigns:manage"))
+ api.PUT("/api/campaigns/:id/archive", pm(handleUpdateCampaignArchive, "campaigns:manage_all", "campaigns:manage"))
+ api.DELETE("/api/campaigns/:id", pm(handleDeleteCampaign, "campaigns:manage_all", "campaigns:manage"))
api.GET("/api/media", pm(handleGetMedia, "media:get"))
api.GET("/api/media/:id", pm(handleGetMedia, "media:get"))
diff --git a/cmd/init.go b/cmd/init.go
index 50932504c..82caea424 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -1013,7 +1013,7 @@ func initAuth(db *sql.DB, ko *koanf.Koanf, co *core.Core) (bool, *auth.Auth) {
Status: models.UserStatusEnabled,
Type: models.UserTypeAPI,
}
- u.UserRole.ID = auth.SuperAdminRoleID
+ u.UserRole.ID = models.SuperAdminRoleID
a.CacheAPIUser(u)
lo.Println(`WARNING: Remove the admin_username and admin_password fields from the TOML configuration file. If you are using APIs, create and use new credentials. Users are now managed via the Admin -> Settings -> Users dashboard.`)
diff --git a/cmd/lists.go b/cmd/lists.go
index 8b84da002..a3bd7af02 100644
--- a/cmd/lists.go
+++ b/cmd/lists.go
@@ -28,19 +28,12 @@ func handleGetLists(c echo.Context) error {
out models.PageResults
)
- var (
- permittedIDs []int
- getAll = false
- )
- if _, ok := user.PermissionsMap[models.PermListGetAll]; ok {
- getAll = true
- } else {
- permittedIDs = user.GetListIDs
- }
+ // Get the list IDs (or blanket permission) the user has access to.
+ hasAllPerm, permittedIDs := user.GetPermittedLists(true, false)
// Minimal query simply returns the list of all lists without JOIN subscriber counts. This is fast.
if minimal {
- res, err := app.core.GetLists("", getAll, permittedIDs)
+ res, err := app.core.GetLists("", hasAllPerm, permittedIDs)
if err != nil {
return err
}
@@ -58,7 +51,7 @@ func handleGetLists(c echo.Context) error {
}
// Full list query.
- res, total, err := app.core.QueryLists(query, typ, optin, tags, orderBy, order, getAll, permittedIDs, pg.Offset, pg.Limit)
+ res, total, err := app.core.QueryLists(query, typ, optin, tags, orderBy, order, hasAllPerm, permittedIDs, pg.Offset, pg.Limit)
if err != nil {
return err
}
@@ -73,6 +66,7 @@ func handleGetLists(c echo.Context) error {
}
// handleGetList retrieves a single list by id.
+// It's permission checked by the listPerm middleware.
func handleGetList(c echo.Context) error {
var (
app = c.Get("app").(*App)
@@ -112,6 +106,7 @@ func handleCreateList(c echo.Context) error {
}
// handleUpdateList handles list modification.
+// It's permission checked by the listPerm middleware.
func handleUpdateList(c echo.Context) error {
var (
app = c.Get("app").(*App)
@@ -142,6 +137,7 @@ func handleUpdateList(c echo.Context) error {
}
// handleDeleteLists handles list deletion, either a single one (ID in the URI), or a list.
+// It's permission checked by the listPerm middleware.
func handleDeleteLists(c echo.Context) error {
var (
app = c.Get("app").(*App)
@@ -185,11 +181,12 @@ func listPerm(next echo.HandlerFunc) echo.HandlerFunc {
}
// Check if the user has permissions for all lists or the specific list.
- if _, ok := user.PermissionsMap[permAll]; ok {
+ if user.HasPerm(permAll) {
return next(c)
}
+
if id > 0 {
- if _, ok := user.ListPermissionsMap[id][perm]; ok {
+ if user.HasListPerm(id, perm) {
return next(c)
}
}
diff --git a/cmd/subscribers.go b/cmd/subscribers.go
index ef9983ab8..1a25eb59d 100644
--- a/cmd/subscribers.go
+++ b/cmd/subscribers.go
@@ -672,7 +672,7 @@ func sendOptinConfirmationHook(app *App) func(sub models.Subscriber, listIDs []i
// hasSubPerm checks whether the current user has permission to access the given list
// of subscriber IDs.
func hasSubPerm(u models.User, subIDs []int, app *App) error {
- if u.UserRoleID == auth.SuperAdminRoleID {
+ if u.UserRoleID == models.SuperAdminRoleID {
return nil
}
diff --git a/frontend/src/views/Campaign.vue b/frontend/src/views/Campaign.vue
index b37df27ba..7bcd7cd8b 100644
--- a/frontend/src/views/Campaign.vue
+++ b/frontend/src/views/Campaign.vue
@@ -23,7 +23,7 @@