strapi-plugin-hubspot 0.11.0 → 0.13.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/CHANGELOG.md +33 -0
- package/README.md +18 -0
- package/dist/_chunks/{Forms-DadTbe9Q.mjs → Forms-CHB-sKem.mjs} +218 -121
- package/dist/_chunks/{Forms-Dn2W6OS4.js → Forms-lrzuWzoB.js} +217 -120
- package/dist/_chunks/{HubspotFormInput-S_Z2t_DN.js → HubspotFormInput-Bb6it0nP.js} +1 -1
- package/dist/_chunks/{HubspotFormInput-NO59AtM_.mjs → HubspotFormInput-YI09keTC.mjs} +1 -1
- package/dist/_chunks/{HubspotObjectInput-DAo3FAMz.mjs → HubspotObjectInput-CzLKT9oj.mjs} +3 -3
- package/dist/_chunks/{HubspotObjectInput-BIgIa_Dm.js → HubspotObjectInput-D3A6xzb3.js} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-KX1Bhcsl.mjs → HubspotPropertyInput-DicskLcN.mjs} +3 -3
- package/dist/_chunks/{HubspotPropertyInput-B-piLcIy.js → HubspotPropertyInput-Dss3eUlL.js} +3 -3
- package/dist/_chunks/{Settings-Dz0PEFOv.mjs → Settings-4CbX3SPK.mjs} +2 -2
- package/dist/_chunks/{Settings-DMu_QThB.js → Settings-Ch8tPZAV.js} +2 -2
- package/dist/_chunks/{en-CH5LSPkL.mjs → en-D8DJiSxd.mjs} +3 -1
- package/dist/_chunks/{en-BDGqW_yS.js → en-PYFibnDc.js} +3 -1
- package/dist/_chunks/{fr-65-RaB3w.mjs → fr-BMjixfXn.mjs} +3 -1
- package/dist/_chunks/{fr-C82LMqUM.js → fr-yHk4sSEl.js} +3 -1
- package/dist/_chunks/{index-B7Ru1fu5.js → index-BAvedd2m.js} +6 -6
- package/dist/_chunks/{index-DA31TY2n.mjs → index-BE2ribUL.mjs} +6 -6
- package/dist/_chunks/{objectLabels-FTh984_h.js → objectLabels-CycoU0zY.js} +1 -1
- package/dist/_chunks/{objectLabels-Bl7sO5pb.mjs → objectLabels-DGJxYjKz.mjs} +1 -1
- package/dist/_chunks/{useHubspotSchema-C2_PuXEm.js → useHubspotSchema-DRfaI6mF.js} +1 -1
- package/dist/_chunks/{useHubspotSchema-BlDilCHc.mjs → useHubspotSchema-e31UsYb2.mjs} +1 -1
- package/dist/admin/index.js +1 -1
- package/dist/admin/index.mjs +1 -1
- package/dist/admin/src/builder/Sortable.d.ts +25 -0
- package/dist/admin/src/builder/__tests__/CompanyMapEditor.test.d.ts +1 -0
- package/dist/admin/src/builder/__tests__/ConditionEditor.test.d.ts +1 -0
- package/dist/admin/src/builder/__tests__/testUtils.d.ts +4 -0
- package/dist/server/index.js +110 -44
- package/dist/server/index.mjs +110 -44
- package/dist/server/src/__tests__/contract.test.d.ts +1 -0
- package/dist/server/src/__tests__/rateLimit.test.d.ts +1 -0
- package/dist/server/src/company.d.ts +5 -0
- package/dist/server/src/index.d.ts +12 -3
- package/dist/server/src/rateLimit.d.ts +18 -0
- package/package.json +11 -2
|
@@ -25,7 +25,7 @@ const index = {
|
|
|
25
25
|
icon: PaperPlane,
|
|
26
26
|
intlLabel: { id: `${PLUGIN_ID}.menu.label`, defaultMessage: "HubSpot Forms" },
|
|
27
27
|
permissions: [{ action: `plugin::${PLUGIN_ID}.forms`, subject: null }],
|
|
28
|
-
Component: async () => (await import("./Forms-
|
|
28
|
+
Component: async () => (await import("./Forms-CHB-sKem.mjs")).default
|
|
29
29
|
});
|
|
30
30
|
app.customFields.register({
|
|
31
31
|
name: "property",
|
|
@@ -40,7 +40,7 @@ const index = {
|
|
|
40
40
|
defaultMessage: "Picked from the portal's real properties"
|
|
41
41
|
},
|
|
42
42
|
components: {
|
|
43
|
-
Input: async () => import("./HubspotPropertyInput-
|
|
43
|
+
Input: async () => import("./HubspotPropertyInput-DicskLcN.mjs")
|
|
44
44
|
},
|
|
45
45
|
// Exposed in the Content-Type Builder. Without this, `objectField` could
|
|
46
46
|
// only be set by hand-editing schema.json — which is not how anyone
|
|
@@ -103,7 +103,7 @@ const index = {
|
|
|
103
103
|
defaultMessage: "Picked from the portal's configured objects"
|
|
104
104
|
},
|
|
105
105
|
components: {
|
|
106
|
-
Input: async () => import("./HubspotObjectInput-
|
|
106
|
+
Input: async () => import("./HubspotObjectInput-CzLKT9oj.mjs")
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
app.customFields.register({
|
|
@@ -119,7 +119,7 @@ const index = {
|
|
|
119
119
|
defaultMessage: "A form built in the HubSpot form builder, stored by slug"
|
|
120
120
|
},
|
|
121
121
|
components: {
|
|
122
|
-
Input: async () => import("./HubspotFormInput-
|
|
122
|
+
Input: async () => import("./HubspotFormInput-YI09keTC.mjs")
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
125
|
app.createSettingSection(
|
|
@@ -135,7 +135,7 @@ const index = {
|
|
|
135
135
|
// Mirrors the RBAC action registered server-side: a role without it
|
|
136
136
|
// doesn't see the link at all — the routes refuse it anyway.
|
|
137
137
|
permissions: [{ action: `plugin::${PLUGIN_ID}.settings`, subject: null }],
|
|
138
|
-
Component: async () => (await import("./Settings-
|
|
138
|
+
Component: async () => (await import("./Settings-4CbX3SPK.mjs")).default
|
|
139
139
|
}
|
|
140
140
|
]
|
|
141
141
|
);
|
|
@@ -145,7 +145,7 @@ const index = {
|
|
|
145
145
|
return Promise.all(
|
|
146
146
|
locales.map(async (locale) => {
|
|
147
147
|
try {
|
|
148
|
-
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-
|
|
148
|
+
const { default: data } = await __variableDynamicImportRuntimeHelper(/* @__PURE__ */ Object.assign({ "./translations/en.json": () => import("./en-D8DJiSxd.mjs"), "./translations/fr.json": () => import("./fr-BMjixfXn.mjs") }), `./translations/${locale}.json`, 3);
|
|
149
149
|
return { data: prefixPluginTranslations(data), locale };
|
|
150
150
|
} catch {
|
|
151
151
|
return { data: {}, locale };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const React = require("react");
|
|
3
3
|
const admin = require("@strapi/strapi/admin");
|
|
4
|
-
const index = require("./index-
|
|
4
|
+
const index = require("./index-BAvedd2m.js");
|
|
5
5
|
function _interopNamespace(e) {
|
|
6
6
|
if (e && e.__esModule) return e;
|
|
7
7
|
const n = Object.create(null, { [Symbol.toStringTag]: { value: "Module" } });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
3
|
-
import { P as PLUGIN_ID } from "./index-
|
|
3
|
+
import { P as PLUGIN_ID } from "./index-BE2ribUL.mjs";
|
|
4
4
|
let cached = null;
|
|
5
5
|
let inFlight = null;
|
|
6
6
|
const listeners = /* @__PURE__ */ new Set();
|
package/dist/admin/index.js
CHANGED
package/dist/admin/index.mjs
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { arrayMove } from "@dnd-kit/sortable";
|
|
3
|
+
export { arrayMove };
|
|
4
|
+
/** The drag-handle props to spread on the grip element. */
|
|
5
|
+
export interface HandleProps {
|
|
6
|
+
attributes: React.HTMLAttributes<HTMLElement>;
|
|
7
|
+
listeners: Record<string, unknown> | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One vertical sortable list. Each list gets its OWN DndContext: steps sort
|
|
11
|
+
* among steps, a step's fields sort among themselves — no cross-container
|
|
12
|
+
* ambiguity, and the arrow buttons keep covering cross-step moves (they also
|
|
13
|
+
* stay the accessible path).
|
|
14
|
+
*/
|
|
15
|
+
export declare function SortableList({ ids, onReorder, children, }: {
|
|
16
|
+
ids: string[];
|
|
17
|
+
/** Called with the reordered ids after a completed drag. */
|
|
18
|
+
onReorder: (ids: string[]) => void;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}): React.JSX.Element;
|
|
21
|
+
/** A sortable row: wraps the card, hands the grip props to its children. */
|
|
22
|
+
export declare function SortableItem({ id, children, }: {
|
|
23
|
+
id: string;
|
|
24
|
+
children: (handle: HandleProps) => React.ReactNode;
|
|
25
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server/index.js
CHANGED
|
@@ -1179,13 +1179,20 @@ async function query(params) {
|
|
|
1179
1179
|
return null;
|
|
1180
1180
|
}
|
|
1181
1181
|
}
|
|
1182
|
+
function normalizeQuery(q) {
|
|
1183
|
+
const trimmed = q.trim();
|
|
1184
|
+
const digits = trimmed.replace(/[\s.-]/g, "");
|
|
1185
|
+
return /^\d{9}$/.test(digits) || /^\d{14}$/.test(digits) ? digits : trimmed;
|
|
1186
|
+
}
|
|
1182
1187
|
async function searchCompanies(q) {
|
|
1183
|
-
const trimmed = q
|
|
1188
|
+
const trimmed = normalizeQuery(q).slice(0, 60);
|
|
1184
1189
|
if (trimmed.length < 3) return [];
|
|
1185
1190
|
const key = trimmed.toLowerCase();
|
|
1186
1191
|
const cached = cache.get(key);
|
|
1187
1192
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.hits;
|
|
1188
|
-
const payload = await query(
|
|
1193
|
+
const payload = await query(
|
|
1194
|
+
`q=${encodeURIComponent(trimmed)}&per_page=8&etat_administratif=A&limite_matching_etablissements=5`
|
|
1195
|
+
);
|
|
1189
1196
|
const hits = payload ? normalizeCompanyHits(payload) : [];
|
|
1190
1197
|
if (payload) cache.set(key, { at: Date.now(), hits });
|
|
1191
1198
|
return hits;
|
|
@@ -1746,6 +1753,10 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1746
1753
|
});
|
|
1747
1754
|
}
|
|
1748
1755
|
async function submit(form, rawValues, meta = {}) {
|
|
1756
|
+
const hp = rawValues.__hp;
|
|
1757
|
+
if (typeof hp === "string" && hp.trim() !== "") {
|
|
1758
|
+
return { ok: true, hubspotSynced: false };
|
|
1759
|
+
}
|
|
1749
1760
|
const resolution = resolveSubmission(form.definition, rawValues);
|
|
1750
1761
|
if (resolution.missingRequired.length) {
|
|
1751
1762
|
return { ok: false, missingRequired: resolution.missingRequired };
|
|
@@ -2503,6 +2514,36 @@ function createFormsAdminController(strapi) {
|
|
|
2503
2514
|
}
|
|
2504
2515
|
};
|
|
2505
2516
|
}
|
|
2517
|
+
function createLimiter({
|
|
2518
|
+
windowMs,
|
|
2519
|
+
max,
|
|
2520
|
+
now = Date.now
|
|
2521
|
+
}) {
|
|
2522
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2523
|
+
let lastSweep = 0;
|
|
2524
|
+
return {
|
|
2525
|
+
allow(key) {
|
|
2526
|
+
if (max <= 0) return true;
|
|
2527
|
+
const at = now();
|
|
2528
|
+
const floor = at - windowMs;
|
|
2529
|
+
if (at - lastSweep >= windowMs) {
|
|
2530
|
+
lastSweep = at;
|
|
2531
|
+
for (const [k, stamps] of hits) {
|
|
2532
|
+
if (!stamps.some((t) => t > floor)) hits.delete(k);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const recent = (hits.get(key) ?? []).filter((t) => t > floor);
|
|
2536
|
+
if (recent.length >= max) {
|
|
2537
|
+
hits.set(key, recent);
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
recent.push(at);
|
|
2541
|
+
hits.set(key, recent);
|
|
2542
|
+
return true;
|
|
2543
|
+
},
|
|
2544
|
+
size: () => hits.size
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2506
2547
|
function fieldOrder(definition) {
|
|
2507
2548
|
if (!definition) return [];
|
|
2508
2549
|
return definition.steps.flatMap((step) => step.fields.map((field) => field.name));
|
|
@@ -2690,7 +2731,12 @@ const config = {
|
|
|
2690
2731
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
2691
2732
|
// domain) and associates it to the contact;
|
|
2692
2733
|
// - timelineNote: a note recaps the submission on the contact's timeline.
|
|
2693
|
-
forms: {
|
|
2734
|
+
forms: {
|
|
2735
|
+
companyFromDomain: true,
|
|
2736
|
+
timelineNote: true,
|
|
2737
|
+
// Per-IP brakes on the public routes; 0 disables one.
|
|
2738
|
+
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
2739
|
+
}
|
|
2694
2740
|
},
|
|
2695
2741
|
validator(cfg) {
|
|
2696
2742
|
if (cfg.validate && !Array.isArray(cfg.validate)) {
|
|
@@ -2752,13 +2798,23 @@ const controllers = {
|
|
|
2752
2798
|
}
|
|
2753
2799
|
}),
|
|
2754
2800
|
formsAdmin: ({ strapi }) => createFormsAdminController(strapi),
|
|
2755
|
-
company: () =>
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
}
|
|
2761
|
-
|
|
2801
|
+
company: ({ strapi }) => {
|
|
2802
|
+
const limits = strapi.plugin("hubspot").config("forms", {});
|
|
2803
|
+
const limiter = createLimiter({
|
|
2804
|
+
windowMs: 6e4,
|
|
2805
|
+
max: limits.rateLimit?.searchPerMinute ?? 30
|
|
2806
|
+
});
|
|
2807
|
+
return {
|
|
2808
|
+
/** SIRENE autocomplete for the company field — bounded, cached upstream. */
|
|
2809
|
+
async search(ctx) {
|
|
2810
|
+
if (!limiter.allow(ctx.request.ip ?? "unknown")) {
|
|
2811
|
+
ctx.throw(429, "Too many searches — slow down.");
|
|
2812
|
+
}
|
|
2813
|
+
const q = typeof ctx.query.q === "string" ? ctx.query.q : "";
|
|
2814
|
+
ctx.body = { companies: await searchCompanies(q) };
|
|
2815
|
+
}
|
|
2816
|
+
};
|
|
2817
|
+
},
|
|
2762
2818
|
submissions: ({ strapi }) => ({
|
|
2763
2819
|
/** Paged submissions, newest first, optionally narrowed to one form. */
|
|
2764
2820
|
async list(ctx) {
|
|
@@ -2802,41 +2858,51 @@ const controllers = {
|
|
|
2802
2858
|
};
|
|
2803
2859
|
}
|
|
2804
2860
|
}),
|
|
2805
|
-
forms: ({ strapi }) =>
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
}
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
if (
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
ctx.
|
|
2834
|
-
|
|
2835
|
-
|
|
2861
|
+
forms: ({ strapi }) => {
|
|
2862
|
+
const limits = strapi.plugin("hubspot").config("forms", {});
|
|
2863
|
+
const submitLimiter = createLimiter({
|
|
2864
|
+
windowMs: 6e4,
|
|
2865
|
+
max: limits.rateLimit?.submitPerMinute ?? 6
|
|
2866
|
+
});
|
|
2867
|
+
return {
|
|
2868
|
+
/** Published form for the host frontend — CRM mapping stripped. */
|
|
2869
|
+
async findOne(ctx) {
|
|
2870
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
2871
|
+
filters: { slug: ctx.params.slug },
|
|
2872
|
+
status: "published",
|
|
2873
|
+
locale: ctx.query.locale
|
|
2874
|
+
});
|
|
2875
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
2876
|
+
ctx.body = publicForm(entry);
|
|
2877
|
+
},
|
|
2878
|
+
async submit(ctx) {
|
|
2879
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
2880
|
+
filters: { slug: ctx.params.slug },
|
|
2881
|
+
status: "published",
|
|
2882
|
+
locale: ctx.query.locale
|
|
2883
|
+
});
|
|
2884
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
2885
|
+
if (!submitLimiter.allow(ctx.request.ip ?? "unknown")) {
|
|
2886
|
+
ctx.throw(429, "Too many submissions — try again in a minute.");
|
|
2887
|
+
}
|
|
2888
|
+
const values = sanitizeRawValues(ctx.request.body?.values);
|
|
2889
|
+
if (!values || !Object.keys(values).length) ctx.throw(422, "Invalid submission");
|
|
2890
|
+
const rawMeta = ctx.request.body?.meta ?? {};
|
|
2891
|
+
const meta = {};
|
|
2892
|
+
for (const key of ["pagePath", "pageUrl", "originPath", "originLabel", "source"]) {
|
|
2893
|
+
const v = rawMeta[key];
|
|
2894
|
+
if (typeof v === "string" && v) meta[key] = v.slice(0, 500);
|
|
2895
|
+
}
|
|
2896
|
+
const outcome = await strapi.plugin("hubspot").service("forms").submit(entry, values, meta);
|
|
2897
|
+
if (!outcome.ok) {
|
|
2898
|
+
ctx.status = 422;
|
|
2899
|
+
ctx.body = { ok: false, missingRequired: outcome.missingRequired ?? [] };
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
ctx.body = { ok: true, hubspotSynced: outcome.hubspotSynced };
|
|
2836
2903
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
}),
|
|
2904
|
+
};
|
|
2905
|
+
},
|
|
2840
2906
|
settings: ({ strapi }) => ({
|
|
2841
2907
|
async get(ctx) {
|
|
2842
2908
|
ctx.body = await publicSettings(strapi);
|
package/dist/server/index.mjs
CHANGED
|
@@ -1178,13 +1178,20 @@ async function query(params) {
|
|
|
1178
1178
|
return null;
|
|
1179
1179
|
}
|
|
1180
1180
|
}
|
|
1181
|
+
function normalizeQuery(q) {
|
|
1182
|
+
const trimmed = q.trim();
|
|
1183
|
+
const digits = trimmed.replace(/[\s.-]/g, "");
|
|
1184
|
+
return /^\d{9}$/.test(digits) || /^\d{14}$/.test(digits) ? digits : trimmed;
|
|
1185
|
+
}
|
|
1181
1186
|
async function searchCompanies(q) {
|
|
1182
|
-
const trimmed = q
|
|
1187
|
+
const trimmed = normalizeQuery(q).slice(0, 60);
|
|
1183
1188
|
if (trimmed.length < 3) return [];
|
|
1184
1189
|
const key = trimmed.toLowerCase();
|
|
1185
1190
|
const cached = cache.get(key);
|
|
1186
1191
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.hits;
|
|
1187
|
-
const payload = await query(
|
|
1192
|
+
const payload = await query(
|
|
1193
|
+
`q=${encodeURIComponent(trimmed)}&per_page=8&etat_administratif=A&limite_matching_etablissements=5`
|
|
1194
|
+
);
|
|
1188
1195
|
const hits = payload ? normalizeCompanyHits(payload) : [];
|
|
1189
1196
|
if (payload) cache.set(key, { at: Date.now(), hits });
|
|
1190
1197
|
return hits;
|
|
@@ -1745,6 +1752,10 @@ function createFormsService(strapi, _opts = {}) {
|
|
|
1745
1752
|
});
|
|
1746
1753
|
}
|
|
1747
1754
|
async function submit(form, rawValues, meta = {}) {
|
|
1755
|
+
const hp = rawValues.__hp;
|
|
1756
|
+
if (typeof hp === "string" && hp.trim() !== "") {
|
|
1757
|
+
return { ok: true, hubspotSynced: false };
|
|
1758
|
+
}
|
|
1748
1759
|
const resolution = resolveSubmission(form.definition, rawValues);
|
|
1749
1760
|
if (resolution.missingRequired.length) {
|
|
1750
1761
|
return { ok: false, missingRequired: resolution.missingRequired };
|
|
@@ -2502,6 +2513,36 @@ function createFormsAdminController(strapi) {
|
|
|
2502
2513
|
}
|
|
2503
2514
|
};
|
|
2504
2515
|
}
|
|
2516
|
+
function createLimiter({
|
|
2517
|
+
windowMs,
|
|
2518
|
+
max,
|
|
2519
|
+
now = Date.now
|
|
2520
|
+
}) {
|
|
2521
|
+
const hits = /* @__PURE__ */ new Map();
|
|
2522
|
+
let lastSweep = 0;
|
|
2523
|
+
return {
|
|
2524
|
+
allow(key) {
|
|
2525
|
+
if (max <= 0) return true;
|
|
2526
|
+
const at = now();
|
|
2527
|
+
const floor = at - windowMs;
|
|
2528
|
+
if (at - lastSweep >= windowMs) {
|
|
2529
|
+
lastSweep = at;
|
|
2530
|
+
for (const [k, stamps] of hits) {
|
|
2531
|
+
if (!stamps.some((t) => t > floor)) hits.delete(k);
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
const recent = (hits.get(key) ?? []).filter((t) => t > floor);
|
|
2535
|
+
if (recent.length >= max) {
|
|
2536
|
+
hits.set(key, recent);
|
|
2537
|
+
return false;
|
|
2538
|
+
}
|
|
2539
|
+
recent.push(at);
|
|
2540
|
+
hits.set(key, recent);
|
|
2541
|
+
return true;
|
|
2542
|
+
},
|
|
2543
|
+
size: () => hits.size
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2505
2546
|
function fieldOrder(definition) {
|
|
2506
2547
|
if (!definition) return [];
|
|
2507
2548
|
return definition.steps.flatMap((step) => step.fields.map((field) => field.name));
|
|
@@ -2689,7 +2730,12 @@ const config = {
|
|
|
2689
2730
|
// - companyFromDomain: a corporate email upserts the Company (deduped by
|
|
2690
2731
|
// domain) and associates it to the contact;
|
|
2691
2732
|
// - timelineNote: a note recaps the submission on the contact's timeline.
|
|
2692
|
-
forms: {
|
|
2733
|
+
forms: {
|
|
2734
|
+
companyFromDomain: true,
|
|
2735
|
+
timelineNote: true,
|
|
2736
|
+
// Per-IP brakes on the public routes; 0 disables one.
|
|
2737
|
+
rateLimit: { submitPerMinute: 6, searchPerMinute: 30 }
|
|
2738
|
+
}
|
|
2693
2739
|
},
|
|
2694
2740
|
validator(cfg) {
|
|
2695
2741
|
if (cfg.validate && !Array.isArray(cfg.validate)) {
|
|
@@ -2751,13 +2797,23 @@ const controllers = {
|
|
|
2751
2797
|
}
|
|
2752
2798
|
}),
|
|
2753
2799
|
formsAdmin: ({ strapi }) => createFormsAdminController(strapi),
|
|
2754
|
-
company: () =>
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2800
|
+
company: ({ strapi }) => {
|
|
2801
|
+
const limits = strapi.plugin("hubspot").config("forms", {});
|
|
2802
|
+
const limiter = createLimiter({
|
|
2803
|
+
windowMs: 6e4,
|
|
2804
|
+
max: limits.rateLimit?.searchPerMinute ?? 30
|
|
2805
|
+
});
|
|
2806
|
+
return {
|
|
2807
|
+
/** SIRENE autocomplete for the company field — bounded, cached upstream. */
|
|
2808
|
+
async search(ctx) {
|
|
2809
|
+
if (!limiter.allow(ctx.request.ip ?? "unknown")) {
|
|
2810
|
+
ctx.throw(429, "Too many searches — slow down.");
|
|
2811
|
+
}
|
|
2812
|
+
const q = typeof ctx.query.q === "string" ? ctx.query.q : "";
|
|
2813
|
+
ctx.body = { companies: await searchCompanies(q) };
|
|
2814
|
+
}
|
|
2815
|
+
};
|
|
2816
|
+
},
|
|
2761
2817
|
submissions: ({ strapi }) => ({
|
|
2762
2818
|
/** Paged submissions, newest first, optionally narrowed to one form. */
|
|
2763
2819
|
async list(ctx) {
|
|
@@ -2801,41 +2857,51 @@ const controllers = {
|
|
|
2801
2857
|
};
|
|
2802
2858
|
}
|
|
2803
2859
|
}),
|
|
2804
|
-
forms: ({ strapi }) =>
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
}
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
if (
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
ctx.
|
|
2833
|
-
|
|
2834
|
-
|
|
2860
|
+
forms: ({ strapi }) => {
|
|
2861
|
+
const limits = strapi.plugin("hubspot").config("forms", {});
|
|
2862
|
+
const submitLimiter = createLimiter({
|
|
2863
|
+
windowMs: 6e4,
|
|
2864
|
+
max: limits.rateLimit?.submitPerMinute ?? 6
|
|
2865
|
+
});
|
|
2866
|
+
return {
|
|
2867
|
+
/** Published form for the host frontend — CRM mapping stripped. */
|
|
2868
|
+
async findOne(ctx) {
|
|
2869
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
2870
|
+
filters: { slug: ctx.params.slug },
|
|
2871
|
+
status: "published",
|
|
2872
|
+
locale: ctx.query.locale
|
|
2873
|
+
});
|
|
2874
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
2875
|
+
ctx.body = publicForm(entry);
|
|
2876
|
+
},
|
|
2877
|
+
async submit(ctx) {
|
|
2878
|
+
const entry = await strapi.documents(FORM_UID).findFirst({
|
|
2879
|
+
filters: { slug: ctx.params.slug },
|
|
2880
|
+
status: "published",
|
|
2881
|
+
locale: ctx.query.locale
|
|
2882
|
+
});
|
|
2883
|
+
if (!entry?.definition) ctx.throw(404, "Form not found");
|
|
2884
|
+
if (!submitLimiter.allow(ctx.request.ip ?? "unknown")) {
|
|
2885
|
+
ctx.throw(429, "Too many submissions — try again in a minute.");
|
|
2886
|
+
}
|
|
2887
|
+
const values = sanitizeRawValues(ctx.request.body?.values);
|
|
2888
|
+
if (!values || !Object.keys(values).length) ctx.throw(422, "Invalid submission");
|
|
2889
|
+
const rawMeta = ctx.request.body?.meta ?? {};
|
|
2890
|
+
const meta = {};
|
|
2891
|
+
for (const key of ["pagePath", "pageUrl", "originPath", "originLabel", "source"]) {
|
|
2892
|
+
const v = rawMeta[key];
|
|
2893
|
+
if (typeof v === "string" && v) meta[key] = v.slice(0, 500);
|
|
2894
|
+
}
|
|
2895
|
+
const outcome = await strapi.plugin("hubspot").service("forms").submit(entry, values, meta);
|
|
2896
|
+
if (!outcome.ok) {
|
|
2897
|
+
ctx.status = 422;
|
|
2898
|
+
ctx.body = { ok: false, missingRequired: outcome.missingRequired ?? [] };
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
ctx.body = { ok: true, hubspotSynced: outcome.hubspotSynced };
|
|
2835
2902
|
}
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
}),
|
|
2903
|
+
};
|
|
2904
|
+
},
|
|
2839
2905
|
settings: ({ strapi }) => ({
|
|
2840
2906
|
async get(ctx) {
|
|
2841
2907
|
ctx.body = await publicSettings(strapi);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -70,6 +70,11 @@ export declare function pickSiret(payload: SearchPayload, siret: string): Compan
|
|
|
70
70
|
* `companyMap`. Only complete mappings with an actual value produce a write.
|
|
71
71
|
*/
|
|
72
72
|
export declare function companyProperties(map: CompanyMap, hit: CompanyHit): Record<string, Record<string, Primitive>>;
|
|
73
|
+
/**
|
|
74
|
+
* A pasted identifier survives its formatting: "798 841 284 00010" queries as
|
|
75
|
+
* a SIRET, "798.841.284" as a SIREN. Anything else is a plain text query.
|
|
76
|
+
*/
|
|
77
|
+
export declare function normalizeQuery(q: string): string;
|
|
73
78
|
/** Autocomplete search. Bounded, cached; empty on any failure. */
|
|
74
79
|
export declare function searchCompanies(q: string): Promise<CompanyHit[]>;
|
|
75
80
|
/** Authoritative re-resolution of a selected SIRET; null when unreachable. */
|
|
@@ -10,6 +10,10 @@ declare const _default: {
|
|
|
10
10
|
forms: {
|
|
11
11
|
companyFromDomain: boolean;
|
|
12
12
|
timelineNote: boolean;
|
|
13
|
+
rateLimit: {
|
|
14
|
+
submitPerMinute: number;
|
|
15
|
+
searchPerMinute: number;
|
|
16
|
+
};
|
|
13
17
|
};
|
|
14
18
|
};
|
|
15
19
|
validator(cfg: {
|
|
@@ -27,7 +31,6 @@ declare const _default: {
|
|
|
27
31
|
displayName: string;
|
|
28
32
|
description: string;
|
|
29
33
|
};
|
|
30
|
-
/** RBAC action gating the settings screen and its routes. */
|
|
31
34
|
options: {
|
|
32
35
|
draftAndPublish: boolean;
|
|
33
36
|
};
|
|
@@ -36,7 +39,7 @@ declare const _default: {
|
|
|
36
39
|
visible: boolean;
|
|
37
40
|
};
|
|
38
41
|
"content-type-builder": {
|
|
39
|
-
visible: boolean;
|
|
42
|
+
visible: boolean; /** RBAC action gating the form builder and its routes. */
|
|
40
43
|
};
|
|
41
44
|
i18n: {
|
|
42
45
|
localized: boolean;
|
|
@@ -265,13 +268,19 @@ declare const _default: {
|
|
|
265
268
|
duplicate(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
266
269
|
remove(ctx: import("./formsAdmin").Ctx): Promise<void>;
|
|
267
270
|
};
|
|
268
|
-
company: (
|
|
271
|
+
company: ({ strapi }: {
|
|
272
|
+
strapi: Core.Strapi;
|
|
273
|
+
}) => {
|
|
269
274
|
/** SIRENE autocomplete for the company field — bounded, cached upstream. */
|
|
270
275
|
search(ctx: {
|
|
271
276
|
query: {
|
|
272
277
|
q?: string;
|
|
273
278
|
};
|
|
279
|
+
request: {
|
|
280
|
+
ip?: string;
|
|
281
|
+
};
|
|
274
282
|
body: unknown;
|
|
283
|
+
throw: (s: number, m: string) => never;
|
|
275
284
|
}): Promise<void>;
|
|
276
285
|
};
|
|
277
286
|
submissions: ({ strapi }: {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory sliding-window rate limiter for the public routes. Per-process on
|
|
3
|
+
* purpose: a form submission endpoint needs a cheap brake against a single
|
|
4
|
+
* abusive IP, not distributed accounting — and a multi-instance deployment
|
|
5
|
+
* simply multiplies the budget by the instance count, which is fine.
|
|
6
|
+
*/
|
|
7
|
+
export interface Limiter {
|
|
8
|
+
/** True when the call fits the window; false = refuse (429). */
|
|
9
|
+
allow(key: string): boolean;
|
|
10
|
+
/** Number of tracked keys — a test hook for the sweep. */
|
|
11
|
+
size(): number;
|
|
12
|
+
}
|
|
13
|
+
export declare function createLimiter({ windowMs, max, now, }: {
|
|
14
|
+
windowMs: number;
|
|
15
|
+
/** 0 disables the limiter entirely. */
|
|
16
|
+
max: number;
|
|
17
|
+
now?: () => number;
|
|
18
|
+
}): Limiter;
|