strapi-bulk-publish 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,9 @@ Strapi 5 plugin to bulk publish content across all locales with a single webhook
11
11
  - Batch selection and one-click publish across all locales
12
12
  - Configurable content type and display field
13
13
  - Auto-detection of default locale from Strapi i18n settings
14
- - Single consolidated webhook call after publishing (no per-locale rebuilds)
14
+ - Flexible webhook with preset formats: **Generic JSON** and **GitLab Pipeline Trigger**
15
+ - Token-based authentication for webhooks
16
+ - Configurable key-value variables sent with webhook requests
15
17
  - SSRF protection for webhook URLs
16
18
  - Confirmation dialog before bulk actions
17
19
  - Custom admin permissions
@@ -40,7 +42,6 @@ export default ({ env }) => ({
40
42
  config: {
41
43
  contentType: 'api::blog-post.blog-post', // required — your content type UID
42
44
  titleField: 'title', // optional, default: 'title'
43
- webhookUrl: '', // optional, can also be set via Settings UI
44
45
  },
45
46
  },
46
47
  });
@@ -52,13 +53,14 @@ export default ({ env }) => ({
52
53
  |--------|------|----------|---------|-------------|
53
54
  | `contentType` | `string` | **yes** | — | Strapi content type UID (e.g. `api::article.article`) |
54
55
  | `titleField` | `string` | no | `'title'` | Field name used as the display title in the admin list |
55
- | `webhookUrl` | `string` | no | `''` | Initial webhook URL; can be changed later in Settings UI |
56
-
57
- The `webhookUrl` set in config serves as the initial seed value. Once changed through the admin Settings page, the UI value takes precedence.
58
56
 
59
57
  ## Webhook
60
58
 
61
- After publishing, a single POST request is sent to the configured webhook URL:
59
+ Configure the webhook in **Settings > Bulk Publish > Webhook**. Two formats are supported:
60
+
61
+ ### Generic JSON
62
+
63
+ Sends a `POST` request with `Content-Type: application/json`:
62
64
 
63
65
  ```json
64
66
  {
@@ -68,7 +70,32 @@ After publishing, a single POST request is sent to the configured webhook URL:
68
70
  }
69
71
  ```
70
72
 
71
- The webhook request has a 10-second timeout. Private/internal URLs (localhost, private IP ranges) are blocked for security.
73
+ If a token is configured, it is sent as an `Authorization: Bearer <token>` header. Any custom variables are merged into the JSON body as top-level keys.
74
+
75
+ ### GitLab Pipeline Trigger
76
+
77
+ Sends a `POST` request with `Content-Type: multipart/form-data`, matching the [GitLab pipeline trigger API](https://docs.gitlab.com/ee/ci/triggers/):
78
+
79
+ ```
80
+ token=<trigger-token>
81
+ ref=<branch>
82
+ variables[BULK_PUBLISH_EVENT]=bulk-publish
83
+ variables[BULK_PUBLISH_POSTS]=docId1,docId2
84
+ variables[BULK_PUBLISH_DATE]=2026-05-08T12:00:00.000Z
85
+ variables[YOUR_CUSTOM_VAR]=value
86
+ ```
87
+
88
+ ### Webhook Settings
89
+
90
+ | Field | Description |
91
+ |-------|-------------|
92
+ | **Preset** | `Generic JSON` or `GitLab Pipeline Trigger` |
93
+ | **URL** | Webhook endpoint URL |
94
+ | **Token** | Auth token (Bearer header for generic, trigger token for GitLab) |
95
+ | **Branch Ref** | Git branch for GitLab pipeline trigger (GitLab only) |
96
+ | **Variables** | Key-value pairs sent as extra fields with every request |
97
+
98
+ The webhook request has a 10-second timeout. Private/internal URLs (localhost, private IP ranges) are blocked for SSRF protection.
72
99
 
73
100
  ## Permissions
74
101
 
@@ -77,7 +104,7 @@ Configure in Settings > Roles:
77
104
  | Action | Purpose |
78
105
  |--------|---------|
79
106
  | `plugin::bulk-publish.publish` | Access bulk publish page and publish documents |
80
- | `plugin::bulk-publish.settings` | View and edit webhook URL |
107
+ | `plugin::bulk-publish.settings` | View and edit webhook settings |
81
108
 
82
109
  ## Prerequisites
83
110
 
@@ -4,7 +4,7 @@ import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
4
  import { Flex, Badge, Main, Box, Typography, Dialog, Button, Loader, Table, Thead, Tr, Th, Checkbox, Tbody, Td } from "@strapi/design-system";
5
5
  import { WarningCircle } from "@strapi/icons";
6
6
  import { useFetchClient, useNotification, Page } from "@strapi/strapi/admin";
7
- import { P as PLUGIN_ID, p as pluginPermissions } from "./index-CBDMXg1c.mjs";
7
+ import { P as PLUGIN_ID, p as pluginPermissions } from "./index-otKZLiQd.mjs";
8
8
  const statusColors = {
9
9
  draft: { textColor: "success700", backgroundColor: "success100" },
10
10
  published: { textColor: "primary700", backgroundColor: "primary100" },
@@ -6,7 +6,7 @@ const index = require("./index-DkTxsEqL.js");
6
6
  const designSystem = require("@strapi/design-system");
7
7
  const icons = require("@strapi/icons");
8
8
  const admin = require("@strapi/strapi/admin");
9
- const index$1 = require("./index-BBZKQRB2.js");
9
+ const index$1 = require("./index-CPTlV111.js");
10
10
  const statusColors = {
11
11
  draft: { textColor: "success700", backgroundColor: "success100" },
12
12
  published: { textColor: "primary700", backgroundColor: "primary100" },
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const jsxRuntime = require("react/jsx-runtime");
4
+ const React = require("react");
5
+ const index = require("./index-DkTxsEqL.js");
6
+ const designSystem = require("@strapi/design-system");
7
+ const icons = require("@strapi/icons");
8
+ const admin = require("@strapi/strapi/admin");
9
+ const index$1 = require("./index-CPTlV111.js");
10
+ const DEFAULT_CONFIG = {
11
+ preset: "generic",
12
+ url: "",
13
+ token: "",
14
+ ref: "main",
15
+ variables: []
16
+ };
17
+ const SettingsPage = () => {
18
+ const { formatMessage } = index.useIntl();
19
+ const { get, put } = admin.useFetchClient();
20
+ const { toggleNotification } = admin.useNotification();
21
+ const [config, setConfig] = React.useState(DEFAULT_CONFIG);
22
+ const [initialConfig, setInitialConfig] = React.useState("");
23
+ const [loading, setLoading] = React.useState(true);
24
+ const [saving, setSaving] = React.useState(false);
25
+ const fetchSettings = React.useCallback(async () => {
26
+ try {
27
+ const { data } = await get(`/${index$1.PLUGIN_ID}/settings`);
28
+ const fetched = {
29
+ preset: data.data?.preset || "generic",
30
+ url: data.data?.url || "",
31
+ token: data.data?.token || "",
32
+ ref: data.data?.ref || "main",
33
+ variables: data.data?.variables || []
34
+ };
35
+ setConfig(fetched);
36
+ setInitialConfig(JSON.stringify(fetched));
37
+ } catch {
38
+ toggleNotification({
39
+ type: "danger",
40
+ message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.error` })
41
+ });
42
+ } finally {
43
+ setLoading(false);
44
+ }
45
+ }, [get, toggleNotification, formatMessage]);
46
+ React.useEffect(() => {
47
+ fetchSettings();
48
+ }, [fetchSettings]);
49
+ const handleSave = async () => {
50
+ try {
51
+ setSaving(true);
52
+ await put(`/${index$1.PLUGIN_ID}/settings`, config);
53
+ setInitialConfig(JSON.stringify(config));
54
+ toggleNotification({
55
+ type: "success",
56
+ message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.success` })
57
+ });
58
+ } catch {
59
+ toggleNotification({
60
+ type: "danger",
61
+ message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.error` })
62
+ });
63
+ } finally {
64
+ setSaving(false);
65
+ }
66
+ };
67
+ const updateField = (key, value) => {
68
+ setConfig((prev) => ({ ...prev, [key]: value }));
69
+ };
70
+ const addVariable = () => {
71
+ setConfig((prev) => ({
72
+ ...prev,
73
+ variables: [...prev.variables, { key: "", value: "" }]
74
+ }));
75
+ };
76
+ const updateVariable = (index2, field, val) => {
77
+ setConfig((prev) => ({
78
+ ...prev,
79
+ variables: prev.variables.map((v, i) => i === index2 ? { ...v, [field]: val } : v)
80
+ }));
81
+ };
82
+ const removeVariable = (index2) => {
83
+ setConfig((prev) => ({
84
+ ...prev,
85
+ variables: prev.variables.filter((_, i) => i !== index2)
86
+ }));
87
+ };
88
+ const hasChanged = JSON.stringify(config) !== initialConfig;
89
+ const isGitlab = config.preset === "gitlab";
90
+ const msg = (id) => formatMessage({ id: `${index$1.PLUGIN_ID}.${id}` });
91
+ return /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Protect, { permissions: index$1.pluginPermissions.settings, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { children: loading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", paddingTop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: msg("loading.settings") }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
92
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 8, paddingBottom: 4, paddingLeft: 10, paddingRight: 10, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
93
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
94
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", children: msg("settings.title") }),
95
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: msg("settings.subtitle") })
96
+ ] }),
97
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleSave, disabled: !hasChanged || saving, loading: saving, children: msg("button.save") })
98
+ ] }) }),
99
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingLeft: 10, paddingRight: 10, paddingBottom: 10, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral0", padding: 6, shadow: "tableShadow", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { direction: "column", gap: 4, children: [
100
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Field.Root, { name: "preset", children: [
101
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Field.Label, { children: msg("webhook.preset.label") }),
102
+ /* @__PURE__ */ jsxRuntime.jsxs(
103
+ designSystem.SingleSelect,
104
+ {
105
+ value: config.preset,
106
+ onChange: (value) => updateField("preset", value),
107
+ children: [
108
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "generic", children: msg("webhook.preset.generic") }),
109
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.SingleSelectOption, { value: "gitlab", children: msg("webhook.preset.gitlab") })
110
+ ]
111
+ }
112
+ )
113
+ ] }),
114
+ /* @__PURE__ */ jsxRuntime.jsx(
115
+ designSystem.TextInput,
116
+ {
117
+ label: msg("webhook.url.label"),
118
+ placeholder: isGitlab ? msg("webhook.url.placeholder.gitlab") : msg("webhook.url.placeholder"),
119
+ hint: msg("webhook.url.hint"),
120
+ name: "url",
121
+ value: config.url,
122
+ onChange: (e) => updateField("url", e.target.value)
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsxRuntime.jsx(
126
+ designSystem.TextInput,
127
+ {
128
+ label: msg("webhook.token.label"),
129
+ placeholder: msg("webhook.token.placeholder"),
130
+ hint: isGitlab ? msg("webhook.token.hint.gitlab") : msg("webhook.token.hint"),
131
+ name: "token",
132
+ type: "password",
133
+ value: config.token,
134
+ onChange: (e) => updateField("token", e.target.value)
135
+ }
136
+ ),
137
+ isGitlab && /* @__PURE__ */ jsxRuntime.jsx(
138
+ designSystem.TextInput,
139
+ {
140
+ label: msg("webhook.ref.label"),
141
+ placeholder: msg("webhook.ref.placeholder"),
142
+ hint: msg("webhook.ref.hint"),
143
+ name: "ref",
144
+ value: config.ref,
145
+ onChange: (e) => updateField("ref", e.target.value)
146
+ }
147
+ ),
148
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
149
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
150
+ /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
151
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: msg("webhook.variables.label") }),
152
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "pi", textColor: "neutral600", display: "block", children: isGitlab ? msg("webhook.variables.hint.gitlab") : msg("webhook.variables.hint") })
153
+ ] }),
154
+ /* @__PURE__ */ jsxRuntime.jsx(
155
+ designSystem.Button,
156
+ {
157
+ variant: "tertiary",
158
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(icons.Plus, {}),
159
+ onClick: addVariable,
160
+ size: "S",
161
+ children: msg("webhook.variables.add")
162
+ }
163
+ )
164
+ ] }),
165
+ config.variables.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { direction: "column", gap: 2, children: config.variables.map((variable, index2) => /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Grid.Root, { gap: 2, children: [
166
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 5, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
167
+ designSystem.TextInput,
168
+ {
169
+ "aria-label": msg("webhook.variables.key"),
170
+ placeholder: msg("webhook.variables.key"),
171
+ name: `var-key-${index2}`,
172
+ value: variable.key,
173
+ onChange: (e) => updateVariable(index2, "key", e.target.value)
174
+ }
175
+ ) }),
176
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
177
+ designSystem.TextInput,
178
+ {
179
+ "aria-label": msg("webhook.variables.value"),
180
+ placeholder: msg("webhook.variables.value"),
181
+ name: `var-value-${index2}`,
182
+ value: variable.value,
183
+ onChange: (e) => updateVariable(index2, "value", e.target.value)
184
+ }
185
+ ) }),
186
+ /* @__PURE__ */ jsxRuntime.jsx(designSystem.Grid.Item, { col: 1, s: 12, children: /* @__PURE__ */ jsxRuntime.jsx(
187
+ designSystem.IconButton,
188
+ {
189
+ onClick: () => removeVariable(index2),
190
+ label: "Delete",
191
+ variant: "ghost",
192
+ children: /* @__PURE__ */ jsxRuntime.jsx(icons.Trash, {})
193
+ }
194
+ ) })
195
+ ] }, index2)) })
196
+ ] })
197
+ ] }) }) })
198
+ ] }) }) });
199
+ };
200
+ exports.SettingsPage = SettingsPage;
@@ -0,0 +1,200 @@
1
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
+ import { useState, useCallback, useEffect } from "react";
3
+ import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
+ import { Main, Flex, Loader, Box, Typography, Button, Field, SingleSelect, SingleSelectOption, TextInput, Grid, IconButton } from "@strapi/design-system";
5
+ import { Plus, Trash } from "@strapi/icons";
6
+ import { useFetchClient, useNotification, Page } from "@strapi/strapi/admin";
7
+ import { P as PLUGIN_ID, p as pluginPermissions } from "./index-otKZLiQd.mjs";
8
+ const DEFAULT_CONFIG = {
9
+ preset: "generic",
10
+ url: "",
11
+ token: "",
12
+ ref: "main",
13
+ variables: []
14
+ };
15
+ const SettingsPage = () => {
16
+ const { formatMessage } = useIntl();
17
+ const { get, put } = useFetchClient();
18
+ const { toggleNotification } = useNotification();
19
+ const [config, setConfig] = useState(DEFAULT_CONFIG);
20
+ const [initialConfig, setInitialConfig] = useState("");
21
+ const [loading, setLoading] = useState(true);
22
+ const [saving, setSaving] = useState(false);
23
+ const fetchSettings = useCallback(async () => {
24
+ try {
25
+ const { data } = await get(`/${PLUGIN_ID}/settings`);
26
+ const fetched = {
27
+ preset: data.data?.preset || "generic",
28
+ url: data.data?.url || "",
29
+ token: data.data?.token || "",
30
+ ref: data.data?.ref || "main",
31
+ variables: data.data?.variables || []
32
+ };
33
+ setConfig(fetched);
34
+ setInitialConfig(JSON.stringify(fetched));
35
+ } catch {
36
+ toggleNotification({
37
+ type: "danger",
38
+ message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.error` })
39
+ });
40
+ } finally {
41
+ setLoading(false);
42
+ }
43
+ }, [get, toggleNotification, formatMessage]);
44
+ useEffect(() => {
45
+ fetchSettings();
46
+ }, [fetchSettings]);
47
+ const handleSave = async () => {
48
+ try {
49
+ setSaving(true);
50
+ await put(`/${PLUGIN_ID}/settings`, config);
51
+ setInitialConfig(JSON.stringify(config));
52
+ toggleNotification({
53
+ type: "success",
54
+ message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.success` })
55
+ });
56
+ } catch {
57
+ toggleNotification({
58
+ type: "danger",
59
+ message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.error` })
60
+ });
61
+ } finally {
62
+ setSaving(false);
63
+ }
64
+ };
65
+ const updateField = (key, value) => {
66
+ setConfig((prev) => ({ ...prev, [key]: value }));
67
+ };
68
+ const addVariable = () => {
69
+ setConfig((prev) => ({
70
+ ...prev,
71
+ variables: [...prev.variables, { key: "", value: "" }]
72
+ }));
73
+ };
74
+ const updateVariable = (index, field, val) => {
75
+ setConfig((prev) => ({
76
+ ...prev,
77
+ variables: prev.variables.map((v, i) => i === index ? { ...v, [field]: val } : v)
78
+ }));
79
+ };
80
+ const removeVariable = (index) => {
81
+ setConfig((prev) => ({
82
+ ...prev,
83
+ variables: prev.variables.filter((_, i) => i !== index)
84
+ }));
85
+ };
86
+ const hasChanged = JSON.stringify(config) !== initialConfig;
87
+ const isGitlab = config.preset === "gitlab";
88
+ const msg = (id) => formatMessage({ id: `${PLUGIN_ID}.${id}` });
89
+ return /* @__PURE__ */ jsx(Page.Protect, { permissions: pluginPermissions.settings, children: /* @__PURE__ */ jsx(Main, { children: loading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", paddingTop: 8, children: /* @__PURE__ */ jsx(Loader, { children: msg("loading.settings") }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ /* @__PURE__ */ jsx(Box, { paddingTop: 8, paddingBottom: 4, paddingLeft: 10, paddingRight: 10, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
91
+ /* @__PURE__ */ jsxs(Box, { children: [
92
+ /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: msg("settings.title") }),
93
+ /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: msg("settings.subtitle") })
94
+ ] }),
95
+ /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: !hasChanged || saving, loading: saving, children: msg("button.save") })
96
+ ] }) }),
97
+ /* @__PURE__ */ jsx(Box, { paddingLeft: 10, paddingRight: 10, paddingBottom: 10, children: /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, shadow: "tableShadow", hasRadius: true, children: /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 4, children: [
98
+ /* @__PURE__ */ jsxs(Field.Root, { name: "preset", children: [
99
+ /* @__PURE__ */ jsx(Field.Label, { children: msg("webhook.preset.label") }),
100
+ /* @__PURE__ */ jsxs(
101
+ SingleSelect,
102
+ {
103
+ value: config.preset,
104
+ onChange: (value) => updateField("preset", value),
105
+ children: [
106
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "generic", children: msg("webhook.preset.generic") }),
107
+ /* @__PURE__ */ jsx(SingleSelectOption, { value: "gitlab", children: msg("webhook.preset.gitlab") })
108
+ ]
109
+ }
110
+ )
111
+ ] }),
112
+ /* @__PURE__ */ jsx(
113
+ TextInput,
114
+ {
115
+ label: msg("webhook.url.label"),
116
+ placeholder: isGitlab ? msg("webhook.url.placeholder.gitlab") : msg("webhook.url.placeholder"),
117
+ hint: msg("webhook.url.hint"),
118
+ name: "url",
119
+ value: config.url,
120
+ onChange: (e) => updateField("url", e.target.value)
121
+ }
122
+ ),
123
+ /* @__PURE__ */ jsx(
124
+ TextInput,
125
+ {
126
+ label: msg("webhook.token.label"),
127
+ placeholder: msg("webhook.token.placeholder"),
128
+ hint: isGitlab ? msg("webhook.token.hint.gitlab") : msg("webhook.token.hint"),
129
+ name: "token",
130
+ type: "password",
131
+ value: config.token,
132
+ onChange: (e) => updateField("token", e.target.value)
133
+ }
134
+ ),
135
+ isGitlab && /* @__PURE__ */ jsx(
136
+ TextInput,
137
+ {
138
+ label: msg("webhook.ref.label"),
139
+ placeholder: msg("webhook.ref.placeholder"),
140
+ hint: msg("webhook.ref.hint"),
141
+ name: "ref",
142
+ value: config.ref,
143
+ onChange: (e) => updateField("ref", e.target.value)
144
+ }
145
+ ),
146
+ /* @__PURE__ */ jsxs(Box, { children: [
147
+ /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", paddingBottom: 2, children: [
148
+ /* @__PURE__ */ jsxs(Box, { children: [
149
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", fontWeight: "bold", textColor: "neutral800", children: msg("webhook.variables.label") }),
150
+ /* @__PURE__ */ jsx(Typography, { variant: "pi", textColor: "neutral600", display: "block", children: isGitlab ? msg("webhook.variables.hint.gitlab") : msg("webhook.variables.hint") })
151
+ ] }),
152
+ /* @__PURE__ */ jsx(
153
+ Button,
154
+ {
155
+ variant: "tertiary",
156
+ startIcon: /* @__PURE__ */ jsx(Plus, {}),
157
+ onClick: addVariable,
158
+ size: "S",
159
+ children: msg("webhook.variables.add")
160
+ }
161
+ )
162
+ ] }),
163
+ config.variables.length > 0 && /* @__PURE__ */ jsx(Flex, { direction: "column", gap: 2, children: config.variables.map((variable, index) => /* @__PURE__ */ jsxs(Grid.Root, { gap: 2, children: [
164
+ /* @__PURE__ */ jsx(Grid.Item, { col: 5, s: 12, children: /* @__PURE__ */ jsx(
165
+ TextInput,
166
+ {
167
+ "aria-label": msg("webhook.variables.key"),
168
+ placeholder: msg("webhook.variables.key"),
169
+ name: `var-key-${index}`,
170
+ value: variable.key,
171
+ onChange: (e) => updateVariable(index, "key", e.target.value)
172
+ }
173
+ ) }),
174
+ /* @__PURE__ */ jsx(Grid.Item, { col: 6, s: 12, children: /* @__PURE__ */ jsx(
175
+ TextInput,
176
+ {
177
+ "aria-label": msg("webhook.variables.value"),
178
+ placeholder: msg("webhook.variables.value"),
179
+ name: `var-value-${index}`,
180
+ value: variable.value,
181
+ onChange: (e) => updateVariable(index, "value", e.target.value)
182
+ }
183
+ ) }),
184
+ /* @__PURE__ */ jsx(Grid.Item, { col: 1, s: 12, children: /* @__PURE__ */ jsx(
185
+ IconButton,
186
+ {
187
+ onClick: () => removeVariable(index),
188
+ label: "Delete",
189
+ variant: "ghost",
190
+ children: /* @__PURE__ */ jsx(Trash, {})
191
+ }
192
+ ) })
193
+ ] }, index)) })
194
+ ] })
195
+ ] }) }) })
196
+ ] }) }) });
197
+ };
198
+ export {
199
+ SettingsPage
200
+ };
@@ -5,7 +5,7 @@ const en = {
5
5
  "page.title": "Bulk Publish",
6
6
  "page.subtitle": "Publish documents across all locales with a single action",
7
7
  "settings.title": "Bulk Publish Settings",
8
- "settings.subtitle": "Configure webhook for frontend rebuild",
8
+ "settings.subtitle": "Configure webhook triggered after publishing",
9
9
  "table.title": "Title",
10
10
  "table.locales": "Locales",
11
11
  "table.status": "Status",
@@ -34,9 +34,26 @@ const en = {
34
34
  "notification.settings.success": "Settings saved successfully",
35
35
  "notification.settings.error": "Failed to save settings",
36
36
  "notification.load.error": "Failed to load posts",
37
- "webhook.label": "Webhook URL",
38
- "webhook.placeholder": "https://example.com/api/rebuild",
39
- "webhook.hint": "POST request will be sent to this URL after publishing",
37
+ "webhook.preset.label": "Webhook Format",
38
+ "webhook.preset.generic": "Generic JSON",
39
+ "webhook.preset.gitlab": "GitLab Pipeline Trigger",
40
+ "webhook.url.label": "Webhook URL",
41
+ "webhook.url.placeholder": "https://example.com/api/rebuild",
42
+ "webhook.url.placeholder.gitlab": "https://gitlab.example.com/api/v4/projects/ID/trigger/pipeline",
43
+ "webhook.url.hint": "POST request will be sent to this URL after publishing",
44
+ "webhook.token.label": "Token",
45
+ "webhook.token.placeholder": "Enter token",
46
+ "webhook.token.hint": "Sent as Authorization Bearer header",
47
+ "webhook.token.hint.gitlab": "GitLab pipeline trigger token",
48
+ "webhook.ref.label": "Branch Ref",
49
+ "webhook.ref.placeholder": "main",
50
+ "webhook.ref.hint": "Git branch to trigger the pipeline on",
51
+ "webhook.variables.label": "Variables",
52
+ "webhook.variables.add": "Add Variable",
53
+ "webhook.variables.key": "Key",
54
+ "webhook.variables.value": "Value",
55
+ "webhook.variables.hint": "Extra fields sent with webhook requests",
56
+ "webhook.variables.hint.gitlab": "Passed as CI/CD variables to the pipeline",
40
57
  "time.just-now": "just now",
41
58
  "time.minutes-ago": "{count}m ago",
42
59
  "time.hours-ago": "{count}h ago",
@@ -7,7 +7,7 @@ const en = {
7
7
  "page.title": "Bulk Publish",
8
8
  "page.subtitle": "Publish documents across all locales with a single action",
9
9
  "settings.title": "Bulk Publish Settings",
10
- "settings.subtitle": "Configure webhook for frontend rebuild",
10
+ "settings.subtitle": "Configure webhook triggered after publishing",
11
11
  "table.title": "Title",
12
12
  "table.locales": "Locales",
13
13
  "table.status": "Status",
@@ -36,9 +36,26 @@ const en = {
36
36
  "notification.settings.success": "Settings saved successfully",
37
37
  "notification.settings.error": "Failed to save settings",
38
38
  "notification.load.error": "Failed to load posts",
39
- "webhook.label": "Webhook URL",
40
- "webhook.placeholder": "https://example.com/api/rebuild",
41
- "webhook.hint": "POST request will be sent to this URL after publishing",
39
+ "webhook.preset.label": "Webhook Format",
40
+ "webhook.preset.generic": "Generic JSON",
41
+ "webhook.preset.gitlab": "GitLab Pipeline Trigger",
42
+ "webhook.url.label": "Webhook URL",
43
+ "webhook.url.placeholder": "https://example.com/api/rebuild",
44
+ "webhook.url.placeholder.gitlab": "https://gitlab.example.com/api/v4/projects/ID/trigger/pipeline",
45
+ "webhook.url.hint": "POST request will be sent to this URL after publishing",
46
+ "webhook.token.label": "Token",
47
+ "webhook.token.placeholder": "Enter token",
48
+ "webhook.token.hint": "Sent as Authorization Bearer header",
49
+ "webhook.token.hint.gitlab": "GitLab pipeline trigger token",
50
+ "webhook.ref.label": "Branch Ref",
51
+ "webhook.ref.placeholder": "main",
52
+ "webhook.ref.hint": "Git branch to trigger the pipeline on",
53
+ "webhook.variables.label": "Variables",
54
+ "webhook.variables.add": "Add Variable",
55
+ "webhook.variables.key": "Key",
56
+ "webhook.variables.value": "Value",
57
+ "webhook.variables.hint": "Extra fields sent with webhook requests",
58
+ "webhook.variables.hint.gitlab": "Passed as CI/CD variables to the pipeline",
42
59
  "time.just-now": "just now",
43
60
  "time.minutes-ago": "{count}m ago",
44
61
  "time.hours-ago": "{count}h ago",
@@ -31,7 +31,7 @@ const index = {
31
31
  defaultMessage: "Bulk Publish"
32
32
  },
33
33
  Component: async () => {
34
- const { HomePage } = await Promise.resolve().then(() => require("./HomePage-DBJToziQ.js"));
34
+ const { HomePage } = await Promise.resolve().then(() => require("./HomePage-Rymin2Ze.js"));
35
35
  return HomePage;
36
36
  },
37
37
  permissions: pluginPermissions.publish,
@@ -54,7 +54,7 @@ const index = {
54
54
  id: "webhook",
55
55
  to: `${PLUGIN_ID}/webhook`,
56
56
  Component: async () => {
57
- const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-D6yyZjsD.js"));
57
+ const { SettingsPage } = await Promise.resolve().then(() => require("./SettingsPage-B99xmYKO.js"));
58
58
  return SettingsPage;
59
59
  },
60
60
  permissions: pluginPermissions.settings
@@ -70,7 +70,7 @@ const index = {
70
70
  return Promise.all(
71
71
  locales.map(async (locale) => {
72
72
  try {
73
- const data = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-BU8SCgAJ.js")) }), `./translations/${locale}.json`, 3);
73
+ const data = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => Promise.resolve().then(() => require("./en-BuqYI-YP.js")) }), `./translations/${locale}.json`, 3);
74
74
  return {
75
75
  data: prefixPluginTranslations(data.default, PLUGIN_ID),
76
76
  locale
@@ -30,7 +30,7 @@ const index = {
30
30
  defaultMessage: "Bulk Publish"
31
31
  },
32
32
  Component: async () => {
33
- const { HomePage } = await import("./HomePage-C-cyV6ij.mjs");
33
+ const { HomePage } = await import("./HomePage-CCD8Pl7z.mjs");
34
34
  return HomePage;
35
35
  },
36
36
  permissions: pluginPermissions.publish,
@@ -53,7 +53,7 @@ const index = {
53
53
  id: "webhook",
54
54
  to: `${PLUGIN_ID}/webhook`,
55
55
  Component: async () => {
56
- const { SettingsPage } = await import("./SettingsPage-BpvQogmY.mjs");
56
+ const { SettingsPage } = await import("./SettingsPage-r2PCnoy5.mjs");
57
57
  return SettingsPage;
58
58
  },
59
59
  permissions: pluginPermissions.settings
@@ -69,7 +69,7 @@ const index = {
69
69
  return Promise.all(
70
70
  locales.map(async (locale) => {
71
71
  try {
72
- const data = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-ua_GGXUg.mjs") }), `./translations/${locale}.json`, 3);
72
+ const data = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-BqCcy0LI.mjs") }), `./translations/${locale}.json`, 3);
73
73
  return {
74
74
  data: prefixPluginTranslations(data.default, PLUGIN_ID),
75
75
  locale
@@ -1,4 +1,4 @@
1
1
  "use strict";
2
- const index = require("../_chunks/index-BBZKQRB2.js");
2
+ const index = require("../_chunks/index-CPTlV111.js");
3
3
  require("@strapi/icons");
4
4
  module.exports = index.index;
@@ -1,4 +1,4 @@
1
- import { i } from "../_chunks/index-CBDMXg1c.mjs";
1
+ import { i } from "../_chunks/index-otKZLiQd.mjs";
2
2
  import "@strapi/icons";
3
3
  export {
4
4
  i as default
@@ -1,6 +1,14 @@
1
1
  "use strict";
2
2
  const register = (_args) => {
3
3
  };
4
+ const WEBHOOK_PRESETS = ["generic", "gitlab"];
5
+ const DEFAULT_WEBHOOK_CONFIG = {
6
+ preset: "generic",
7
+ url: "",
8
+ token: "",
9
+ ref: "main",
10
+ variables: []
11
+ };
4
12
  const bootstrap = async ({ strapi }) => {
5
13
  strapi.log.debug("Bulk Publish plugin bootstrapped");
6
14
  await strapi.service("admin::permission").actionProvider.registerMany([
@@ -18,17 +26,30 @@ const bootstrap = async ({ strapi }) => {
18
26
  }
19
27
  ]);
20
28
  const store = strapi.store({ type: "plugin", name: "bulk-publish" });
21
- const existingUrl = await store.get({ key: "webhookUrl" });
22
- if (existingUrl === null || existingUrl === void 0) {
23
- const configUrl = strapi.plugin("bulk-publish").config("webhookUrl") || "";
24
- await store.set({ key: "webhookUrl", value: configUrl });
29
+ const existingConfig = await store.get({ key: "webhookConfig" });
30
+ if (existingConfig) return;
31
+ const legacyUrl = await store.get({ key: "webhookUrl" });
32
+ if (legacyUrl && typeof legacyUrl === "string") {
33
+ const migrated = {
34
+ ...DEFAULT_WEBHOOK_CONFIG,
35
+ url: legacyUrl
36
+ };
37
+ await store.set({ key: "webhookConfig", value: migrated });
38
+ await store.set({ key: "webhookUrl", value: null });
39
+ strapi.log.info("bulk-publish: migrated webhookUrl to webhookConfig");
40
+ return;
25
41
  }
42
+ const configUrl = strapi.plugin("bulk-publish").config("webhookUrl") || "";
43
+ const seeded = {
44
+ ...DEFAULT_WEBHOOK_CONFIG,
45
+ url: typeof configUrl === "string" ? configUrl : ""
46
+ };
47
+ await store.set({ key: "webhookConfig", value: seeded });
26
48
  };
27
49
  const config = {
28
50
  default: {
29
51
  contentType: "",
30
- titleField: "title",
31
- webhookUrl: ""
52
+ titleField: "title"
32
53
  },
33
54
  validator: (config2) => {
34
55
  if (!config2.contentType || typeof config2.contentType !== "string") {
@@ -39,9 +60,6 @@ const config = {
39
60
  if (config2.titleField && typeof config2.titleField !== "string") {
40
61
  throw new Error("bulk-publish: titleField must be a string");
41
62
  }
42
- if (config2.webhookUrl && typeof config2.webhookUrl !== "string") {
43
- throw new Error("bulk-publish: webhookUrl must be a string");
44
- }
45
63
  }
46
64
  };
47
65
  const admin = {
@@ -144,40 +162,80 @@ function isAllowedWebhookUrl(urlStr) {
144
162
  return false;
145
163
  }
146
164
  }
165
+ function buildGenericRequest(config2, documentIds) {
166
+ const payload = {
167
+ event: "bulk-publish",
168
+ posts: documentIds,
169
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString()
170
+ };
171
+ for (const { key, value } of config2.variables) {
172
+ if (key) payload[key] = value;
173
+ }
174
+ const headers = { "Content-Type": "application/json" };
175
+ if (config2.token) {
176
+ headers["Authorization"] = `Bearer ${config2.token}`;
177
+ }
178
+ return { headers, body: JSON.stringify(payload) };
179
+ }
180
+ function buildGitlabRequest(config2, documentIds) {
181
+ const form = new FormData();
182
+ form.append("token", config2.token);
183
+ form.append("ref", config2.ref || "main");
184
+ form.append("variables[BULK_PUBLISH_EVENT]", "bulk-publish");
185
+ form.append("variables[BULK_PUBLISH_POSTS]", documentIds.join(","));
186
+ form.append("variables[BULK_PUBLISH_DATE]", (/* @__PURE__ */ new Date()).toISOString());
187
+ for (const { key, value } of config2.variables) {
188
+ if (key) form.append(`variables[${key}]`, value);
189
+ }
190
+ return form;
191
+ }
192
+ function logTruncated(strapi, status, text) {
193
+ const truncated = text.length > MAX_LOG_BODY_LENGTH ? text.slice(0, MAX_LOG_BODY_LENGTH) + "..." : text;
194
+ strapi.log.warn(`bulk-publish webhook returned ${status}: ${truncated}`);
195
+ }
147
196
  const webhook = ({ strapi }) => {
148
197
  const getStore = () => strapi.store({ type: "plugin", name: "bulk-publish" });
149
198
  return {
150
- async getWebhookUrl() {
151
- const url = await getStore().get({ key: "webhookUrl" });
152
- return url || "";
199
+ async getConfig() {
200
+ const stored = await getStore().get({ key: "webhookConfig" });
201
+ if (stored && typeof stored === "object") {
202
+ return stored;
203
+ }
204
+ return { ...DEFAULT_WEBHOOK_CONFIG };
153
205
  },
154
- async setWebhookUrl(url) {
155
- await getStore().set({ key: "webhookUrl", value: url });
206
+ async setConfig(config2) {
207
+ await getStore().set({ key: "webhookConfig", value: config2 });
156
208
  },
157
209
  async trigger(documentIds) {
158
- const webhookUrl = await this.getWebhookUrl();
159
- if (!webhookUrl) {
210
+ const config2 = await this.getConfig();
211
+ if (!config2.url) {
160
212
  return { triggered: false, error: "No webhook URL configured" };
161
213
  }
162
- if (!isAllowedWebhookUrl(webhookUrl)) {
214
+ if (!isAllowedWebhookUrl(config2.url)) {
163
215
  strapi.log.warn("bulk-publish: webhook URL blocked by SSRF protection");
164
216
  return { triggered: false, error: "Webhook URL is not allowed" };
165
217
  }
166
218
  try {
167
- const response = await fetch(webhookUrl, {
168
- method: "POST",
169
- headers: { "Content-Type": "application/json" },
170
- body: JSON.stringify({
171
- event: "bulk-publish",
172
- posts: documentIds,
173
- publishedAt: (/* @__PURE__ */ new Date()).toISOString()
174
- }),
175
- signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
176
- });
219
+ let response;
220
+ if (config2.preset === "gitlab") {
221
+ const form = buildGitlabRequest(config2, documentIds);
222
+ response = await fetch(config2.url, {
223
+ method: "POST",
224
+ body: form,
225
+ signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
226
+ });
227
+ } else {
228
+ const { headers, body } = buildGenericRequest(config2, documentIds);
229
+ response = await fetch(config2.url, {
230
+ method: "POST",
231
+ headers,
232
+ body,
233
+ signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
234
+ });
235
+ }
177
236
  if (!response.ok) {
178
237
  const text = await response.text();
179
- const truncated = text.length > MAX_LOG_BODY_LENGTH ? text.slice(0, MAX_LOG_BODY_LENGTH) + "..." : text;
180
- strapi.log.warn(`bulk-publish webhook returned ${response.status}: ${truncated}`);
238
+ logTruncated(strapi, response.status, text);
181
239
  return { triggered: true, error: `Webhook returned ${response.status}` };
182
240
  }
183
241
  return { triggered: true };
@@ -226,19 +284,44 @@ const bulkPublish = ({ strapi }) => {
226
284
  };
227
285
  },
228
286
  async getSettings(ctx) {
229
- const webhookUrl = await webhookService().getWebhookUrl();
230
- ctx.body = { data: { webhookUrl } };
287
+ const config2 = await webhookService().getConfig();
288
+ ctx.body = { data: config2 };
231
289
  },
232
290
  async updateSettings(ctx) {
233
- const { webhookUrl } = ctx.request.body;
234
- if (typeof webhookUrl !== "string") {
235
- return ctx.badRequest("webhookUrl must be a string");
291
+ const body = ctx.request.body;
292
+ if (!body || typeof body !== "object") {
293
+ return ctx.badRequest("Request body is required");
294
+ }
295
+ const { preset, url, token, ref, variables } = body;
296
+ if (typeof preset !== "string" || !WEBHOOK_PRESETS.includes(preset)) {
297
+ return ctx.badRequest(`preset must be one of: ${WEBHOOK_PRESETS.join(", ")}`);
298
+ }
299
+ if (typeof url !== "string") {
300
+ return ctx.badRequest("url must be a string");
301
+ }
302
+ if (url && !isAllowedWebhookUrl(url)) {
303
+ return ctx.badRequest("url must be a valid public HTTP(S) URL");
304
+ }
305
+ if (typeof token !== "string") {
306
+ return ctx.badRequest("token must be a string");
307
+ }
308
+ if (typeof ref !== "string") {
309
+ return ctx.badRequest("ref must be a string");
310
+ }
311
+ if (preset === "gitlab" && !ref) {
312
+ return ctx.badRequest("ref is required for GitLab preset");
313
+ }
314
+ if (!Array.isArray(variables)) {
315
+ return ctx.badRequest("variables must be an array");
236
316
  }
237
- if (webhookUrl && !isAllowedWebhookUrl(webhookUrl)) {
238
- return ctx.badRequest("webhookUrl must be a valid public HTTP(S) URL");
317
+ if (!variables.every(
318
+ (v) => typeof v === "object" && v !== null && typeof v.key === "string" && typeof v.value === "string"
319
+ )) {
320
+ return ctx.badRequest("Each variable must have string key and value");
239
321
  }
240
- await webhookService().setWebhookUrl(webhookUrl);
241
- ctx.body = { data: { webhookUrl } };
322
+ const config2 = { preset, url, token, ref, variables };
323
+ await webhookService().setConfig(config2);
324
+ ctx.body = { data: config2 };
242
325
  }
243
326
  };
244
327
  };
@@ -1,5 +1,13 @@
1
1
  const register = (_args) => {
2
2
  };
3
+ const WEBHOOK_PRESETS = ["generic", "gitlab"];
4
+ const DEFAULT_WEBHOOK_CONFIG = {
5
+ preset: "generic",
6
+ url: "",
7
+ token: "",
8
+ ref: "main",
9
+ variables: []
10
+ };
3
11
  const bootstrap = async ({ strapi }) => {
4
12
  strapi.log.debug("Bulk Publish plugin bootstrapped");
5
13
  await strapi.service("admin::permission").actionProvider.registerMany([
@@ -17,17 +25,30 @@ const bootstrap = async ({ strapi }) => {
17
25
  }
18
26
  ]);
19
27
  const store = strapi.store({ type: "plugin", name: "bulk-publish" });
20
- const existingUrl = await store.get({ key: "webhookUrl" });
21
- if (existingUrl === null || existingUrl === void 0) {
22
- const configUrl = strapi.plugin("bulk-publish").config("webhookUrl") || "";
23
- await store.set({ key: "webhookUrl", value: configUrl });
28
+ const existingConfig = await store.get({ key: "webhookConfig" });
29
+ if (existingConfig) return;
30
+ const legacyUrl = await store.get({ key: "webhookUrl" });
31
+ if (legacyUrl && typeof legacyUrl === "string") {
32
+ const migrated = {
33
+ ...DEFAULT_WEBHOOK_CONFIG,
34
+ url: legacyUrl
35
+ };
36
+ await store.set({ key: "webhookConfig", value: migrated });
37
+ await store.set({ key: "webhookUrl", value: null });
38
+ strapi.log.info("bulk-publish: migrated webhookUrl to webhookConfig");
39
+ return;
24
40
  }
41
+ const configUrl = strapi.plugin("bulk-publish").config("webhookUrl") || "";
42
+ const seeded = {
43
+ ...DEFAULT_WEBHOOK_CONFIG,
44
+ url: typeof configUrl === "string" ? configUrl : ""
45
+ };
46
+ await store.set({ key: "webhookConfig", value: seeded });
25
47
  };
26
48
  const config = {
27
49
  default: {
28
50
  contentType: "",
29
- titleField: "title",
30
- webhookUrl: ""
51
+ titleField: "title"
31
52
  },
32
53
  validator: (config2) => {
33
54
  if (!config2.contentType || typeof config2.contentType !== "string") {
@@ -38,9 +59,6 @@ const config = {
38
59
  if (config2.titleField && typeof config2.titleField !== "string") {
39
60
  throw new Error("bulk-publish: titleField must be a string");
40
61
  }
41
- if (config2.webhookUrl && typeof config2.webhookUrl !== "string") {
42
- throw new Error("bulk-publish: webhookUrl must be a string");
43
- }
44
62
  }
45
63
  };
46
64
  const admin = {
@@ -143,40 +161,80 @@ function isAllowedWebhookUrl(urlStr) {
143
161
  return false;
144
162
  }
145
163
  }
164
+ function buildGenericRequest(config2, documentIds) {
165
+ const payload = {
166
+ event: "bulk-publish",
167
+ posts: documentIds,
168
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString()
169
+ };
170
+ for (const { key, value } of config2.variables) {
171
+ if (key) payload[key] = value;
172
+ }
173
+ const headers = { "Content-Type": "application/json" };
174
+ if (config2.token) {
175
+ headers["Authorization"] = `Bearer ${config2.token}`;
176
+ }
177
+ return { headers, body: JSON.stringify(payload) };
178
+ }
179
+ function buildGitlabRequest(config2, documentIds) {
180
+ const form = new FormData();
181
+ form.append("token", config2.token);
182
+ form.append("ref", config2.ref || "main");
183
+ form.append("variables[BULK_PUBLISH_EVENT]", "bulk-publish");
184
+ form.append("variables[BULK_PUBLISH_POSTS]", documentIds.join(","));
185
+ form.append("variables[BULK_PUBLISH_DATE]", (/* @__PURE__ */ new Date()).toISOString());
186
+ for (const { key, value } of config2.variables) {
187
+ if (key) form.append(`variables[${key}]`, value);
188
+ }
189
+ return form;
190
+ }
191
+ function logTruncated(strapi, status, text) {
192
+ const truncated = text.length > MAX_LOG_BODY_LENGTH ? text.slice(0, MAX_LOG_BODY_LENGTH) + "..." : text;
193
+ strapi.log.warn(`bulk-publish webhook returned ${status}: ${truncated}`);
194
+ }
146
195
  const webhook = ({ strapi }) => {
147
196
  const getStore = () => strapi.store({ type: "plugin", name: "bulk-publish" });
148
197
  return {
149
- async getWebhookUrl() {
150
- const url = await getStore().get({ key: "webhookUrl" });
151
- return url || "";
198
+ async getConfig() {
199
+ const stored = await getStore().get({ key: "webhookConfig" });
200
+ if (stored && typeof stored === "object") {
201
+ return stored;
202
+ }
203
+ return { ...DEFAULT_WEBHOOK_CONFIG };
152
204
  },
153
- async setWebhookUrl(url) {
154
- await getStore().set({ key: "webhookUrl", value: url });
205
+ async setConfig(config2) {
206
+ await getStore().set({ key: "webhookConfig", value: config2 });
155
207
  },
156
208
  async trigger(documentIds) {
157
- const webhookUrl = await this.getWebhookUrl();
158
- if (!webhookUrl) {
209
+ const config2 = await this.getConfig();
210
+ if (!config2.url) {
159
211
  return { triggered: false, error: "No webhook URL configured" };
160
212
  }
161
- if (!isAllowedWebhookUrl(webhookUrl)) {
213
+ if (!isAllowedWebhookUrl(config2.url)) {
162
214
  strapi.log.warn("bulk-publish: webhook URL blocked by SSRF protection");
163
215
  return { triggered: false, error: "Webhook URL is not allowed" };
164
216
  }
165
217
  try {
166
- const response = await fetch(webhookUrl, {
167
- method: "POST",
168
- headers: { "Content-Type": "application/json" },
169
- body: JSON.stringify({
170
- event: "bulk-publish",
171
- posts: documentIds,
172
- publishedAt: (/* @__PURE__ */ new Date()).toISOString()
173
- }),
174
- signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
175
- });
218
+ let response;
219
+ if (config2.preset === "gitlab") {
220
+ const form = buildGitlabRequest(config2, documentIds);
221
+ response = await fetch(config2.url, {
222
+ method: "POST",
223
+ body: form,
224
+ signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
225
+ });
226
+ } else {
227
+ const { headers, body } = buildGenericRequest(config2, documentIds);
228
+ response = await fetch(config2.url, {
229
+ method: "POST",
230
+ headers,
231
+ body,
232
+ signal: AbortSignal.timeout(WEBHOOK_TIMEOUT_MS)
233
+ });
234
+ }
176
235
  if (!response.ok) {
177
236
  const text = await response.text();
178
- const truncated = text.length > MAX_LOG_BODY_LENGTH ? text.slice(0, MAX_LOG_BODY_LENGTH) + "..." : text;
179
- strapi.log.warn(`bulk-publish webhook returned ${response.status}: ${truncated}`);
237
+ logTruncated(strapi, response.status, text);
180
238
  return { triggered: true, error: `Webhook returned ${response.status}` };
181
239
  }
182
240
  return { triggered: true };
@@ -225,19 +283,44 @@ const bulkPublish = ({ strapi }) => {
225
283
  };
226
284
  },
227
285
  async getSettings(ctx) {
228
- const webhookUrl = await webhookService().getWebhookUrl();
229
- ctx.body = { data: { webhookUrl } };
286
+ const config2 = await webhookService().getConfig();
287
+ ctx.body = { data: config2 };
230
288
  },
231
289
  async updateSettings(ctx) {
232
- const { webhookUrl } = ctx.request.body;
233
- if (typeof webhookUrl !== "string") {
234
- return ctx.badRequest("webhookUrl must be a string");
290
+ const body = ctx.request.body;
291
+ if (!body || typeof body !== "object") {
292
+ return ctx.badRequest("Request body is required");
293
+ }
294
+ const { preset, url, token, ref, variables } = body;
295
+ if (typeof preset !== "string" || !WEBHOOK_PRESETS.includes(preset)) {
296
+ return ctx.badRequest(`preset must be one of: ${WEBHOOK_PRESETS.join(", ")}`);
297
+ }
298
+ if (typeof url !== "string") {
299
+ return ctx.badRequest("url must be a string");
300
+ }
301
+ if (url && !isAllowedWebhookUrl(url)) {
302
+ return ctx.badRequest("url must be a valid public HTTP(S) URL");
303
+ }
304
+ if (typeof token !== "string") {
305
+ return ctx.badRequest("token must be a string");
306
+ }
307
+ if (typeof ref !== "string") {
308
+ return ctx.badRequest("ref must be a string");
309
+ }
310
+ if (preset === "gitlab" && !ref) {
311
+ return ctx.badRequest("ref is required for GitLab preset");
312
+ }
313
+ if (!Array.isArray(variables)) {
314
+ return ctx.badRequest("variables must be an array");
235
315
  }
236
- if (webhookUrl && !isAllowedWebhookUrl(webhookUrl)) {
237
- return ctx.badRequest("webhookUrl must be a valid public HTTP(S) URL");
316
+ if (!variables.every(
317
+ (v) => typeof v === "object" && v !== null && typeof v.key === "string" && typeof v.value === "string"
318
+ )) {
319
+ return ctx.badRequest("Each variable must have string key and value");
238
320
  }
239
- await webhookService().setWebhookUrl(webhookUrl);
240
- ctx.body = { data: { webhookUrl } };
321
+ const config2 = { preset, url, token, ref, variables };
322
+ await webhookService().setConfig(config2);
323
+ ctx.body = { data: config2 };
241
324
  }
242
325
  };
243
326
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-bulk-publish",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Strapi 5 plugin to bulk publish content across all locales with a single webhook trigger",
5
5
  "license": "MIT",
6
6
  "author": "Alexander Vitshas <avitshas@gmail.com>",
@@ -1,75 +0,0 @@
1
- import { jsx, jsxs, Fragment } from "react/jsx-runtime";
2
- import { useState, useCallback, useEffect } from "react";
3
- import { u as useIntl } from "./index-CEh8vkxY.mjs";
4
- import { Main, Flex, Loader, Box, Typography, Button, TextInput } from "@strapi/design-system";
5
- import { useFetchClient, useNotification, Page } from "@strapi/strapi/admin";
6
- import { P as PLUGIN_ID, p as pluginPermissions } from "./index-CBDMXg1c.mjs";
7
- const SettingsPage = () => {
8
- const { formatMessage } = useIntl();
9
- const { get, put } = useFetchClient();
10
- const { toggleNotification } = useNotification();
11
- const [webhookUrl, setWebhookUrl] = useState("");
12
- const [initialUrl, setInitialUrl] = useState("");
13
- const [loading, setLoading] = useState(true);
14
- const [saving, setSaving] = useState(false);
15
- const fetchSettings = useCallback(async () => {
16
- try {
17
- const { data } = await get(`/${PLUGIN_ID}/settings`);
18
- const url = data.data?.webhookUrl || "";
19
- setWebhookUrl(url);
20
- setInitialUrl(url);
21
- } catch {
22
- toggleNotification({
23
- type: "danger",
24
- message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.error` })
25
- });
26
- } finally {
27
- setLoading(false);
28
- }
29
- }, [get, toggleNotification, formatMessage]);
30
- useEffect(() => {
31
- fetchSettings();
32
- }, [fetchSettings]);
33
- const handleSave = async () => {
34
- try {
35
- setSaving(true);
36
- await put(`/${PLUGIN_ID}/settings`, { webhookUrl });
37
- setInitialUrl(webhookUrl);
38
- toggleNotification({
39
- type: "success",
40
- message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.success` })
41
- });
42
- } catch {
43
- toggleNotification({
44
- type: "danger",
45
- message: formatMessage({ id: `${PLUGIN_ID}.notification.settings.error` })
46
- });
47
- } finally {
48
- setSaving(false);
49
- }
50
- };
51
- const hasChanged = webhookUrl !== initialUrl;
52
- return /* @__PURE__ */ jsx(Page.Protect, { permissions: pluginPermissions.settings, children: /* @__PURE__ */ jsx(Main, { children: loading ? /* @__PURE__ */ jsx(Flex, { justifyContent: "center", paddingTop: 8, children: /* @__PURE__ */ jsx(Loader, { children: formatMessage({ id: `${PLUGIN_ID}.loading.settings` }) }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
53
- /* @__PURE__ */ jsx(Box, { paddingTop: 8, paddingBottom: 4, paddingLeft: 10, paddingRight: 10, children: /* @__PURE__ */ jsxs(Flex, { justifyContent: "space-between", alignItems: "center", children: [
54
- /* @__PURE__ */ jsxs(Box, { children: [
55
- /* @__PURE__ */ jsx(Typography, { variant: "alpha", tag: "h1", children: formatMessage({ id: `${PLUGIN_ID}.settings.title` }) }),
56
- /* @__PURE__ */ jsx(Typography, { variant: "epsilon", textColor: "neutral600", children: formatMessage({ id: `${PLUGIN_ID}.settings.subtitle` }) })
57
- ] }),
58
- /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: !hasChanged || saving, loading: saving, children: formatMessage({ id: `${PLUGIN_ID}.button.save` }) })
59
- ] }) }),
60
- /* @__PURE__ */ jsx(Box, { paddingLeft: 10, paddingRight: 10, paddingBottom: 10, children: /* @__PURE__ */ jsx(Box, { background: "neutral0", padding: 6, shadow: "tableShadow", hasRadius: true, children: /* @__PURE__ */ jsx(
61
- TextInput,
62
- {
63
- label: formatMessage({ id: `${PLUGIN_ID}.webhook.label` }),
64
- placeholder: formatMessage({ id: `${PLUGIN_ID}.webhook.placeholder` }),
65
- hint: formatMessage({ id: `${PLUGIN_ID}.webhook.hint` }),
66
- name: "webhookUrl",
67
- value: webhookUrl,
68
- onChange: (e) => setWebhookUrl(e.target.value)
69
- }
70
- ) }) })
71
- ] }) }) });
72
- };
73
- export {
74
- SettingsPage
75
- };
@@ -1,75 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const jsxRuntime = require("react/jsx-runtime");
4
- const React = require("react");
5
- const index = require("./index-DkTxsEqL.js");
6
- const designSystem = require("@strapi/design-system");
7
- const admin = require("@strapi/strapi/admin");
8
- const index$1 = require("./index-BBZKQRB2.js");
9
- const SettingsPage = () => {
10
- const { formatMessage } = index.useIntl();
11
- const { get, put } = admin.useFetchClient();
12
- const { toggleNotification } = admin.useNotification();
13
- const [webhookUrl, setWebhookUrl] = React.useState("");
14
- const [initialUrl, setInitialUrl] = React.useState("");
15
- const [loading, setLoading] = React.useState(true);
16
- const [saving, setSaving] = React.useState(false);
17
- const fetchSettings = React.useCallback(async () => {
18
- try {
19
- const { data } = await get(`/${index$1.PLUGIN_ID}/settings`);
20
- const url = data.data?.webhookUrl || "";
21
- setWebhookUrl(url);
22
- setInitialUrl(url);
23
- } catch {
24
- toggleNotification({
25
- type: "danger",
26
- message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.error` })
27
- });
28
- } finally {
29
- setLoading(false);
30
- }
31
- }, [get, toggleNotification, formatMessage]);
32
- React.useEffect(() => {
33
- fetchSettings();
34
- }, [fetchSettings]);
35
- const handleSave = async () => {
36
- try {
37
- setSaving(true);
38
- await put(`/${index$1.PLUGIN_ID}/settings`, { webhookUrl });
39
- setInitialUrl(webhookUrl);
40
- toggleNotification({
41
- type: "success",
42
- message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.success` })
43
- });
44
- } catch {
45
- toggleNotification({
46
- type: "danger",
47
- message: formatMessage({ id: `${index$1.PLUGIN_ID}.notification.settings.error` })
48
- });
49
- } finally {
50
- setSaving(false);
51
- }
52
- };
53
- const hasChanged = webhookUrl !== initialUrl;
54
- return /* @__PURE__ */ jsxRuntime.jsx(admin.Page.Protect, { permissions: index$1.pluginPermissions.settings, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Main, { children: loading ? /* @__PURE__ */ jsxRuntime.jsx(designSystem.Flex, { justifyContent: "center", paddingTop: 8, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Loader, { children: formatMessage({ id: `${index$1.PLUGIN_ID}.loading.settings` }) }) }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
55
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingTop: 8, paddingBottom: 4, paddingLeft: 10, paddingRight: 10, children: /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Flex, { justifyContent: "space-between", alignItems: "center", children: [
56
- /* @__PURE__ */ jsxRuntime.jsxs(designSystem.Box, { children: [
57
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "alpha", tag: "h1", children: formatMessage({ id: `${index$1.PLUGIN_ID}.settings.title` }) }),
58
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Typography, { variant: "epsilon", textColor: "neutral600", children: formatMessage({ id: `${index$1.PLUGIN_ID}.settings.subtitle` }) })
59
- ] }),
60
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Button, { onClick: handleSave, disabled: !hasChanged || saving, loading: saving, children: formatMessage({ id: `${index$1.PLUGIN_ID}.button.save` }) })
61
- ] }) }),
62
- /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { paddingLeft: 10, paddingRight: 10, paddingBottom: 10, children: /* @__PURE__ */ jsxRuntime.jsx(designSystem.Box, { background: "neutral0", padding: 6, shadow: "tableShadow", hasRadius: true, children: /* @__PURE__ */ jsxRuntime.jsx(
63
- designSystem.TextInput,
64
- {
65
- label: formatMessage({ id: `${index$1.PLUGIN_ID}.webhook.label` }),
66
- placeholder: formatMessage({ id: `${index$1.PLUGIN_ID}.webhook.placeholder` }),
67
- hint: formatMessage({ id: `${index$1.PLUGIN_ID}.webhook.hint` }),
68
- name: "webhookUrl",
69
- value: webhookUrl,
70
- onChange: (e) => setWebhookUrl(e.target.value)
71
- }
72
- ) }) })
73
- ] }) }) });
74
- };
75
- exports.SettingsPage = SettingsPage;