diff --git a/.env.sample b/.env.sample index a69237e..730e803 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= +HCAPTCHA_SECRET_KEY= \ No newline at end of file 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..83b7771 --- /dev/null +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -0,0 +1,258 @@ +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"; +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; + onSubmit: (data: BALWidgetConfig) => Promise; +}; + +const StyledForm = styled.form` + h3, + h4 { + margin-bottom: 1rem; + } + + section { + margin: 1.5rem 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: [], + }, + communes: { + outdatedApiDepotClients: [], + outdatedHarvestSources: [], + }, + gitbook: { + welcomeBlockTitle: "Ces articles pourraient vous aider", + topArticles: [], + }, + contactUs: { + welcomeBlockTitle: "Nous contacter", + subjects: [], + }, +}; + +export const BALWidgetConfigForm = ({ + onSubmit, + config: baseConfig, +}: BALWidgetConfigFormProps) => { + 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 = await getClients(); + setApiDepotClients(clients); + } catch (error) { + console.log(error); + } + try { + const sources = await getSources(); + setHarvestSources(sources); + } catch (error) { + console.log(error); + } + } + + fetchData(); + }, []); + + 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) + } + placeholder="Path de la page autorisée (/programme-bal)" + /> +
+
+

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

+ + + 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/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/components/multi-link-input.tsx b/components/multi-link-input.tsx new file mode 100644 index 0000000..dd26e7a --- /dev/null +++ b/components/multi-link-input.tsx @@ -0,0 +1,88 @@ +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[]; + placeholders?: string[]; + 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, + placeholders = [], +}: 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={placeholders[0]} + /> + handleChange(index, "url", e.target.value)} + placeholder={placeholders[1]} + /> +
    + ))} +
    + ); +}; diff --git a/components/multi-string-input.tsx b/components/multi-string-input.tsx new file mode 100644 index 0000000..7b5d68e --- /dev/null +++ b/components/multi-string-input.tsx @@ -0,0 +1,73 @@ +import React from "react"; +import { Button } from "@codegouvfr/react-dsfr/Button"; +import styled from "styled-components"; + +type MultiStringInputProps = { + label: string; + value: string[]; + placeholder?: 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, + placeholder, +}: 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={placeholder} + /> +
    + ))} +
    + ); +}; diff --git a/lib/bal-widget.ts b/lib/bal-widget.ts new file mode 100644 index 0000000..25fd214 --- /dev/null +++ b/lib/bal-widget.ts @@ -0,0 +1,37 @@ +import { BALWidgetConfig } from "../types/bal-widget"; + +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/package.json b/package.json index b98a6df..78eedc1 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-dropzone": "^14.2.2", "react-js-pagination": "^3.0.3", "react-toastify": "^9.1.3", + "sanitize-html": "^2.11.0", "sharp": "^0.31.2", "styled-components": "^5.3.9" }, 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..bc5725d 100644 --- a/server/index.js +++ b/server/index.js @@ -38,6 +38,7 @@ 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 diff --git a/server/lib/bal-widget/controller.js b/server/lib/bal-widget/controller.js new file mode 100644 index 0000000..4a58328 --- /dev/null +++ b/server/lib/bal-widget/controller.js @@ -0,0 +1,42 @@ +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(); + +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 }); + } +}); + +BALWidgetRoutes.post("/send-mail", async (req, res) => { + try { + await MailerService.sendFormContactMail(req.body); + res.json(true); + } 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..7b1b792 --- /dev/null +++ b/server/lib/bal-widget/service.js @@ -0,0 +1,38 @@ +const mongoClient = require("../../utils/mongo-client"); + +const collectionName = "bal-widget"; + +async function getConfig(query = {}) { + const { draft } = query; + + const mongoQuery = { + type: draft ? "draft" : "published", + }; + + try { + const {_id, _created, _updated, ...record} = await mongoClient.findOne(collectionName, mongoQuery); + + return record; + } catch (error) { + return null + } +} + +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/lib/mailer/email.templates.js b/server/lib/mailer/email.templates.js index 3f4d32e..e97dcb9 100644 --- a/server/lib/mailer/email.templates.js +++ b/server/lib/mailer/email.templates.js @@ -1,9 +1,20 @@ +const sanitizeHtml = require('sanitize-html'); + 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: `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: sanitizeHtml(`

    Bonjour,

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

    ${lastName ? `

    Nom: ${lastName}

    ` : ''}${firstName ? `

    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..c2eaa6f --- /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" }, + captchaToken: { isRequired: true, type: "string" }, +}; + +module.exports = { + mailSchema, +}; diff --git a/server/lib/mailer/service.js b/server/lib/mailer/service.js index d81e482..b840f73 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,43 @@ async function sendMail(templateKey) { return true } +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 captcha est invalide') + } + + return json.success +} + +async function sendFormContactMail(payload) { + const validatedPayload = validPayload(payload, mailSchema) + const {captchaToken, ...emailData } = validatedPayload + + await checkCaptcha(captchaToken) + + 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 } 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) } diff --git a/server/proxy-api-depot.js b/server/proxy-api-depot.js index 4d4e78c..c9f764b 100644 --- a/server/proxy-api-depot.js +++ b/server/proxy-api-depot.js @@ -157,4 +157,3 @@ app.get('/stats/publications', w(getStatPublications)) app.get('/communes/:codeCommune/revisions', w(getAllRevisionsByCommune)) module.exports = app - 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..6685df3 --- /dev/null +++ b/types/bal-widget.ts @@ -0,0 +1,24 @@ +export interface BALWidgetLink { + label: string; + url: string; +} + +export interface BALWidgetConfig { + global: { + title: string; + hideWidget: boolean; + showOnPages: string[]; + }; + communes: { + outdatedApiDepotClients: string[]; + outdatedHarvestSources: string[]; + }; + gitbook: { + welcomeBlockTitle: string; + topArticles: BALWidgetLink[]; + }; + contactUs: { + welcomeBlockTitle: string; + subjects: string[]; + }; +} 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; -}; diff --git a/yarn.lock b/yarn.lock index 9eb92b7..5ab812e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1329,6 +1329,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + defer-to-connect@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" @@ -1401,6 +1406,36 @@ dom-helpers@^5.0.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dotenv@^16.0.3: version "16.0.3" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" @@ -1444,6 +1479,11 @@ enhanced-resolve@^5.7.0: graceful-fs "^4.2.4" tapable "^2.2.0" +entities@^4.2.0, entities@^4.4.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -2184,6 +2224,16 @@ hoist-non-react-statics@^3.0.0, hoist-non-react-statics@^3.3.1: dependencies: react-is "^16.7.0" +htmlparser2@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" + integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + entities "^4.4.0" + http-cache-semantics@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" @@ -2395,6 +2445,11 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -2739,6 +2794,11 @@ nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + napi-build-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" @@ -2997,6 +3057,11 @@ parse-json@^5.0.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -3056,6 +3121,15 @@ postcss@8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.3.11: + version "8.4.33" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.0.2" + preact-render-to-string@^5.1.19: version "5.2.6" resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604" @@ -3404,6 +3478,18 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sanitize-html@^2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.11.0.tgz#9a6434ee8fcaeddc740d8ae7cd5dd71d3981f8f6" + integrity sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA== + dependencies: + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^8.0.0" + is-plain-object "^5.0.0" + parse-srcset "^1.0.2" + postcss "^8.3.11" + saslprep@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"