added /dist
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Configures @payloadcms/plugin-form-builder from IPAL's FormsOption.
|
||||
*
|
||||
* Provides the form/form-submissions collections and field types. Email
|
||||
* delivery is intentionally NOT handled here — the plugin's own SMTP-from-panel
|
||||
* sender (submitForm) does that, so the form-builder's built-in email (which
|
||||
* needs a Payload email adapter) is left unused.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=formsPluginConfig.d.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["formsPluginConfig.d.ts"],"sourcesContent":["import type { Plugin } from 'payload';\nimport type { FormsOption } from './types.js';\n/**\n * Configures @payloadcms/plugin-form-builder from IPAL's FormsOption.\n *\n * Provides the form/form-submissions collections and field types. Email\n * delivery is intentionally NOT handled here — the plugin's own SMTP-from-panel\n * sender (submitForm) does that, so the form-builder's built-in email (which\n * needs a Payload email adapter) is left unused.\n */\nexport declare function buildFormsPlugin(forms: FormsOption): Plugin;\n"],"names":[],"mappings":"AAEA;;;;;;;CAOC,GACD,WAAqE"}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import type { Plugin } from 'payload';
|
||||
import type { FormsOption } from './types.js';
|
||||
/**
|
||||
* Configures @payloadcms/plugin-form-builder from IPAL's FormsOption.
|
||||
*
|
||||
* Provides the form/form-submissions collections and field types. Email
|
||||
* delivery is intentionally NOT handled here — the plugin's own SMTP-from-panel
|
||||
* sender (submitForm) does that, so the form-builder's built-in email (which
|
||||
* needs a Payload email adapter) is left unused.
|
||||
*/
|
||||
export declare function buildFormsPlugin(forms: FormsOption): Plugin;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder';
|
||||
/**
|
||||
* Configures @payloadcms/plugin-form-builder from IPAL's FormsOption.
|
||||
*
|
||||
* Provides the form/form-submissions collections and field types. Email
|
||||
* delivery is intentionally NOT handled here — the plugin's own SMTP-from-panel
|
||||
* sender (submitForm) does that, so the form-builder's built-in email (which
|
||||
* needs a Payload email adapter) is left unused.
|
||||
*/ export function buildFormsPlugin(forms) {
|
||||
return formBuilderPlugin({
|
||||
fields: {
|
||||
checkbox: true,
|
||||
email: true,
|
||||
message: true,
|
||||
number: true,
|
||||
payment: false,
|
||||
select: true,
|
||||
text: true,
|
||||
textarea: true,
|
||||
...forms.fields
|
||||
},
|
||||
...forms.redirectRelationships ? {
|
||||
redirectRelationships: forms.redirectRelationships
|
||||
} : {},
|
||||
// Client-supplied collection overrides (e.g. a per-form notification
|
||||
// address). The plugin provides the hook, not the opinion about which
|
||||
// extra fields a form should carry.
|
||||
...forms.formOverrides ? {
|
||||
formOverrides: forms.formOverrides
|
||||
} : {},
|
||||
...forms.formSubmissionOverrides ? {
|
||||
formSubmissionOverrides: forms.formSubmissionOverrides
|
||||
} : {}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=formsPluginConfig.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/forms/formsPluginConfig.ts"],"sourcesContent":["import type { Plugin } from 'payload'\n\nimport { formBuilderPlugin } from '@payloadcms/plugin-form-builder'\n\nimport type { FormsOption } from './types.js'\n\n/**\n * Configures @payloadcms/plugin-form-builder from IPAL's FormsOption.\n *\n * Provides the form/form-submissions collections and field types. Email\n * delivery is intentionally NOT handled here — the plugin's own SMTP-from-panel\n * sender (submitForm) does that, so the form-builder's built-in email (which\n * needs a Payload email adapter) is left unused.\n */\nexport function buildFormsPlugin(forms: FormsOption): Plugin {\n return formBuilderPlugin({\n fields: {\n checkbox: true,\n email: true,\n message: true,\n number: true,\n payment: false,\n select: true,\n text: true,\n textarea: true,\n ...forms.fields,\n },\n ...(forms.redirectRelationships ? { redirectRelationships: forms.redirectRelationships } : {}),\n // Client-supplied collection overrides (e.g. a per-form notification\n // address). The plugin provides the hook, not the opinion about which\n // extra fields a form should carry.\n ...(forms.formOverrides ? { formOverrides: forms.formOverrides } : {}),\n ...(forms.formSubmissionOverrides\n ? { formSubmissionOverrides: forms.formSubmissionOverrides }\n : {}),\n })\n}\n"],"names":["formBuilderPlugin","buildFormsPlugin","forms","fields","checkbox","email","message","number","payment","select","text","textarea","redirectRelationships","formOverrides","formSubmissionOverrides"],"mappings":"AAEA,SAASA,iBAAiB,QAAQ,kCAAiC;AAInE;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAkB;IACjD,OAAOF,kBAAkB;QACvBG,QAAQ;YACNC,UAAU;YACVC,OAAO;YACPC,SAAS;YACTC,QAAQ;YACRC,SAAS;YACTC,QAAQ;YACRC,MAAM;YACNC,UAAU;YACV,GAAGT,MAAMC,MAAM;QACjB;QACA,GAAID,MAAMU,qBAAqB,GAAG;YAAEA,uBAAuBV,MAAMU,qBAAqB;QAAC,IAAI,CAAC,CAAC;QAC7F,qEAAqE;QACrE,sEAAsE;QACtE,oCAAoC;QACpC,GAAIV,MAAMW,aAAa,GAAG;YAAEA,eAAeX,MAAMW,aAAa;QAAC,IAAI,CAAC,CAAC;QACrE,GAAIX,MAAMY,uBAAuB,GAC7B;YAAEA,yBAAyBZ,MAAMY,uBAAuB;QAAC,IACzD,CAAC,CAAC;IACR;AACF"}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
export { buildFormsPlugin } from './formsPluginConfig.js';
|
||||
export { checkRateLimit } from './rateLimit.js';
|
||||
export { submitForm } from './submitForm.js';
|
||||
export { validateSubmission } from './validateSubmission.js';
|
||||
|
||||
//# sourceMappingURL=index.d.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["index.d.ts"],"sourcesContent":["export { buildFormsPlugin } from './formsPluginConfig.js';\nexport { checkRateLimit } from './rateLimit.js';\nexport type { RateLimitArgs } from './rateLimit.js';\nexport { submitForm } from './submitForm.js';\nexport type { SubmitFormArgs, SubmitFormResult } from './submitForm.js';\nexport type { FormsCollectionOverrides, FormsFieldsOverride, FormsOption } from './types.js';\nexport { validateSubmission } from './validateSubmission.js';\nexport type { FormValidationResult } from './validateSubmission.js';\n"],"names":["buildFormsPlugin","checkRateLimit","submitForm","validateSubmission"],"mappings":"AAAA,SAASA,gBAAgB,QAAQ,yBAAyB;AAC1D,SAASC,cAAc,QAAQ,iBAAiB;AAEhD,SAASC,UAAU,QAAQ,kBAAkB;AAG7C,SAASC,kBAAkB,QAAQ,0BAA0B"}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
export { buildFormsPlugin } from './formsPluginConfig.js';
|
||||
export { checkRateLimit } from './rateLimit.js';
|
||||
export type { RateLimitArgs } from './rateLimit.js';
|
||||
export { submitForm } from './submitForm.js';
|
||||
export type { SubmitFormArgs, SubmitFormResult } from './submitForm.js';
|
||||
export type { FormsCollectionOverrides, FormsFieldsOverride, FormsOption } from './types.js';
|
||||
export { validateSubmission } from './validateSubmission.js';
|
||||
export type { FormValidationResult } from './validateSubmission.js';
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
// buildFormsPlugin is config-time (safe anywhere). submitForm is server-only
|
||||
// (Turnstile secret) — never import it from a client component.
|
||||
export { buildFormsPlugin } from './formsPluginConfig.js';
|
||||
export { checkRateLimit } from './rateLimit.js';
|
||||
export { submitForm } from './submitForm.js';
|
||||
export { validateSubmission } from './validateSubmission.js';
|
||||
|
||||
//# sourceMappingURL=index.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/forms/index.ts"],"sourcesContent":["// buildFormsPlugin is config-time (safe anywhere). submitForm is server-only\n// (Turnstile secret) — never import it from a client component.\nexport { buildFormsPlugin } from './formsPluginConfig.js'\nexport { checkRateLimit } from './rateLimit.js'\nexport type { RateLimitArgs } from './rateLimit.js'\nexport { submitForm } from './submitForm.js'\nexport type { SubmitFormArgs, SubmitFormResult } from './submitForm.js'\nexport type { FormsCollectionOverrides, FormsFieldsOverride, FormsOption } from './types.js'\nexport { validateSubmission } from './validateSubmission.js'\nexport type { FormValidationResult } from './validateSubmission.js'\n"],"names":["buildFormsPlugin","checkRateLimit","submitForm","validateSubmission"],"mappings":"AAAA,6EAA6E;AAC7E,gEAAgE;AAChE,SAASA,gBAAgB,QAAQ,yBAAwB;AACzD,SAASC,cAAc,QAAQ,iBAAgB;AAE/C,SAASC,UAAU,QAAQ,kBAAiB;AAG5C,SAASC,kBAAkB,QAAQ,0BAAyB"}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Returns true when the request is within the limit, false when it should be
|
||||
* rejected. A missing key (no IP) is allowed through — better to accept a
|
||||
* submission than to block everyone behind a proxy that strips the header.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=rateLimit.d.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["rateLimit.d.ts"],"sourcesContent":["export type RateLimitArgs = {\n /** Identifier to limit on — typically the client IP. */\n key: string;\n /** Max submissions allowed per window. Defaults to 5. */\n max?: number;\n /** Window length in ms. Defaults to 60_000 (one minute). */\n windowMs?: number;\n};\n/**\n * Returns true when the request is within the limit, false when it should be\n * rejected. A missing key (no IP) is allowed through — better to accept a\n * submission than to block everyone behind a proxy that strips the header.\n */\nexport declare function checkRateLimit({ key, max, windowMs }: RateLimitArgs): boolean;\n"],"names":[],"mappings":"AAQA;;;;CAIC,GACD,WAAuF"}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
export type RateLimitArgs = {
|
||||
/** Identifier to limit on — typically the client IP. */
|
||||
key: string;
|
||||
/** Max submissions allowed per window. Defaults to 5. */
|
||||
max?: number;
|
||||
/** Window length in ms. Defaults to 60_000 (one minute). */
|
||||
windowMs?: number;
|
||||
};
|
||||
/**
|
||||
* Returns true when the request is within the limit, false when it should be
|
||||
* rejected. A missing key (no IP) is allowed through — better to accept a
|
||||
* submission than to block everyone behind a proxy that strips the header.
|
||||
*/
|
||||
export declare function checkRateLimit({ key, max, windowMs }: RateLimitArgs): boolean;
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Per-IP sliding window, in memory.
|
||||
*
|
||||
* Deliberately simple: no Redis, no dependency. The trade-off is that the
|
||||
* counter lives in one process — with several instances behind a load balancer
|
||||
* each keeps its own, so the effective limit is per-instance, not global. For a
|
||||
* contact form that's fine (it raises the cost of flooding without pretending
|
||||
* to be airtight); a high-security form should put a real limiter in front.
|
||||
*
|
||||
* State is module-level, so it survives between requests but resets on redeploy
|
||||
* — acceptable for abuse throttling.
|
||||
*/ const buckets = new Map();
|
||||
/** Sweep expired buckets occasionally so the map doesn't grow unbounded. */ let lastSweep = Date.now();
|
||||
const SWEEP_INTERVAL = 60_000;
|
||||
function sweep(now) {
|
||||
if (now - lastSweep < SWEEP_INTERVAL) {
|
||||
return;
|
||||
}
|
||||
lastSweep = now;
|
||||
for (const [key, bucket] of buckets){
|
||||
if (bucket.resetAt <= now) {
|
||||
buckets.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns true when the request is within the limit, false when it should be
|
||||
* rejected. A missing key (no IP) is allowed through — better to accept a
|
||||
* submission than to block everyone behind a proxy that strips the header.
|
||||
*/ export function checkRateLimit({ key, max = 5, windowMs = 60_000 }) {
|
||||
if (!key) {
|
||||
return true;
|
||||
}
|
||||
const now = Date.now();
|
||||
sweep(now);
|
||||
const bucket = buckets.get(key);
|
||||
if (!bucket || bucket.resetAt <= now) {
|
||||
buckets.set(key, {
|
||||
count: 1,
|
||||
resetAt: now + windowMs
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (bucket.count >= max) {
|
||||
return false;
|
||||
}
|
||||
bucket.count += 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
//# sourceMappingURL=rateLimit.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/forms/rateLimit.ts"],"sourcesContent":["type Bucket = { count: number; resetAt: number }\n\n/**\n * Per-IP sliding window, in memory.\n *\n * Deliberately simple: no Redis, no dependency. The trade-off is that the\n * counter lives in one process — with several instances behind a load balancer\n * each keeps its own, so the effective limit is per-instance, not global. For a\n * contact form that's fine (it raises the cost of flooding without pretending\n * to be airtight); a high-security form should put a real limiter in front.\n *\n * State is module-level, so it survives between requests but resets on redeploy\n * — acceptable for abuse throttling.\n */\nconst buckets = new Map<string, Bucket>()\n\n/** Sweep expired buckets occasionally so the map doesn't grow unbounded. */\nlet lastSweep = Date.now()\nconst SWEEP_INTERVAL = 60_000\n\nfunction sweep(now: number) {\n if (now - lastSweep < SWEEP_INTERVAL) {return}\n lastSweep = now\n for (const [key, bucket] of buckets) {\n if (bucket.resetAt <= now) {buckets.delete(key)}\n }\n}\n\nexport type RateLimitArgs = {\n /** Identifier to limit on — typically the client IP. */\n key: string\n /** Max submissions allowed per window. Defaults to 5. */\n max?: number\n /** Window length in ms. Defaults to 60_000 (one minute). */\n windowMs?: number\n}\n\n/**\n * Returns true when the request is within the limit, false when it should be\n * rejected. A missing key (no IP) is allowed through — better to accept a\n * submission than to block everyone behind a proxy that strips the header.\n */\nexport function checkRateLimit({ key, max = 5, windowMs = 60_000 }: RateLimitArgs): boolean {\n if (!key) {return true}\n\n const now = Date.now()\n sweep(now)\n\n const bucket = buckets.get(key)\n\n if (!bucket || bucket.resetAt <= now) {\n buckets.set(key, { count: 1, resetAt: now + windowMs })\n return true\n }\n\n if (bucket.count >= max) {\n return false\n }\n\n bucket.count += 1\n return true\n}\n"],"names":["buckets","Map","lastSweep","Date","now","SWEEP_INTERVAL","sweep","key","bucket","resetAt","delete","checkRateLimit","max","windowMs","get","set","count"],"mappings":"AAEA;;;;;;;;;;;CAWC,GACD,MAAMA,UAAU,IAAIC;AAEpB,0EAA0E,GAC1E,IAAIC,YAAYC,KAAKC,GAAG;AACxB,MAAMC,iBAAiB;AAEvB,SAASC,MAAMF,GAAW;IACxB,IAAIA,MAAMF,YAAYG,gBAAgB;QAAC;IAAM;IAC7CH,YAAYE;IACZ,KAAK,MAAM,CAACG,KAAKC,OAAO,IAAIR,QAAS;QACnC,IAAIQ,OAAOC,OAAO,IAAIL,KAAK;YAACJ,QAAQU,MAAM,CAACH;QAAI;IACjD;AACF;AAWA;;;;CAIC,GACD,OAAO,SAASI,eAAe,EAAEJ,GAAG,EAAEK,MAAM,CAAC,EAAEC,WAAW,MAAM,EAAiB;IAC/E,IAAI,CAACN,KAAK;QAAC,OAAO;IAAI;IAEtB,MAAMH,MAAMD,KAAKC,GAAG;IACpBE,MAAMF;IAEN,MAAMI,SAASR,QAAQc,GAAG,CAACP;IAE3B,IAAI,CAACC,UAAUA,OAAOC,OAAO,IAAIL,KAAK;QACpCJ,QAAQe,GAAG,CAACR,KAAK;YAAES,OAAO;YAAGP,SAASL,MAAMS;QAAS;QACrD,OAAO;IACT;IAEA,IAAIL,OAAOQ,KAAK,IAAIJ,KAAK;QACvB,OAAO;IACT;IAEAJ,OAAOQ,KAAK,IAAI;IAChB,OAAO;AACT"}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import 'server-only';
|
||||
|
||||
//# sourceMappingURL=submitForm.d.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["submitForm.d.ts"],"sourcesContent":["import 'server-only';\nimport type { BasePayload } from 'payload';\nexport type SubmitFormArgs = {\n /** Submitted field data — shape matches the form's fields. */\n data: Record<string, unknown>;\n /** Form-builder form ID this submission belongs to. */\n formId: string;\n /** Client IP — used for Turnstile and rate limiting. */\n ip?: string;\n /**\n * Rate limit: max submissions per IP per minute. Defaults to 5.\n * Set to 0 to disable (e.g. when a real limiter sits in front).\n */\n maxPerMinute?: number;\n payload: BasePayload;\n /** Turnstile token; when present it is verified, when absent it is skipped. */\n turnstileToken?: string;\n};\n/**\n * Why a submission failed — a code, never a user-facing string.\n *\n * The plugin knows what went wrong; it deliberately doesn't decide how to say\n * it. The frontend maps these to its own copy, in its own language, and renders\n * whatever component fits — a field error, a toast, a full message. The plugin\n * has no business choosing the wording or the locale.\n *\n * - `rate_limited` — too many submissions from this IP\n * - `turnstile` — bot check failed\n * - `validation` — a field is missing/too long, or unknown keys were sent;\n * `field` and `kind` narrow it down when a specific field is\n * at fault (absent for whole-payload problems like unknown keys)\n * - `not_found` — no form with this id\n * - `error` — persistence failed unexpectedly\n */\nexport type SubmitFailure = {\n /** The offending field's name, when one field is at fault. */\n field?: string;\n /** What was wrong with it. */\n kind?: 'required' | 'too_long' | 'unknown_fields';\n reason: 'validation';\n success: false;\n} | {\n reason: 'error';\n success: false;\n} | {\n reason: 'not_found';\n success: false;\n} | {\n reason: 'rate_limited';\n success: false;\n} | {\n reason: 'turnstile';\n success: false;\n};\nexport type SubmitFormResult = {\n submissionId: number | string;\n success: true;\n} | SubmitFailure;\n/**\n * Handles a form submission end to end: rate limit, verify Turnstile, validate\n * against the form's own schema, then store.\n *\n * The order is cost-ascending on purpose — the cheapest checks reject first, so\n * a flood never reaches Turnstile's network call or the database.\n *\n * Emails aren't sent here. The form-builder sends whatever an editor configured\n * under the form's \"Emails\" tab (form-submissions hook → payload.sendEmail),\n * which goes out over panelSmtpAdapter. Storing the submission is enough.\n *\n * server-only: touches the Turnstile secret.\n */\nexport declare function submitForm({ data, formId, ip, maxPerMinute, payload, turnstileToken, }: SubmitFormArgs): Promise<SubmitFormResult>;\n"],"names":[],"mappings":"AAAA,OAAO,cAAc"}
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
import 'server-only';
|
||||
import type { BasePayload } from 'payload';
|
||||
export type SubmitFormArgs = {
|
||||
/** Submitted field data — shape matches the form's fields. */
|
||||
data: Record<string, unknown>;
|
||||
/** Form-builder form ID this submission belongs to. */
|
||||
formId: string;
|
||||
/** Client IP — used for Turnstile and rate limiting. */
|
||||
ip?: string;
|
||||
/**
|
||||
* Rate limit: max submissions per IP per minute. Defaults to 5.
|
||||
* Set to 0 to disable (e.g. when a real limiter sits in front).
|
||||
*/
|
||||
maxPerMinute?: number;
|
||||
payload: BasePayload;
|
||||
/** Turnstile token; when present it is verified, when absent it is skipped. */
|
||||
turnstileToken?: string;
|
||||
};
|
||||
/**
|
||||
* Why a submission failed — a code, never a user-facing string.
|
||||
*
|
||||
* The plugin knows what went wrong; it deliberately doesn't decide how to say
|
||||
* it. The frontend maps these to its own copy, in its own language, and renders
|
||||
* whatever component fits — a field error, a toast, a full message. The plugin
|
||||
* has no business choosing the wording or the locale.
|
||||
*
|
||||
* - `rate_limited` — too many submissions from this IP
|
||||
* - `turnstile` — bot check failed
|
||||
* - `validation` — a field is missing/too long, or unknown keys were sent;
|
||||
* `field` and `kind` narrow it down when a specific field is
|
||||
* at fault (absent for whole-payload problems like unknown keys)
|
||||
* - `not_found` — no form with this id
|
||||
* - `error` — persistence failed unexpectedly
|
||||
*/
|
||||
export type SubmitFailure = {
|
||||
/** The offending field's name, when one field is at fault. */
|
||||
field?: string;
|
||||
/** What was wrong with it. */
|
||||
kind?: 'required' | 'too_long' | 'unknown_fields';
|
||||
reason: 'validation';
|
||||
success: false;
|
||||
} | {
|
||||
reason: 'error';
|
||||
success: false;
|
||||
} | {
|
||||
reason: 'not_found';
|
||||
success: false;
|
||||
} | {
|
||||
reason: 'rate_limited';
|
||||
success: false;
|
||||
} | {
|
||||
reason: 'turnstile';
|
||||
success: false;
|
||||
};
|
||||
export type SubmitFormResult = {
|
||||
submissionId: number | string;
|
||||
success: true;
|
||||
} | SubmitFailure;
|
||||
/**
|
||||
* Handles a form submission end to end: rate limit, verify Turnstile, validate
|
||||
* against the form's own schema, then store.
|
||||
*
|
||||
* The order is cost-ascending on purpose — the cheapest checks reject first, so
|
||||
* a flood never reaches Turnstile's network call or the database.
|
||||
*
|
||||
* Emails aren't sent here. The form-builder sends whatever an editor configured
|
||||
* under the form's "Emails" tab (form-submissions hook → payload.sendEmail),
|
||||
* which goes out over panelSmtpAdapter. Storing the submission is enough.
|
||||
*
|
||||
* server-only: touches the Turnstile secret.
|
||||
*/
|
||||
export declare function submitForm({ data, formId, ip, maxPerMinute, payload, turnstileToken, }: SubmitFormArgs): Promise<SubmitFormResult>;
|
||||
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
import 'server-only';
|
||||
import { verifyTurnstile } from '../turnstile/index.js';
|
||||
import { checkRateLimit } from './rateLimit.js';
|
||||
import { validateSubmission } from './validateSubmission.js';
|
||||
/**
|
||||
* Handles a form submission end to end: rate limit, verify Turnstile, validate
|
||||
* against the form's own schema, then store.
|
||||
*
|
||||
* The order is cost-ascending on purpose — the cheapest checks reject first, so
|
||||
* a flood never reaches Turnstile's network call or the database.
|
||||
*
|
||||
* Emails aren't sent here. The form-builder sends whatever an editor configured
|
||||
* under the form's "Emails" tab (form-submissions hook → payload.sendEmail),
|
||||
* which goes out over panelSmtpAdapter. Storing the submission is enough.
|
||||
*
|
||||
* server-only: touches the Turnstile secret.
|
||||
*/ export async function submitForm({ data, formId, ip, maxPerMinute = 5, payload, turnstileToken }) {
|
||||
// 1. Rate limit — cheapest gate, drops a flood before any real work.
|
||||
if (maxPerMinute > 0 && ip) {
|
||||
if (!checkRateLimit({
|
||||
key: ip,
|
||||
max: maxPerMinute
|
||||
})) {
|
||||
return {
|
||||
reason: 'rate_limited',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// 2. Turnstile — verify when a token is supplied; reject on failure.
|
||||
if (turnstileToken !== undefined) {
|
||||
const ok = await verifyTurnstile({
|
||||
ip,
|
||||
payload,
|
||||
token: turnstileToken
|
||||
});
|
||||
if (!ok) {
|
||||
return {
|
||||
reason: 'turnstile',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
// 3. Validate against the form's schema. A public endpoint can't trust the
|
||||
// shape of `data` — drop unknown keys, enforce required, cap length.
|
||||
const validation = await validateSubmission(payload, formId, data);
|
||||
if (!validation.ok) {
|
||||
if (validation.reason === 'not_found') {
|
||||
return {
|
||||
reason: 'not_found',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
return {
|
||||
reason: 'validation',
|
||||
success: false,
|
||||
...validation.field ? {
|
||||
field: validation.field
|
||||
} : {},
|
||||
...validation.kind ? {
|
||||
kind: validation.kind
|
||||
} : {}
|
||||
};
|
||||
}
|
||||
// 4. Persist (form-builder shape: submissionData array). Triggers the email
|
||||
// hook. Only validated, known fields are stored.
|
||||
try {
|
||||
const submission = await payload.create({
|
||||
collection: 'form-submissions',
|
||||
data: {
|
||||
form: formId,
|
||||
submissionData: Object.entries(validation.cleaned).map(([field, value])=>({
|
||||
field,
|
||||
value: value == null ? '' : String(value)
|
||||
}))
|
||||
}
|
||||
});
|
||||
return {
|
||||
submissionId: submission.id,
|
||||
success: true
|
||||
};
|
||||
} catch (err) {
|
||||
payload.logger.error(`[ipal] Form submission failed: ${err.message}`);
|
||||
return {
|
||||
reason: 'error',
|
||||
success: false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
//# sourceMappingURL=submitForm.js.map
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Forms configuration — mirrors the fields a client enables in the
|
||||
* form-builder plugin. Kept minimal; the plugin passes these through.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=types.d.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["types.d.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload';\n/**\n * Receives the collection's default fields and returns the final list — add,\n * remove, or reorder. Same shape the form-builder uses.\n */\nexport type FormsFieldsOverride = (args: {\n defaultFields: Field[];\n}) => Field[];\n/**\n * Overrides for a forms-related collection: replace the fields and/or any\n * other collection setting (admin, access, hooks…).\n */\nexport type FormsCollectionOverrides = {\n fields?: FormsFieldsOverride;\n} & Partial<Omit<CollectionConfig, 'fields'>>;\n/**\n * Forms configuration — mirrors the fields a client enables in the\n * form-builder plugin. Kept minimal; the plugin passes these through.\n */\nexport type FormsOption = {\n /** Field types available in the form builder. Sensible defaults applied. */\n fields?: {\n checkbox?: boolean;\n email?: boolean;\n message?: boolean;\n number?: boolean;\n payment?: boolean;\n select?: boolean;\n text?: boolean;\n textarea?: boolean;\n };\n /**\n * Override the forms collection. The plugin stays opinion-free about what a\n * form needs beyond its fields — a client that wants, say, a per-form\n * notification address adds it here:\n *\n * formOverrides: {\n * fields: ({ defaultFields }) => [\n * ...defaultFields,\n * { name: 'notificationEmail', type: 'email' },\n * ],\n * }\n */\n formOverrides?: FormsCollectionOverrides;\n /** Override the form-submissions collection (same shape). */\n formSubmissionOverrides?: FormsCollectionOverrides;\n /** Collections a form can redirect to (e.g. ['pages']). */\n redirectRelationships?: string[];\n};\n"],"names":[],"mappings":"AAeA;;;CAGC,GACD,WA6BE"}
|
||||
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
import type { CollectionConfig, Field } from 'payload';
|
||||
/**
|
||||
* Receives the collection's default fields and returns the final list — add,
|
||||
* remove, or reorder. Same shape the form-builder uses.
|
||||
*/
|
||||
export type FormsFieldsOverride = (args: {
|
||||
defaultFields: Field[];
|
||||
}) => Field[];
|
||||
/**
|
||||
* Overrides for a forms-related collection: replace the fields and/or any
|
||||
* other collection setting (admin, access, hooks…).
|
||||
*/
|
||||
export type FormsCollectionOverrides = {
|
||||
fields?: FormsFieldsOverride;
|
||||
} & Partial<Omit<CollectionConfig, 'fields'>>;
|
||||
/**
|
||||
* Forms configuration — mirrors the fields a client enables in the
|
||||
* form-builder plugin. Kept minimal; the plugin passes these through.
|
||||
*/
|
||||
export type FormsOption = {
|
||||
/** Field types available in the form builder. Sensible defaults applied. */
|
||||
fields?: {
|
||||
checkbox?: boolean;
|
||||
email?: boolean;
|
||||
message?: boolean;
|
||||
number?: boolean;
|
||||
payment?: boolean;
|
||||
select?: boolean;
|
||||
text?: boolean;
|
||||
textarea?: boolean;
|
||||
};
|
||||
/**
|
||||
* Override the forms collection. The plugin stays opinion-free about what a
|
||||
* form needs beyond its fields — a client that wants, say, a per-form
|
||||
* notification address adds it here:
|
||||
*
|
||||
* formOverrides: {
|
||||
* fields: ({ defaultFields }) => [
|
||||
* ...defaultFields,
|
||||
* { name: 'notificationEmail', type: 'email' },
|
||||
* ],
|
||||
* }
|
||||
*/
|
||||
formOverrides?: FormsCollectionOverrides;
|
||||
/** Override the form-submissions collection (same shape). */
|
||||
formSubmissionOverrides?: FormsCollectionOverrides;
|
||||
/** Collections a form can redirect to (e.g. ['pages']). */
|
||||
redirectRelationships?: string[];
|
||||
};
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Forms configuration — mirrors the fields a client enables in the
|
||||
* form-builder plugin. Kept minimal; the plugin passes these through.
|
||||
*/ export { };
|
||||
|
||||
//# sourceMappingURL=types.js.map
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["../../../src/modules/forms/types.ts"],"sourcesContent":["import type { CollectionConfig, Field } from 'payload'\n\n/**\n * Receives the collection's default fields and returns the final list — add,\n * remove, or reorder. Same shape the form-builder uses.\n */\nexport type FormsFieldsOverride = (args: { defaultFields: Field[] }) => Field[]\n\n/**\n * Overrides for a forms-related collection: replace the fields and/or any\n * other collection setting (admin, access, hooks…).\n */\nexport type FormsCollectionOverrides = {\n fields?: FormsFieldsOverride\n} & Partial<Omit<CollectionConfig, 'fields'>>\n\n/**\n * Forms configuration — mirrors the fields a client enables in the\n * form-builder plugin. Kept minimal; the plugin passes these through.\n */\nexport type FormsOption = {\n /** Field types available in the form builder. Sensible defaults applied. */\n fields?: {\n checkbox?: boolean\n email?: boolean\n message?: boolean\n number?: boolean\n payment?: boolean\n select?: boolean\n text?: boolean\n textarea?: boolean\n }\n /**\n * Override the forms collection. The plugin stays opinion-free about what a\n * form needs beyond its fields — a client that wants, say, a per-form\n * notification address adds it here:\n *\n * formOverrides: {\n * fields: ({ defaultFields }) => [\n * ...defaultFields,\n * { name: 'notificationEmail', type: 'email' },\n * ],\n * }\n */\n formOverrides?: FormsCollectionOverrides\n /** Override the form-submissions collection (same shape). */\n formSubmissionOverrides?: FormsCollectionOverrides\n /** Collections a form can redirect to (e.g. ['pages']). */\n redirectRelationships?: string[]\n}\n"],"names":[],"mappings":"AAgBA;;;CAGC,GACD,WA6BC"}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export { };
|
||||
|
||||
//# sourceMappingURL=validateSubmission.d.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sources":["validateSubmission.d.ts"],"sourcesContent":["import type { BasePayload } from 'payload';\n/** A form-builder field, trimmed to what validation needs. */\ntype FormField = {\n blockType?: string;\n label?: string;\n name?: string;\n required?: boolean | null;\n};\ntype FormDoc = {\n fields?: FormField[];\n id: number | string;\n /** Per-form notification address, when the client added the field. */\n notificationEmail?: string;\n title?: string;\n};\n/**\n * Validation outcome — codes, not user-facing strings. The frontend turns these\n * into its own copy (see SubmitFailure in submitForm).\n */\nexport type FormValidationResult = {\n /** Offending field, when a single field is at fault. */\n field?: string;\n kind: 'required' | 'too_long' | 'unknown_fields';\n ok: false;\n reason: 'invalid';\n} | {\n cleaned: Record<string, unknown>;\n form: FormDoc;\n ok: true;\n} | {\n ok: false;\n reason: 'not_found';\n};\n/**\n * Checks submitted data against the form's own definition, rather than trusting\n * whatever arrived.\n *\n * The server action is a public endpoint: a caller can skip the rendered form\n * and post arbitrary keys. Without this, unknown fields would be stored,\n * required fields could be missing, and an oversized value could sail through.\n * So we load the form, keep only keys that are real fields, reject when a\n * required one is blank, and cap length.\n *\n * Returns the loaded form on success so the caller doesn't fetch it twice, and\n * a code + offending field on failure so the frontend can point at it.\n */\nexport declare function validateSubmission(payload: BasePayload, formId: string, data: Record<string, unknown>): Promise<FormValidationResult>;\nexport {};\n"],"names":[],"mappings":"AA+CA,WAAU"}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import type { BasePayload } from 'payload';
|
||||
/** A form-builder field, trimmed to what validation needs. */
|
||||
type FormField = {
|
||||
blockType?: string;
|
||||
label?: string;
|
||||
name?: string;
|
||||
required?: boolean | null;
|
||||
};
|
||||
type FormDoc = {
|
||||
fields?: FormField[];
|
||||
id: number | string;
|
||||
/** Per-form notification address, when the client added the field. */
|
||||
notificationEmail?: string;
|
||||
title?: string;
|
||||
};
|
||||
/**
|
||||
* Validation outcome — codes, not user-facing strings. The frontend turns these
|
||||
* into its own copy (see SubmitFailure in submitForm).
|
||||
*/
|
||||
export type FormValidationResult = {
|
||||
/** Offending field, when a single field is at fault. */
|
||||
field?: string;
|
||||
kind: 'required' | 'too_long' | 'unknown_fields';
|
||||
ok: false;
|
||||
reason: 'invalid';
|
||||
} | {
|
||||
cleaned: Record<string, unknown>;
|
||||
form: FormDoc;
|
||||
ok: true;
|
||||
} | {
|
||||
ok: false;
|
||||
reason: 'not_found';
|
||||
};
|
||||
/**
|
||||
* Checks submitted data against the form's own definition, rather than trusting
|
||||
* whatever arrived.
|
||||
*
|
||||
* The server action is a public endpoint: a caller can skip the rendered form
|
||||
* and post arbitrary keys. Without this, unknown fields would be stored,
|
||||
* required fields could be missing, and an oversized value could sail through.
|
||||
* So we load the form, keep only keys that are real fields, reject when a
|
||||
* required one is blank, and cap length.
|
||||
*
|
||||
* Returns the loaded form on success so the caller doesn't fetch it twice, and
|
||||
* a code + offending field on failure so the frontend can point at it.
|
||||
*/
|
||||
export declare function validateSubmission(payload: BasePayload, formId: string, data: Record<string, unknown>): Promise<FormValidationResult>;
|
||||
export {};
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/** Field block types that don't carry a submittable value. */ const NON_DATA_BLOCKS = new Set([
|
||||
'message'
|
||||
]);
|
||||
/** Hard ceiling on a single field's length, independent of the form config. */ const MAX_FIELD_LENGTH = 5000;
|
||||
/**
|
||||
* Checks submitted data against the form's own definition, rather than trusting
|
||||
* whatever arrived.
|
||||
*
|
||||
* The server action is a public endpoint: a caller can skip the rendered form
|
||||
* and post arbitrary keys. Without this, unknown fields would be stored,
|
||||
* required fields could be missing, and an oversized value could sail through.
|
||||
* So we load the form, keep only keys that are real fields, reject when a
|
||||
* required one is blank, and cap length.
|
||||
*
|
||||
* Returns the loaded form on success so the caller doesn't fetch it twice, and
|
||||
* a code + offending field on failure so the frontend can point at it.
|
||||
*/ export async function validateSubmission(payload, formId, data) {
|
||||
let form;
|
||||
try {
|
||||
form = await payload.findByID({
|
||||
id: formId,
|
||||
collection: 'forms',
|
||||
depth: 0
|
||||
});
|
||||
} catch {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'not_found'
|
||||
};
|
||||
}
|
||||
const fields = (form.fields ?? []).filter((f)=>typeof f.name === 'string' && !NON_DATA_BLOCKS.has(f.blockType ?? ''));
|
||||
const known = new Map(fields.map((f)=>[
|
||||
f.name,
|
||||
f
|
||||
]));
|
||||
const cleaned = {};
|
||||
for (const field of fields){
|
||||
const value = data[field.name];
|
||||
const isBlank = value == null || typeof value === 'string' && value.trim() === '' || value === false;
|
||||
if (field.required && isBlank) {
|
||||
return {
|
||||
field: field.name,
|
||||
kind: 'required',
|
||||
ok: false,
|
||||
reason: 'invalid'
|
||||
};
|
||||
}
|
||||
if (typeof value === 'string' && value.length > MAX_FIELD_LENGTH) {
|
||||
return {
|
||||
field: field.name,
|
||||
kind: 'too_long',
|
||||
ok: false,
|
||||
reason: 'invalid'
|
||||
};
|
||||
}
|
||||
// Only carry through keys that belong to the form — unknown keys from a
|
||||
// hand-crafted request are dropped, not stored.
|
||||
if (value !== undefined) {
|
||||
cleaned[field.name] = value;
|
||||
}
|
||||
}
|
||||
// Reject outright if the payload carried keys the form doesn't define — a
|
||||
// sign the request wasn't produced by the rendered form.
|
||||
const unknownKeys = Object.keys(data).filter((k)=>!known.has(k));
|
||||
if (unknownKeys.length > 0) {
|
||||
return {
|
||||
kind: 'unknown_fields',
|
||||
ok: false,
|
||||
reason: 'invalid'
|
||||
};
|
||||
}
|
||||
return {
|
||||
cleaned,
|
||||
form,
|
||||
ok: true
|
||||
};
|
||||
}
|
||||
|
||||
//# sourceMappingURL=validateSubmission.js.map
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user