From 7d353b3578c47b04ec82e2a54d4b7a3224491322 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 7 Dec 2023 17:55:37 +0100 Subject: [PATCH 01/13] feat: add bal widget config page --- .../bal-widget/bal-widget-config-form.tsx | 176 ++++++++++++++++++ components/header.tsx | 8 + components/multi-link-input.tsx | 86 +++++++++ components/multi-string-input.tsx | 71 +++++++ lib/bal-widget.ts | 38 ++++ pages/bal-widget/index.tsx | 43 +++++ server/index.js | 2 + server/lib/bal-widget/controller.js | 31 +++ server/lib/bal-widget/schemas.js | 0 server/lib/bal-widget/service.js | 37 ++++ server/utils/mongo-client.js | 4 + types/bal-widget.ts | 20 ++ 12 files changed, 516 insertions(+) create mode 100644 components/bal-widget/bal-widget-config-form.tsx create mode 100644 components/multi-link-input.tsx create mode 100644 components/multi-string-input.tsx create mode 100644 lib/bal-widget.ts create mode 100644 pages/bal-widget/index.tsx create mode 100644 server/lib/bal-widget/controller.js create mode 100644 server/lib/bal-widget/schemas.js create mode 100644 server/lib/bal-widget/service.js create mode 100644 types/bal-widget.ts diff --git a/components/bal-widget/bal-widget-config-form.tsx b/components/bal-widget/bal-widget-config-form.tsx new file mode 100644 index 0000000..9dce7b6 --- /dev/null +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -0,0 +1,176 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import React, { useState, useMemo } from "react"; +import styled from "styled-components"; +import { Input } from "@codegouvfr/react-dsfr/Input"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox"; + +import { BALWidgetConfig } from "../../types/bal-widget"; +import { MultiStringInput } from "../multi-string-input"; +import { MultiLinkInput } from "../multi-link-input"; + +type BALWidgetConfigFormProps = { + config: BALWidgetConfig; + onSubmit: (data: BALWidgetConfig) => Promise; +}; + +const StyledForm = styled.form` + h3, + h4 { + margin-bottom: 1rem; + } + + section { + margin: 1rem 0; + } + + .form-controls { + display: flex; + align-items: center; + + > :not(:first-child) { + margin-left: 1rem; + } + } +`; + +const defaultConfig: BALWidgetConfig = { + global: { + title: "Centre d'aide Base Adresse Locale", + hideWidget: false, + showOnPages: [], + }, + gitbook: { + welcomeBlockTitle: "Ces articles pourraient vous aider", + topArticles: [], + }, + contactUs: { + welcomeBlockTitle: "Nous contacter", + subjects: [], + }, +}; + +export const BALWidgetConfigForm = ({ + onSubmit, + config: baseConfig, +}: BALWidgetConfigFormProps) => { + const initialConfig = baseConfig ? { ...baseConfig } : { ...defaultConfig }; + const [formData, setFormData] = useState(initialConfig); + + const canPublish = useMemo(() => { + return JSON.stringify(formData) !== JSON.stringify(initialConfig); + }, [formData, initialConfig]); + + const handleEdit = + (section: keyof BALWidgetConfig, property: string) => + (e: React.ChangeEvent) => { + const { value } = e.target; + setFormData((state) => ({ + ...state, + [section]: { + ...state[section], + [property]: value, + }, + })); + }; + + const handleToggle = + (section: keyof BALWidgetConfig, property: string) => () => { + setFormData((state) => ({ + ...state, + [section]: { + ...state[section], + [property]: !state[section][property], + }, + })); + }; + + const resetForm = () => { + setFormData(initialConfig); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + await onSubmit(formData); + }; + + return ( + +

Configuration du widget

+
+

Globale

+ + + + handleEdit("global", "showOnPages")({ target: { value } } as any) + } + /> +
+
+

Gitbook

+ + + handleEdit("gitbook", "topArticles")({ target: { value } } as any) + } + /> +
+
+

Formulaire de contact

+ + + handleEdit("contactUs", "subjects")({ target: { value } } as any) + } + /> +
+
+ + +
+
+ ); +}; diff --git a/components/header.tsx b/components/header.tsx index ff33d49..c9bf6f6 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,3 +1,4 @@ +import React from "react"; import Link from "next/link"; import { signIn, signOut } from "next-auth/react"; import type { Session } from "next-auth"; @@ -138,6 +139,13 @@ const Header = ({ session }: HeaderProps) => ( +
  • + + + BAL Widget + + +
  • diff --git a/components/multi-link-input.tsx b/components/multi-link-input.tsx new file mode 100644 index 0000000..ec3ea2a --- /dev/null +++ b/components/multi-link-input.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import styled from "styled-components"; +import { BALWidgetLink } from "../types/bal-widget"; + +type MultiLinkInputProps = { + label: string; + value: BALWidgetLink[]; + onChange: (value: BALWidgetLink[]) => void; +}; + +const StyledWrapper = styled.div` + > div { + display: flex; + align-items: center; + margin-bottom: 1rem; + + > :not(:first-child) { + margin-left: 1rem; + } + } +`; + +export const MultiLinkInput = ({ + label, + value, + onChange, +}: MultiLinkInputProps) => { + const addValue = () => { + onChange([...value, { label: "", url: "" }]); + }; + + const removeValue = (index: number) => { + onChange(value.filter((_, i) => i !== index)); + }; + + const handleChange = ( + index: number, + property: keyof BALWidgetLink, + newValue: string + ) => { + onChange( + value.map((v, i) => (i === index ? { ...v, [property]: newValue } : v)) + ); + }; + + return ( + +
    + +
    + {value.map(({ label, url }, index) => ( +
    + handleChange(index, "label", e.target.value)} + placeholder="Comment puis-je obtenir une adresse ?" + /> + handleChange(index, "url", e.target.value)} + placeholder="https://example.com" + /> +
    + ))} +
    + ); +}; diff --git a/components/multi-string-input.tsx b/components/multi-string-input.tsx new file mode 100644 index 0000000..3594a25 --- /dev/null +++ b/components/multi-string-input.tsx @@ -0,0 +1,71 @@ +import React from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import styled from "styled-components"; + +type MultiStringInputProps = { + label: string; + value: string[]; + onChange: (value: string[]) => void; +}; + +const StyledWrapper = styled.div` + > div { + display: flex; + align-items: center; + margin-bottom: 1rem; + + > :not(:first-child) { + margin-left: 1rem; + } + } +`; + +export const MultiStringInput = ({ + label, + value, + onChange, +}: MultiStringInputProps) => { + const addValue = () => { + onChange([...value, ""]); + }; + + const removeValue = (index: number) => { + onChange(value.filter((_, i) => i !== index)); + }; + + const handleChange = (index: number, newValue: string) => { + onChange(value.map((v, i) => (i === index ? newValue : v))); + }; + + return ( + +
    + +
    + {value.map((page, index) => ( +
    + handleChange(index, e.target.value)} + placeholder="https://example.com" + /> +
    + ))} +
    + ); +}; diff --git a/lib/bal-widget.ts b/lib/bal-widget.ts new file mode 100644 index 0000000..877fd97 --- /dev/null +++ b/lib/bal-widget.ts @@ -0,0 +1,38 @@ +import { BALWidgetConfig } from "../types/bal-widget"; + +// eslint-disable-next-line @typescript-eslint/naming-convention +const NEXT_PUBLIC_BAL_ADMIN_URL = + process.env.NEXT_PUBLIC_BAL_ADMIN_URL || "http://localhost:3000"; + +export async function getBALWidgetConfig() { + const response = await fetch( + `${NEXT_PUBLIC_BAL_ADMIN_URL}/api/bal-widget/config` + ); + if (!response.ok) { + const body = await response.json(); + throw new Error(body.message); + } + + const data = await response.json(); + + return data as BALWidgetConfig; +} + +export async function setBALWidgetConfig(payload) { + const response = await fetch( + `${NEXT_PUBLIC_BAL_ADMIN_URL}/api/bal-widget/config`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + } + ); + if (!response.ok) { + const body = await response.json(); + throw new Error(body.message); + } + + const data = await response.json(); + + return data as BALWidgetConfig; +} diff --git a/pages/bal-widget/index.tsx b/pages/bal-widget/index.tsx new file mode 100644 index 0000000..cac7b17 --- /dev/null +++ b/pages/bal-widget/index.tsx @@ -0,0 +1,43 @@ +import React, { useState } from "react"; +import { getBALWidgetConfig } from "../../lib/bal-widget"; +import { BALWidgetConfig } from "../../types/bal-widget"; +import { BALWidgetConfigForm } from "../../components/bal-widget/bal-widget-config-form"; +import { setBALWidgetConfig } from "../../lib/bal-widget"; +import { toast } from "react-toastify"; + +type BALWidgetPageProps = { + config: BALWidgetConfig; +}; + +const BALWidgetPage = ({ config: baseConfig }: BALWidgetPageProps) => { + const [config, setConfig] = useState(baseConfig); + const onSubmit = async (formData: BALWidgetConfig) => { + try { + const config = await setBALWidgetConfig(formData); + toast("Configuration du widget mise à jour", { type: "success" }); + setConfig(config); + } catch (error: unknown) { + console.log(error); + toast("Erreur lors de la mise à jour de la configuration du widget", { + type: "error", + }); + } + }; + return ( +
    + +
    + ); +}; + +export async function getServerSideProps() { + const config = await getBALWidgetConfig(); + + return { + props: { + config, + }, + }; +} + +export default BALWidgetPage; diff --git a/server/index.js b/server/index.js index fc033f9..657b4a8 100644 --- a/server/index.js +++ b/server/index.js @@ -38,7 +38,9 @@ async function main() { // Some Partenaire de la charte routes are public, others are protected by routeGuard server.use('/api/partenaires-de-la-charte', require('./lib/partenaire-de-la-charte/controller')) server.use('/api/events', require('./lib/events/controller')) + server.use('/api/bal-widget', require('./lib/bal-widget/controller')) + server.use(async (req, res) => { // Authentification is handled by the next app using next-auth module await nextAppRequestHandler(req, res) diff --git a/server/lib/bal-widget/controller.js b/server/lib/bal-widget/controller.js new file mode 100644 index 0000000..9eccfcc --- /dev/null +++ b/server/lib/bal-widget/controller.js @@ -0,0 +1,31 @@ +const express = require("express"); +const cors = require("cors"); +const {routeGuard} = require('../../route-guard') +const BALWidgetService = require("./service"); + +const BALWidgetRoutes = new express.Router(); + +BALWidgetRoutes.use(express.json()); +BALWidgetRoutes.use(cors()); + +BALWidgetRoutes.get("/config", async (req, res) => { + try { + const config = await BALWidgetService.getConfig(req.query); + res.json(config); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}); + +BALWidgetRoutes.post("/config", routeGuard, async (req, res) => { + try { + const config = await BALWidgetService.setConfig(req.body); + res.json(config); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}); + +module.exports = BALWidgetRoutes; diff --git a/server/lib/bal-widget/schemas.js b/server/lib/bal-widget/schemas.js new file mode 100644 index 0000000..e69de29 diff --git a/server/lib/bal-widget/service.js b/server/lib/bal-widget/service.js new file mode 100644 index 0000000..340eefb --- /dev/null +++ b/server/lib/bal-widget/service.js @@ -0,0 +1,37 @@ +const { ObjectId } = require("mongodb"); +const mongoClient = require("../../utils/mongo-client"); +const { validPayload } = require("../../utils/payload"); +const { sendMail } = require("../mailer/service"); + +const collectionName = "bal-widget"; + +async function getConfig(query = {}) { + const { draft } = query; + + const mongoQuery = { + type: draft ? "draft" : "published", + }; + + const {_id, _created, _updated, ...record} = await mongoClient.findOne(collectionName, mongoQuery); + + return record; +} + +async function setConfig(payload) { + const { draft, ...update } = payload; + const type = draft ? "draft" : "published" + const record = await mongoClient.findOne(collectionName, {type}); + + if (!record) { + await mongoClient.insertOne(collectionName, {type, ...update}); + } else { + await mongoClient.updateOne(collectionName, record._id, {type, ...update}); + } + + return update; +} + +module.exports = { + getConfig, + setConfig, +}; diff --git a/server/utils/mongo-client.js b/server/utils/mongo-client.js index b2b39fa..f60849e 100644 --- a/server/utils/mongo-client.js +++ b/server/utils/mongo-client.js @@ -78,6 +78,10 @@ class Mongo { return this.db.collection(collectionName).findOne({_id: this.parseObjectId(id)}) } + findOne(collectionName, options = {}) { + return this.db.collection(collectionName).findOne({_deleted: undefined, ...options}) + } + findMany(collectionName, options = {}) { return this.db.collection(collectionName).find({_deleted: undefined, ...options}).toArray() } diff --git a/types/bal-widget.ts b/types/bal-widget.ts new file mode 100644 index 0000000..472a3d4 --- /dev/null +++ b/types/bal-widget.ts @@ -0,0 +1,20 @@ +export interface BALWidgetLink { + label: string; + url: string; +} + +export interface BALWidgetConfig { + global: { + title: string; + hideWidget: boolean; + showOnPages: string[]; + }; + gitbook: { + welcomeBlockTitle: string; + topArticles: BALWidgetLink[]; + }; + contactUs: { + welcomeBlockTitle: string; + subjects: string[]; + }; +} From 2dc99c9407b181d5ab41fbc6cb7853bb8673ba96 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 8 Dec 2023 11:56:42 +0100 Subject: [PATCH 02/13] feat: add send form contact mail endpoint --- .env.sample | 3 ++- server/lib/bal-widget/controller.js | 11 ++++++++ server/lib/mailer/email.templates.js | 23 +++++++++++----- server/lib/mailer/schemas.js | 14 ++++++++++ server/lib/mailer/service.js | 39 ++++++++++++++++++++++++++-- 5 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 server/lib/mailer/schemas.js diff --git a/.env.sample b/.env.sample index a69237e..46621b7 100644 --- a/.env.sample +++ b/.env.sample @@ -24,4 +24,5 @@ KEYCLOAK_ISSUER= KEYCLOAK_CLIENT_ID= KEYCLOAK_CLIENT_SECRET= NEXTAUTH_URL= -NEXTAUTH_SECRET= \ No newline at end of file +NEXTAUTH_SECRET= +RE_CAPTCHA_SECRET_KEY= \ No newline at end of file diff --git a/server/lib/bal-widget/controller.js b/server/lib/bal-widget/controller.js index 9eccfcc..2d1f18c 100644 --- a/server/lib/bal-widget/controller.js +++ b/server/lib/bal-widget/controller.js @@ -2,6 +2,7 @@ const express = require("express"); const cors = require("cors"); const {routeGuard} = require('../../route-guard') const BALWidgetService = require("./service"); +const MailerService = require("../mailer/service"); const BALWidgetRoutes = new express.Router(); @@ -28,4 +29,14 @@ BALWidgetRoutes.post("/config", routeGuard, async (req, res) => { } }); +BALWidgetRoutes.post("/send-mail", async (req, res) => { + try { + await MailerService.sendFormContactMail(req.body); + res.json(config); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}); + module.exports = BALWidgetRoutes; diff --git a/server/lib/mailer/email.templates.js b/server/lib/mailer/email.templates.js index 3f4d32e..52392b1 100644 --- a/server/lib/mailer/email.templates.js +++ b/server/lib/mailer/email.templates.js @@ -1,9 +1,18 @@ module.exports = { - 'candidature-partenaire-de-la-charte': { - from: process.env.SMTP_FROM || 'adresse@data.gouv.fr', - to: 'adresse@data.gouv.fr', - subject: 'Nouvelle candidature aux partenaires de la charte', + "candidature-partenaire-de-la-charte": { + from: process.env.SMTP_FROM || "adresse@data.gouv.fr", + to: "adresse@data.gouv.fr", + subject: "Nouvelle candidature aux partenaires de la charte", text: `Bonjour,\n\nUne nouvelle candidature aux partenaires de la charte a été soumise.\n\nVous pouvez la consulter sur BAL Admin.\n\nBonne journée,\n\nL’équipe BAL`, - html: `

    Bonjour,

    Une nouvelle candidature aux partenaires de la charte a été soumise.

    Vous pouvez la consulter sur BAL Admin.

    Bonne journée,

    L’équipe BAL

    ` - } -} + html: `

    Bonjour,

    Une nouvelle candidature aux partenaires de la charte a été soumise.

    Vous pouvez la consulter sur BAL Admin.

    Bonne journée,

    L’équipe BAL

    `, + }, + contact: ({ firstName, lastName, email, message, subject }) => { + return { + from: process.env.SMTP_FROM || "adresse@data.gouv.fr", + to: "adresse@data.gouv.fr", + subject, + text: `Bonjour,\n\nVous avez reçu un nouveau message via le formulaire de contact de BAL Widget.\n\nNom: ${lastName}\nPrénom: ${firstName}\nEmail: ${email}\n\nMessage:\n${message}\n\nBonne journée,\n\nL’équipe BAL`, + html: `

    Bonjour,

    Vous avez reçu un nouveau message via le formulaire de contact de la BAL Widget.

    Nom: ${lastName}

    Prénom: ${firstName}

    Email: ${email}

    Message:

    ${message}

    Bonne journée,

    L’équipe BAL

    ` + }; + }, +}; diff --git a/server/lib/mailer/schemas.js b/server/lib/mailer/schemas.js new file mode 100644 index 0000000..c37b80c --- /dev/null +++ b/server/lib/mailer/schemas.js @@ -0,0 +1,14 @@ +const { validEmail } = require("../../utils/payload"); + +const mailSchema = { + firstName: { isRequired: false, type: "string" }, + lastName: { isRequired: false, type: "string" }, + email: { valid: validEmail, isRequired: true, type: "string" }, + message: { isRequired: true, type: "string" }, + subject: { isRequired: true, type: "string" }, + reCaptchaToken: { isRequired: true, type: "string" }, +}; + +module.exports = { + mailSchema, +}; diff --git a/server/lib/mailer/service.js b/server/lib/mailer/service.js index d81e482..dd0e2be 100644 --- a/server/lib/mailer/service.js +++ b/server/lib/mailer/service.js @@ -1,5 +1,7 @@ const nodemailer = require('nodemailer') const templates = require('./email.templates') +const {mailSchema} = require('./schemas') +const {validPayload} = require('../../utils/payload') function createTransport() { // Use mailhog in development @@ -27,7 +29,7 @@ function createTransport() { const transport = createTransport() -async function sendMail(templateKey) { +async function sendTemplateMail(templateKey) { const template = templates[templateKey] if (!template) { throw new Error(`Le template ${templateKey} n'existe pas`) @@ -42,6 +44,39 @@ async function sendMail(templateKey) { return true } +async function checkReCaptcha(reCaptchaToken) { + const response = await fetch(`https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RE_CAPTCHA_SECRET_KEY}&response=${reCaptchaToken}`, { + method: 'POST' + }) + + const json = await response.json() + + if (!json.success) { + throw new Error('Le reCaptcha est invalide') + } + + return json.success +} + +async function sendFormContactMail(payload) { + const validatedPayload = validPayload(payload, mailSchema) + const {reCaptchaToken, ...emailData } = validatedPayload + + await checkReCaptcha(reCaptchaToken) + + const contactTemplate = templates.contact(emailData) + + const response = await transport.sendMail(contactTemplate) + + if (!response) { + throw new Error('Une erreur est survenue lors de l\'envoi de l\'email') + } + + return true +} + + module.exports = { - sendMail + sendTemplateMail, + sendFormContactMail } From fbd6f10bfc8efd9bb464bead7d47f488b766d65f Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Mon, 11 Dec 2023 11:28:16 +0100 Subject: [PATCH 03/13] fix send template mail --- server/lib/partenaire-de-la-charte/service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/partenaire-de-la-charte/service.js b/server/lib/partenaire-de-la-charte/service.js index e09ce70..14a3664 100644 --- a/server/lib/partenaire-de-la-charte/service.js +++ b/server/lib/partenaire-de-la-charte/service.js @@ -1,7 +1,7 @@ const {ObjectId} = require('mongodb') const mongoClient = require('../../utils/mongo-client') const {validPayload} = require('../../utils/payload') -const {sendMail} = require('../mailer/service') +const {sendTemplateMail} = require('../mailer/service') const {createCommuneSchema, createOrganismeSchema, createEntrepriseSchema} = require('./schemas') const collectionName = 'partenaires-de-la-charte' @@ -84,7 +84,7 @@ async function createOne(payload, options = {}) { if (isCandidate) { try { - await sendMail('candidature-partenaire-de-la-charte') + await sendTemplateMail('candidature-partenaire-de-la-charte') } catch (error) { console.error(error) } From deb6275af0fdfc5ef72e102f0619a358fe198029 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Mon, 11 Dec 2023 11:29:24 +0100 Subject: [PATCH 04/13] fix: bal-widget service --- server/lib/bal-widget/service.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/lib/bal-widget/service.js b/server/lib/bal-widget/service.js index 340eefb..3d7b403 100644 --- a/server/lib/bal-widget/service.js +++ b/server/lib/bal-widget/service.js @@ -1,7 +1,4 @@ -const { ObjectId } = require("mongodb"); const mongoClient = require("../../utils/mongo-client"); -const { validPayload } = require("../../utils/payload"); -const { sendMail } = require("../mailer/service"); const collectionName = "bal-widget"; From 303f2d213ce2a92d022f1fadb31ae66071b41709 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 21 Dec 2023 14:13:03 +0100 Subject: [PATCH 05/13] fix: replace hcaptcha by recaptcha --- .env.sample | 2 +- server/lib/mailer/schemas.js | 2 +- server/lib/mailer/service.js | 16 ++++++++++------ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.env.sample b/.env.sample index 46621b7..730e803 100644 --- a/.env.sample +++ b/.env.sample @@ -25,4 +25,4 @@ KEYCLOAK_CLIENT_ID= KEYCLOAK_CLIENT_SECRET= NEXTAUTH_URL= NEXTAUTH_SECRET= -RE_CAPTCHA_SECRET_KEY= \ No newline at end of file +HCAPTCHA_SECRET_KEY= \ No newline at end of file diff --git a/server/lib/mailer/schemas.js b/server/lib/mailer/schemas.js index c37b80c..c2eaa6f 100644 --- a/server/lib/mailer/schemas.js +++ b/server/lib/mailer/schemas.js @@ -6,7 +6,7 @@ const mailSchema = { email: { valid: validEmail, isRequired: true, type: "string" }, message: { isRequired: true, type: "string" }, subject: { isRequired: true, type: "string" }, - reCaptchaToken: { isRequired: true, type: "string" }, + captchaToken: { isRequired: true, type: "string" }, }; module.exports = { diff --git a/server/lib/mailer/service.js b/server/lib/mailer/service.js index dd0e2be..b840f73 100644 --- a/server/lib/mailer/service.js +++ b/server/lib/mailer/service.js @@ -44,15 +44,19 @@ async function sendTemplateMail(templateKey) { return true } -async function checkReCaptcha(reCaptchaToken) { - const response = await fetch(`https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RE_CAPTCHA_SECRET_KEY}&response=${reCaptchaToken}`, { - method: 'POST' +async function checkCaptcha(captchaToken) { + const response = await fetch(`https://api.hcaptcha.com/siteverify`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: `response=${captchaToken}&secret=${process.env.HCAPTCHA_SECRET_KEY}`, }) const json = await response.json() if (!json.success) { - throw new Error('Le reCaptcha est invalide') + throw new Error('Le captcha est invalide') } return json.success @@ -60,9 +64,9 @@ async function checkReCaptcha(reCaptchaToken) { async function sendFormContactMail(payload) { const validatedPayload = validPayload(payload, mailSchema) - const {reCaptchaToken, ...emailData } = validatedPayload + const {captchaToken, ...emailData } = validatedPayload - await checkReCaptcha(reCaptchaToken) + await checkCaptcha(captchaToken) const contactTemplate = templates.contact(emailData) From 01f7e2b6e17bf071541c7c555e608925772c42ca Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 22 Dec 2023 16:17:32 +0100 Subject: [PATCH 06/13] feat: add commune infos endpoint --- server/index.js | 8 +++++--- server/lib/bal-widget/controller.js | 11 +++++++++++ server/lib/bal-widget/service.js | 23 +++++++++++++++++++++++ server/proxy-api-depot.js | 5 ++++- server/proxy-api-moissonneur-bal.js | 5 ++++- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/server/index.js b/server/index.js index 657b4a8..8aeb44a 100644 --- a/server/index.js +++ b/server/index.js @@ -4,6 +4,8 @@ const next = require('next') require('dotenv').config() const mongoClient = require('./utils/mongo-client') const {routeGuard} = require('./route-guard') +const {apiDepotProxy} = require('./proxy-api-depot') +const {apiMoissonneurProxy} = require('./proxy-api-moissonneur-bal') function setDemoClient(req, res, next) { req.demo = 1 @@ -30,9 +32,9 @@ async function main() { })) // Proxy routes are protected by routeGuard - server.use('/api/proxy-api-depot', routeGuard, require('./proxy-api-depot')) - server.use('/api/proxy-api-depot-demo', routeGuard, setDemoClient, require('./proxy-api-depot')) - server.use('/api/proxy-api-moissonneur-bal', routeGuard, require('./proxy-api-moissonneur-bal')) + server.use('/api/proxy-api-depot', routeGuard, apiDepotProxy) + server.use('/api/proxy-api-depot-demo', routeGuard, setDemoClient, apiDepotProxy) + server.use('/api/proxy-api-moissonneur-bal', routeGuard, apiMoissonneurProxy) server.use('/api/proxy-mes-adresses-api', routeGuard, require('./proxy-mes-adresses-api')) // Some Partenaire de la charte routes are public, others are protected by routeGuard diff --git a/server/lib/bal-widget/controller.js b/server/lib/bal-widget/controller.js index 2d1f18c..c39b6bf 100644 --- a/server/lib/bal-widget/controller.js +++ b/server/lib/bal-widget/controller.js @@ -39,4 +39,15 @@ BALWidgetRoutes.post("/send-mail", async (req, res) => { } }); +BALWidgetRoutes.get("/commune/:code", async (req, res) => { + try { + const { code } = req.params; + const response = await BALWidgetService.getCommuneInfos(code); + res.json(response); + } catch (err) { + console.error(err); + res.status(500).json({ error: err.message }); + } +}); + module.exports = BALWidgetRoutes; diff --git a/server/lib/bal-widget/service.js b/server/lib/bal-widget/service.js index 3d7b403..9d6eb97 100644 --- a/server/lib/bal-widget/service.js +++ b/server/lib/bal-widget/service.js @@ -1,4 +1,8 @@ const mongoClient = require("../../utils/mongo-client"); +const {apiDepotClient} = require("../../proxy-api-depot"); +const {apiMoissonneurClient} = require("../../proxy-api-moissonneur-bal"); + +const NEXT_PUBLIC_API_MES_ADRESSES = process.env.NEXT_PUBLIC_API_MES_ADRESSES || 'https://api-bal.adresse.data.gouv.fr/v1' const collectionName = "bal-widget"; @@ -28,7 +32,26 @@ async function setConfig(payload) { return update; } +async function getCommuneInfos(code) { + const mesAdressesResponse = await fetch(`${NEXT_PUBLIC_API_MES_ADRESSES}/bases-locales/search?commune=${code}`) + const mesAdressesResponseData = await mesAdressesResponse.json() + const balsMesAdresses = mesAdressesResponseData.results.filter(({status}) => { + return status === 'published' || status === 'replaced' + }) + + const apiDepotRevisionsResponse = await apiDepotClient.get(`communes/${code}/revisions?status=all`) + + const apiMoissonneurRevisionsResponse = await apiMoissonneurClient.get(`communes/${code}/revisions`) + + return { + balsMesAdresses, + apiDepotRevisions: apiDepotRevisionsResponse.body, + apiMoissonneurRevisions: apiMoissonneurRevisionsResponse.body, + }; +} + module.exports = { getConfig, setConfig, + getCommuneInfos, }; diff --git a/server/proxy-api-depot.js b/server/proxy-api-depot.js index 4d4e78c..98b2371 100644 --- a/server/proxy-api-depot.js +++ b/server/proxy-api-depot.js @@ -156,5 +156,8 @@ app.get('/stats/publications', w(getStatPublications)) app.get('/communes/:codeCommune/revisions', w(getAllRevisionsByCommune)) -module.exports = app +module.exports = { + apiDepotProxy: app, + apiDepotClient: client +} diff --git a/server/proxy-api-moissonneur-bal.js b/server/proxy-api-moissonneur-bal.js index 2c1f28b..1b82548 100644 --- a/server/proxy-api-moissonneur-bal.js +++ b/server/proxy-api-moissonneur-bal.js @@ -56,4 +56,7 @@ app.put('/sources/:sourceId', w(updateSource)) app.post('/revisions/:revisionId/publish', w(publishRevision)) app.get('/communes/:codeCommune/revisions', w(getRevisionsByCommune)) -module.exports = app +module.exports = { + apiMoissonneurProxy: app, + apiMoissonneurClient: client +} From 1daaff4648b0fa69c22f1c3a78a4e6d1ffe7fff2 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 4 Jan 2024 15:55:20 +0100 Subject: [PATCH 07/13] fix: linter --- components/bal-widget/bal-widget-config-form.tsx | 1 - lib/bal-widget.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/components/bal-widget/bal-widget-config-form.tsx b/components/bal-widget/bal-widget-config-form.tsx index 9dce7b6..57b9552 100644 --- a/components/bal-widget/bal-widget-config-form.tsx +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ import React, { useState, useMemo } from "react"; import styled from "styled-components"; import { Input } from "@codegouvfr/react-dsfr/Input"; diff --git a/lib/bal-widget.ts b/lib/bal-widget.ts index 877fd97..25fd214 100644 --- a/lib/bal-widget.ts +++ b/lib/bal-widget.ts @@ -1,6 +1,5 @@ import { BALWidgetConfig } from "../types/bal-widget"; -// eslint-disable-next-line @typescript-eslint/naming-convention const NEXT_PUBLIC_BAL_ADMIN_URL = process.env.NEXT_PUBLIC_BAL_ADMIN_URL || "http://localhost:3000"; From 52fec6b90b3cbc6661c9e847ee0b32894089e82e Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Fri, 5 Jan 2024 18:51:30 +0100 Subject: [PATCH 08/13] feat: add outdated clients and sources in form --- .../bal-widget/bal-widget-config-form.tsx | 82 ++++++++++++++++++- server/lib/bal-widget/controller.js | 11 --- server/lib/bal-widget/service.js | 23 ------ server/lib/mailer/email.templates.js | 2 +- types/bal-widget.ts | 4 + types/moissoneur.ts | 50 +++++++---- 6 files changed, 116 insertions(+), 56 deletions(-) diff --git a/components/bal-widget/bal-widget-config-form.tsx b/components/bal-widget/bal-widget-config-form.tsx index 57b9552..b67e27a 100644 --- a/components/bal-widget/bal-widget-config-form.tsx +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import styled from "styled-components"; import { Input } from "@codegouvfr/react-dsfr/Input"; import { Button } from "@codegouvfr/react-dsfr/Button"; @@ -7,6 +7,11 @@ import { Checkbox } from "@codegouvfr/react-dsfr/Checkbox"; import { BALWidgetConfig } from "../../types/bal-widget"; import { MultiStringInput } from "../multi-string-input"; import { MultiLinkInput } from "../multi-link-input"; +import { MultiSelectInput } from "../multi-select-input"; +import { ClientApiDepotType } from "types/api-depot"; +import { SourceMoissoneurType } from "types/moissoneur"; +import { getClients } from "@/lib/api-depot"; +import { getSources } from "@/lib/api-moissonneur-bal"; type BALWidgetConfigFormProps = { config: BALWidgetConfig; @@ -20,7 +25,7 @@ const StyledForm = styled.form` } section { - margin: 1rem 0; + margin: 1.5rem 0; } .form-controls { @@ -39,6 +44,10 @@ const defaultConfig: BALWidgetConfig = { hideWidget: false, showOnPages: [], }, + communes: { + outdatedApiDepotClients: [], + outdatedHarvestSources: [], + }, gitbook: { welcomeBlockTitle: "Ces articles pourraient vous aider", topArticles: [], @@ -53,13 +62,41 @@ export const BALWidgetConfigForm = ({ onSubmit, config: baseConfig, }: BALWidgetConfigFormProps) => { - const initialConfig = baseConfig ? { ...baseConfig } : { ...defaultConfig }; + const initialConfig = useMemo( + () => (baseConfig ? { ...baseConfig } : { ...defaultConfig }), + [baseConfig] + ); + const [formData, setFormData] = useState(initialConfig); + const [apiDepotClients, setApiDepotClients] = useState( + [] + ); + const [harvestSources, setHarvestSources] = useState( + [] + ); const canPublish = useMemo(() => { return JSON.stringify(formData) !== JSON.stringify(initialConfig); }, [formData, initialConfig]); + useEffect(() => { + async function fetchData() { + try { + const [clients, sources] = await Promise.all([ + getClients(), + getSources(), + ]); + + setApiDepotClients(clients); + setHarvestSources(sources); + } catch (error) { + console.log(error); + } + } + + fetchData(); + }, []); + const handleEdit = (section: keyof BALWidgetConfig, property: string) => (e: React.ChangeEvent) => { @@ -126,6 +163,45 @@ export const BALWidgetConfigForm = ({ } /> +
    +

    Aide aux communes

    + ({ + value: client._id, + label: client.nom, + }))} + placeholder="Sélectionner les clients API Dépôt caducs" + onChange={(value) => + setFormData((state) => ({ + ...state, + communes: { + ...state.communes, + outdatedApiDepotClients: value, + }, + })) + } + /> + ({ + value: source._id, + label: source.title, + }))} + placeholder="Sélectionner les sources moissonnées caduques" + onChange={(value) => + setFormData((state) => ({ + ...state, + communes: { + ...state.communes, + outdatedHarvestSources: value, + }, + })) + } + /> +

    Gitbook

    { } }); -BALWidgetRoutes.get("/commune/:code", async (req, res) => { - try { - const { code } = req.params; - const response = await BALWidgetService.getCommuneInfos(code); - res.json(response); - } catch (err) { - console.error(err); - res.status(500).json({ error: err.message }); - } -}); - module.exports = BALWidgetRoutes; diff --git a/server/lib/bal-widget/service.js b/server/lib/bal-widget/service.js index 9d6eb97..3d7b403 100644 --- a/server/lib/bal-widget/service.js +++ b/server/lib/bal-widget/service.js @@ -1,8 +1,4 @@ const mongoClient = require("../../utils/mongo-client"); -const {apiDepotClient} = require("../../proxy-api-depot"); -const {apiMoissonneurClient} = require("../../proxy-api-moissonneur-bal"); - -const NEXT_PUBLIC_API_MES_ADRESSES = process.env.NEXT_PUBLIC_API_MES_ADRESSES || 'https://api-bal.adresse.data.gouv.fr/v1' const collectionName = "bal-widget"; @@ -32,26 +28,7 @@ async function setConfig(payload) { return update; } -async function getCommuneInfos(code) { - const mesAdressesResponse = await fetch(`${NEXT_PUBLIC_API_MES_ADRESSES}/bases-locales/search?commune=${code}`) - const mesAdressesResponseData = await mesAdressesResponse.json() - const balsMesAdresses = mesAdressesResponseData.results.filter(({status}) => { - return status === 'published' || status === 'replaced' - }) - - const apiDepotRevisionsResponse = await apiDepotClient.get(`communes/${code}/revisions?status=all`) - - const apiMoissonneurRevisionsResponse = await apiMoissonneurClient.get(`communes/${code}/revisions`) - - return { - balsMesAdresses, - apiDepotRevisions: apiDepotRevisionsResponse.body, - apiMoissonneurRevisions: apiMoissonneurRevisionsResponse.body, - }; -} - module.exports = { getConfig, setConfig, - getCommuneInfos, }; diff --git a/server/lib/mailer/email.templates.js b/server/lib/mailer/email.templates.js index 52392b1..46eeb07 100644 --- a/server/lib/mailer/email.templates.js +++ b/server/lib/mailer/email.templates.js @@ -10,7 +10,7 @@ module.exports = { return { from: process.env.SMTP_FROM || "adresse@data.gouv.fr", to: "adresse@data.gouv.fr", - subject, + subject: `BAL Widget - ${subject}`, text: `Bonjour,\n\nVous avez reçu un nouveau message via le formulaire de contact de BAL Widget.\n\nNom: ${lastName}\nPrénom: ${firstName}\nEmail: ${email}\n\nMessage:\n${message}\n\nBonne journée,\n\nL’équipe BAL`, html: `

    Bonjour,

    Vous avez reçu un nouveau message via le formulaire de contact de la BAL Widget.

    Nom: ${lastName}

    Prénom: ${firstName}

    Email: ${email}

    Message:

    ${message}

    Bonne journée,

    L’équipe BAL

    ` }; diff --git a/types/bal-widget.ts b/types/bal-widget.ts index 472a3d4..6685df3 100644 --- a/types/bal-widget.ts +++ b/types/bal-widget.ts @@ -9,6 +9,10 @@ export interface BALWidgetConfig { hideWidget: boolean; showOnPages: string[]; }; + communes: { + outdatedApiDepotClients: string[]; + outdatedHarvestSources: string[]; + }; gitbook: { welcomeBlockTitle: string; topArticles: BALWidgetLink[]; diff --git a/types/moissoneur.ts b/types/moissoneur.ts index dd85cbf..9646d87 100644 --- a/types/moissoneur.ts +++ b/types/moissoneur.ts @@ -11,6 +11,38 @@ export enum UpdateStatusEnum { UPDATED = "updated", } +export type SourceMoissoneurType = { + _id: string; + _updated: string; + _created: string; + _deleted: boolean; + url: string; + title: string; + type: string; + page: string; + organization: { + name: string; + page: string; + description: string; + }; + model: string; + license: string; + harvesting: { + lastHarvest: string; + lastHarvestStatus: string; + lastHarvestError: string; + }; + enabled: boolean; + description: string; + data: { + fileId: string; + harvestDate: string; + nbRows: number; + nbRowsWithErrors: number; + uniqueErrors: string[]; + }; +}; + export type PublicationMoissoneurType = { status: RevisionStatusMoissoneurEnum; publishedRevisionId?: string | undefined; @@ -34,21 +66,3 @@ export type RevisionMoissoneurType = { publication?: PublicationMoissoneurType; current?: boolean; }; - -export type SourceMoissoneurType = { - _id: string; - _created: Date; - _deleted: boolean; - converter: any; - data: any; - description: string; - enabled: boolean; - harvesting: any; - license: string; - model: string; - organization: any; - page: string; - title: string; - type: string; - url: string; -}; From 2e4b04e9199d5053add9a93ec040dfec5dfe6081 Mon Sep 17 00:00:00 2001 From: Fufeck Date: Wed, 17 Jan 2024 10:45:47 +0100 Subject: [PATCH 09/13] fix : throw error when config is empty --- components/moissonneur-bal/moissonneur-source-item.tsx | 2 +- server/lib/bal-widget/service.js | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/components/moissonneur-bal/moissonneur-source-item.tsx b/components/moissonneur-bal/moissonneur-source-item.tsx index c34d7aa..534b323 100644 --- a/components/moissonneur-bal/moissonneur-source-item.tsx +++ b/components/moissonneur-bal/moissonneur-source-item.tsx @@ -11,7 +11,7 @@ interface MoissoneurSourceItemProps { model: string; type: string; _deleted: boolean; - _updated?: Date; + _updated?: string; } const MoissoneurSourceItem = ({ diff --git a/server/lib/bal-widget/service.js b/server/lib/bal-widget/service.js index 3d7b403..7b1b792 100644 --- a/server/lib/bal-widget/service.js +++ b/server/lib/bal-widget/service.js @@ -9,9 +9,13 @@ async function getConfig(query = {}) { type: draft ? "draft" : "published", }; - const {_id, _created, _updated, ...record} = await mongoClient.findOne(collectionName, mongoQuery); + try { + const {_id, _created, _updated, ...record} = await mongoClient.findOne(collectionName, mongoQuery); - return record; + return record; + } catch (error) { + return null + } } async function setConfig(payload) { From 3780b3f16c80cf62b2f194d8c50dd81d7753f0d4 Mon Sep 17 00:00:00 2001 From: Guillaume Fay Date: Thu, 1 Feb 2024 11:17:34 +0100 Subject: [PATCH 10/13] add placehoders to multi inputs --- components/bal-widget/bal-widget-config-form.tsx | 6 ++++++ components/multi-link-input.tsx | 6 ++++-- components/multi-string-input.tsx | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/components/bal-widget/bal-widget-config-form.tsx b/components/bal-widget/bal-widget-config-form.tsx index b67e27a..afe1676 100644 --- a/components/bal-widget/bal-widget-config-form.tsx +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -161,6 +161,7 @@ export const BALWidgetConfigForm = ({ onChange={(value) => handleEdit("global", "showOnPages")({ target: { value } } as any) } + placeholder="Path de la page autorisée (/programme-bal)" />
    @@ -214,6 +215,10 @@ export const BALWidgetConfigForm = ({ /> handleEdit("gitbook", "topArticles")({ target: { value } } as any) @@ -232,6 +237,7 @@ export const BALWidgetConfigForm = ({ /> handleEdit("contactUs", "subjects")({ target: { value } } as any) diff --git a/components/multi-link-input.tsx b/components/multi-link-input.tsx index ec3ea2a..dd26e7a 100644 --- a/components/multi-link-input.tsx +++ b/components/multi-link-input.tsx @@ -6,6 +6,7 @@ import { BALWidgetLink } from "../types/bal-widget"; type MultiLinkInputProps = { label: string; value: BALWidgetLink[]; + placeholders?: string[]; onChange: (value: BALWidgetLink[]) => void; }; @@ -25,6 +26,7 @@ export const MultiLinkInput = ({ label, value, onChange, + placeholders = [], }: MultiLinkInputProps) => { const addValue = () => { onChange([...value, { label: "", url: "" }]); @@ -63,7 +65,7 @@ export const MultiLinkInput = ({ type="text" value={label} onChange={(e) => handleChange(index, "label", e.target.value)} - placeholder="Comment puis-je obtenir une adresse ?" + placeholder={placeholders[0]} /> handleChange(index, "url", e.target.value)} - placeholder="https://example.com" + placeholder={placeholders[1]} />