@paroicms/contact-form-plugin 0.30.0 → 0.32.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.
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
const ConfigurationAT = type({
|
|
3
|
+
"googleRecaptchaSiteKey?": "string|undefined|null",
|
|
4
|
+
"googleRecaptchaSecretKey?": "string|undefined|null",
|
|
5
|
+
"+": "reject",
|
|
6
|
+
}).pipe((data) => ({
|
|
7
|
+
googleRecaptchaSiteKey: data.googleRecaptchaSiteKey ?? undefined,
|
|
8
|
+
googleRecaptchaSecretKey: data.googleRecaptchaSecretKey ?? undefined,
|
|
9
|
+
}));
|
|
10
|
+
export function getConfiguration(service) {
|
|
11
|
+
const configuration = ConfigurationAT.assert(service.configuration);
|
|
12
|
+
if (!!configuration.googleRecaptchaSecretKey !== !!configuration.googleRecaptchaSiteKey) {
|
|
13
|
+
throw new Error("[Contact Form] Invalid Google reCAPTCHA configuration");
|
|
14
|
+
}
|
|
15
|
+
if (!configuration.googleRecaptchaSiteKey) {
|
|
16
|
+
service.logger.warn("[Contact Form] Google reCAPTCHA keys are not set. The contact form will not be protected.");
|
|
17
|
+
}
|
|
18
|
+
return configuration;
|
|
19
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ensureString, messageOf } from "@paroicms/public-anywhere-lib";
|
|
2
2
|
import { escapeHtml, } from "@paroicms/public-server-lib";
|
|
3
|
-
|
|
3
|
+
import { validateRecaptchaResponse } from "./validate-recaptcha-response.js";
|
|
4
|
+
export async function sendContactFormMail(service, configuration, input, i18n) {
|
|
4
5
|
const { email, name, message, subject, gRecaptchaResponse } = input;
|
|
5
6
|
let contactEmail;
|
|
6
7
|
try {
|
|
@@ -10,7 +11,11 @@ export async function sendContactFormMail(service, input, i18n) {
|
|
|
10
11
|
}));
|
|
11
12
|
if (!contactEmail)
|
|
12
13
|
throw new Error(`['${service.fqdn}'] missing 'contactEmail'`);
|
|
13
|
-
if (
|
|
14
|
+
if (configuration.googleRecaptchaSecretKey &&
|
|
15
|
+
!(await validateRecaptchaResponse(service, {
|
|
16
|
+
gRecaptchaResponse,
|
|
17
|
+
secretKey: configuration.googleRecaptchaSecretKey,
|
|
18
|
+
}))) {
|
|
14
19
|
throw new Error("invalid recaptcha response");
|
|
15
20
|
}
|
|
16
21
|
const defaultSubject = i18n.translate({
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { escapeHtml, loadSimpleTranslatorFromDirectory, makeStylesheetLinkAsyncTag,
|
|
2
|
-
import {
|
|
3
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { escapeHtml, loadSimpleTranslatorFromDirectory, makeStylesheetLinkAsyncTag, } from "@paroicms/public-server-lib";
|
|
2
|
+
import { esmDirName, extractPackageNameAndVersionSync } from "@paroicms/script-lib";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { getConfiguration } from "./configuration.js";
|
|
5
5
|
import { sendContactFormMail } from "./contact-form-mail.js";
|
|
6
6
|
import { SendMailInputBody } from "./data-format.js";
|
|
7
|
-
const
|
|
8
|
-
const projectDir = resolveModuleDirectory(import.meta.url, { parent: true });
|
|
7
|
+
export const projectDir = dirname(esmDirName(import.meta.url));
|
|
9
8
|
const packageDir = dirname(projectDir);
|
|
10
|
-
const
|
|
9
|
+
const { version } = extractPackageNameAndVersionSync(packageDir);
|
|
11
10
|
const plugin = {
|
|
12
11
|
version,
|
|
13
|
-
slug: SLUG,
|
|
14
12
|
async siteInit(service) {
|
|
13
|
+
const configuration = getConfiguration(service);
|
|
15
14
|
const simpleI18n = await loadSimpleTranslatorFromDirectory({
|
|
16
15
|
l10nDir: join(projectDir, "locales"),
|
|
17
16
|
logger: service.logger,
|
|
@@ -21,7 +20,7 @@ const plugin = {
|
|
|
21
20
|
if (!state.has("paContactForm"))
|
|
22
21
|
return;
|
|
23
22
|
return [
|
|
24
|
-
`<script type="module" src="${service.pluginAssetsUrl}/contact-form-plugin.mjs" class="js-script
|
|
23
|
+
`<script type="module" src="${service.pluginAssetsUrl}/contact-form-plugin.mjs" class="js-script-contact-form" data-google-recaptcha-site-key="${escapeHtml(configuration.googleRecaptchaSiteKey ?? "")}" async></script>`,
|
|
25
24
|
makeStylesheetLinkAsyncTag(`${service.pluginAssetsUrl}/contact-form-plugin.css`),
|
|
26
25
|
];
|
|
27
26
|
});
|
|
@@ -43,7 +42,7 @@ const plugin = {
|
|
|
43
42
|
res.status(400).send({ status: 400, message: error.message });
|
|
44
43
|
return;
|
|
45
44
|
}
|
|
46
|
-
const result = await sendContactFormMail(service, input, simpleI18n);
|
|
45
|
+
const result = await sendContactFormMail(service, configuration, input, simpleI18n);
|
|
47
46
|
res.send(result);
|
|
48
47
|
});
|
|
49
48
|
},
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ApiError } from "@paroicms/public-server-lib";
|
|
2
|
+
export async function validateRecaptchaResponse(service, { gRecaptchaResponse, secretKey, }) {
|
|
3
|
+
if (!gRecaptchaResponse)
|
|
4
|
+
throw new ApiError("Missing gRecaptchaResponse", 400);
|
|
5
|
+
if (!secretKey) {
|
|
6
|
+
throw new Error("Invalid configuration, missing 'recaptchaPrivateKey'");
|
|
7
|
+
}
|
|
8
|
+
const url = `https://www.google.com/recaptcha/api/siteverify?secret=${secretKey}&response=${gRecaptchaResponse}`;
|
|
9
|
+
const response = await fetch(url, {
|
|
10
|
+
method: "post",
|
|
11
|
+
});
|
|
12
|
+
if (response.status < 200 || response.status >= 300) {
|
|
13
|
+
throw new Error("Failed to call the Google Recaptcha API");
|
|
14
|
+
}
|
|
15
|
+
const result = (await response.json());
|
|
16
|
+
if (result.hostname !== service.fqdn) {
|
|
17
|
+
throw new ApiError("Unauthorized", 401);
|
|
18
|
+
}
|
|
19
|
+
return result.success;
|
|
20
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{template as
|
|
1
|
+
import{template as E,insert as c,setAttribute as p,createComponent as I,effect as b,render as ae}from"https://esm.sh/solid-js@1.9.7/web";import{createMemo as oe,createSignal as i,Show as U,onMount as re,onCleanup as ce}from"https://esm.sh/solid-js@1.9.7";const le={en:{en:"English",fr:"French",es:"Spanish",de:"German",it:"Italian",pt:"Portuguese"},fr:{en:"Anglais",fr:"Français",es:"Espagnol",de:"Allemand",it:"Italien",pt:"Portugais"},es:{en:"Inglés",fr:"Francés",es:"Español",de:"Alemán",it:"Italiano",pt:"Portugués"},de:{en:"Englisch",fr:"Französisch",es:"Spanisch",de:"Deutsch",it:"Italienisch",pt:"Portugiesisch"},it:{en:"Inglese",fr:"Francese",es:"Spagnolo",de:"Tedesco",it:"Italiano",pt:"Portoghese"},pt:{en:"Inglês",fr:"Francês",es:"Espanhol",de:"Alemão",it:"Italiano",pt:"Português"}},J=Object.keys(le);function ie(e,{pluginLanguages:s,siteLanguages:n=[]}){if(s.includes(e)||J.includes(e)&&s.includes(e))return e;const a=n.find((o=>J.includes(o)&&s.includes(o)));return a||(s.includes("en")?"en":s[0]??"en")}const q=e=>e!=null&&(e=Object.getPrototypeOf(e),e===Array.prototype||e===Object.prototype);function G(e,s,n){for(const[a,o]of Object.entries(s)){const d=`${n}.${a}`;e[d]=o,q(o)&&G(e,o,d)}}function de(e){const s={...e};for(const[n,a]of Object.entries(e))q(a)&&G(s,a,n);return s}const me=(e,s)=>{if(s)for(const[n,a]of Object.entries(s))e=e.replace(new RegExp(`{{\\s*${n}\\s*}}`,"g"),a);return e},ue=e=>e;function he(e,s=ue){return(n,...a)=>{n[0]==="."&&(n=n.slice(1));const o=e()?.[n];switch(typeof o){case"function":return o(...a);case"string":return s(o,a[0]);default:return o}}}const ge="Name",pe="E-Mail",fe="Betreff",$e="Nachricht",Pe="Senden",be="johndoe@gmail.com",ve="John Doe",Ee="Der Betreff Ihrer Nachricht",je="Geben Sie hier Ihre Nachricht ein…",Me="Unerwarteter Fehler, die Nachricht konnte nicht gesendet werden.",Se="Zur Startseite zurückkehren",we="Ihre Nachricht wurde gesendet. Falls es eine Antwort gibt, wird sie an diese Adresse gesendet:",ye={name:ge,email:pe,subject:fe,message:$e,send:Pe,emailPlaceholder:be,namePlaceholder:ve,subjectPlaceholder:Ee,messagePlaceholder:je,unexpectedErrorMessage:Me,returnHome:Se,successMessage:we},Fe="Name",xe="Email",Ie="Subject",_e="Message",Ce="Send",Re="johndoe@gmail.com",He="John Doe",Ae="The subject of your message",ke="Enter your message here…",Ne="Unexpected error, the message could not be sent.",De="Return Home",Oe="Your message has been sent. If there is a response, it will be sent to the address:",Le={name:Fe,email:xe,subject:Ie,message:_e,send:Ce,emailPlaceholder:Re,namePlaceholder:He,subjectPlaceholder:Ae,messagePlaceholder:ke,unexpectedErrorMessage:Ne,returnHome:De,successMessage:Oe},Te="Nombre",Ue="Email",Je="Asunto",ze="Mensaje",Be="Enviar",qe="johndoe@gmail.com",Ge="John Doe",Ve="El asunto de su mensaje",Ye="Ingrese su mensaje aquí…",Ze="Error inesperado, no se pudo enviar el mensaje.",Ke="Volver al Inicio",Qe="Su mensaje ha sido enviado. Si hay una respuesta, será enviada a la dirección:",We={name:Te,email:Ue,subject:Je,message:ze,send:Be,emailPlaceholder:qe,namePlaceholder:Ge,subjectPlaceholder:Ve,messagePlaceholder:Ye,unexpectedErrorMessage:Ze,returnHome:Ke,successMessage:Qe},Xe="Nom",es="Courriel",ss="Sujet",ts="Message",ns="Envoyer",as="johndoe@gmail.com",os="John Doe",rs="Le sujet de votre message",cs="Saisissez votre message…",ls="Erreur inattendue, le message n'a pas pu être envoyé.",is="Retourner à l'accueil",ds="Votre message a été envoyé. S'il y a une réponse, elle sera envoyée à l'adresse :",ms={name:Xe,email:es,subject:ss,message:ts,send:ns,emailPlaceholder:as,namePlaceholder:os,subjectPlaceholder:rs,messagePlaceholder:cs,unexpectedErrorMessage:ls,returnHome:is,successMessage:ds},us="Nome",hs="Email",gs="Oggetto",ps="Messaggio",fs="Invia",$s="johndoe@gmail.com",Ps="John Doe",bs="L'oggetto del tuo messaggio",vs="Inserisci qui il tuo messaggio…",Es="Errore imprevisto, il messaggio non è stato inviato.",js="Torna alla Home",Ms="Il tuo messaggio è stato inviato. Se c'è una risposta, sarà inviata all'indirizzo:",Ss={name:us,email:hs,subject:gs,message:ps,send:fs,emailPlaceholder:$s,namePlaceholder:Ps,subjectPlaceholder:bs,messagePlaceholder:vs,unexpectedErrorMessage:Es,returnHome:js,successMessage:Ms},ws="Nome",ys="Email",Fs="Assunto",xs="Mensagem",Is="Enviar",_s="johndoe@gmail.com",Cs="John Doe",Rs="O assunto da sua mensagem",Hs="Digite sua mensagem aqui…",As="Erro inesperado, a mensagem não pôde ser enviada.",ks="Voltar ao Início",Ns="Sua mensagem foi enviada. Se houver uma resposta, ela será enviada para o endereço:",Ds={name:ws,email:ys,subject:Fs,message:xs,send:Is,emailPlaceholder:_s,namePlaceholder:Cs,subjectPlaceholder:Rs,messagePlaceholder:Hs,unexpectedErrorMessage:As,returnHome:ks,successMessage:Ns},z={en:Le,fr:ms,es:We,de:ye,it:Ss,pt:Ds};function Os(e,s){const n=oe((()=>{const o=Object.keys(z),j=ie(e,{pluginLanguages:o,siteLanguages:s});return de(z[j])}));return{t:he(n,me)}}const Ls="/api/plugin/contact-form-plugin",Ts=Ls;async function Us(e,s,n){n(!0);try{const a=await Js(e);return s.reset(),a}finally{n(!1)}}async function Js(e){const s=await fetch(Ts,{headers:{"Content-Type":"application/json"},body:JSON.stringify(e),method:"POST"});if(s.status!==200){const a=await s.text();throw new Error(`Unexpected error: ${a}`)}return await s.json()}var zs=E("<div class=PaForm-error>"),Bs=E('<div class=PaForm-ended><div class=PaForm-message><p> <i></i></p></div><div class=PaForm-action><a class="PaForm-button PaButton">'),qs=E('<form class=PaForm><div class=PaForm-fields><label class=PaField><span class=PaField-label></span><input class=PaField-input type=text name=name required></label><label class=PaField><span class=PaField-label></span><input class=PaField-input type=email name=email required></label><label class=PaField><span class=PaField-label></span><textarea class=PaField-input rows=5 name=message required></textarea></label></div><div class=PaForm-action><button class="PaForm-button PaButton"type=submit>'),Gs=E("<div class=PaForm-captcha><div class=g-recaptcha data-callback=onRecaptchaSubmitted data-expired-callback=onRecaptchaExpired>");function Vs({googleRecaptchaSiteKey:e,language:s,homeUrl:n}){const[a,o]=i(""),[d,j]=i(""),[_,Y]=i(""),[Z,K]=i(),[C,R]=i(!e),[H,M]=i(),[Q,A]=i(""),[W,X]=i(!1),{t:r}=Os(s),ee=async l=>{try{if(l.preventDefault(),!C()||!f)throw new Error("Recaptcha error");const m=await Us({language:s,name:a(),message:_(),email:d(),gRecaptchaResponse:Z()},f,X);m.success?M(!0):(M(!1),A(m.message??r("unexpectedErrorMessage")))}catch{M(!1),A(r("unexpectedErrorMessage"))}},f=(()=>{var l=qs(),m=l.firstChild,k=m.firstChild,N=k.firstChild,S=N.nextSibling,D=k.nextSibling,O=D.firstChild,w=O.nextSibling,ne=D.nextSibling,L=ne.firstChild,y=L.nextSibling,F=m.nextSibling,x=F.firstChild;return l.addEventListener("submit",ee),c(N,(()=>r("name"))),S.addEventListener("change",(t=>o(t.currentTarget.value))),c(O,(()=>r("email"))),w.addEventListener("change",(t=>{j(t.currentTarget.value)})),c(L,(()=>r("message"))),y.addEventListener("change",(t=>Y(t.currentTarget.value))),c(l,e&&(()=>{var t=Gs(),u=t.firstChild;return p(u,"data-sitekey",e),t})(),F),c(l,I(U,{get when(){return H()===!1},get children(){var t=zs();return c(t,Q),t}}),F),c(x,(()=>r("send"))),c(l,I(U,{get when(){return H()===!0},get children(){var t=Bs(),u=t.firstChild,h=u.firstChild,g=h.firstChild,$=g.nextSibling,P=u.nextSibling,T=P.firstChild;return c(h,(()=>r("successMessage")),g),c($,d),p(T,"href",n),c(T,(()=>r("returnHome"))),t}}),null),b((t=>{var u=r("namePlaceholder"),h=r("emailPlaceholder"),g=r("messagePlaceholder"),$=!!W(),P=!C();return u!==t.e&&p(S,"placeholder",t.e=u),h!==t.t&&p(w,"placeholder",t.t=h),g!==t.a&&p(y,"placeholder",t.a=g),$!==t.o&&x.classList.toggle("inProgress",t.o=$),P!==t.i&&(x.disabled=t.i=P),t}),{e:void 0,t:void 0,a:void 0,o:void 0,i:void 0}),b((()=>S.value=a())),b((()=>w.value=d())),b((()=>y.value=_())),l})();return re((()=>{e&&(window.onRecaptchaSubmitted=se,window.onRecaptchaExpired=te)})),ce((()=>{e&&(window.onRecaptchaSubmitted=void 0,window.onRecaptchaExpired=void 0)})),f;function se(l){if(!l)return;R(!0);const m=f?.elements.namedItem("g-recaptcha-response");K(m.value)}function te(){R(!1)}}let V;const[v]=document.getElementsByClassName("js-script-contact-form");if(v&&v instanceof HTMLScriptElement){if(!v.dataset.googleRecaptchaSiteKey)throw new Error('missing "data-google-recaptcha-site-key" in ".js-script-contact-form" element');V=v.dataset.googleRecaptchaSiteKey}function Ys(e,{language:s}){if(!s)throw new Error("Missing language");e.dataset.recaptchaKey&&console.warn('Remove "data-recaptcha-key" attribute, it is not needed anymore.');const n=e.dataset.homeUrl;if(!n)throw new Error('Missing "data-home-url"');const a=document.createElement("script");a.setAttribute("src","https://www.google.com/recaptcha/api.js"),document.head.appendChild(a),ae((()=>I(Vs,{googleRecaptchaSiteKey:V,language:s,homeUrl:n})),e)}document.readyState==="loading"?document.addEventListener("DOMContentLoaded",B):B();function B(){const e=document.documentElement.lang,s=document.querySelector("[data-effect='paContactForm']");s&&Ys(s,{language:e})}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paroicms/contact-form-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.0",
|
|
4
4
|
"description": "Contact form plugin for ParoiCMS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"paroicms",
|
|
@@ -16,17 +16,17 @@
|
|
|
16
16
|
"author": "Paroi Team",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"type": "module",
|
|
19
|
-
"main": "backend/dist/
|
|
19
|
+
"main": "backend/dist/index.js",
|
|
20
20
|
"scripts": {
|
|
21
|
-
"
|
|
22
|
-
"build": "npm run build:backend && npm run build:public",
|
|
21
|
+
"build": "npm run build:backend && npm run build:frontend",
|
|
23
22
|
"build:backend": "(cd backend && tsc)",
|
|
24
|
-
"build:
|
|
25
|
-
"
|
|
26
|
-
"build:
|
|
23
|
+
"build:backend:watch": "(cd backend && tsc --watch --preserveWatchOutput)",
|
|
24
|
+
"build:frontend": "(cd frontend && npm run build)",
|
|
25
|
+
"build:frontend:watch": "(cd frontend && npm run build:watch)",
|
|
27
26
|
"clear": "rimraf backend/dist/* frontend/dist/*"
|
|
28
27
|
},
|
|
29
28
|
"dependencies": {
|
|
29
|
+
"@paroicms/script-lib": "0.2.0",
|
|
30
30
|
"arktype": "~2.1.20"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
@@ -34,18 +34,11 @@
|
|
|
34
34
|
"@paroicms/public-server-lib": "0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
-
"@paroicms/public-anywhere-lib": "0.
|
|
38
|
-
"@paroicms/public-server-lib": "0.
|
|
39
|
-
"@solid-primitives/i18n": "~2.2.1",
|
|
37
|
+
"@paroicms/public-anywhere-lib": "0.34.0",
|
|
38
|
+
"@paroicms/public-server-lib": "0.43.0",
|
|
40
39
|
"@types/node": "~24.0.1",
|
|
41
40
|
"rimraf": "~6.0.1",
|
|
42
|
-
"
|
|
43
|
-
"solid-devtools": "~0.34.0",
|
|
44
|
-
"solid-js": "1.9.7",
|
|
45
|
-
"terser": "~5.42.0",
|
|
46
|
-
"typescript": "~5.8.3",
|
|
47
|
-
"vite": "~6.3.5",
|
|
48
|
-
"vite-plugin-solid": "~2.11.6"
|
|
41
|
+
"typescript": "~5.8.3"
|
|
49
42
|
},
|
|
50
43
|
"files": [
|
|
51
44
|
"backend/dist",
|