diff --git a/components/bal-widget/bal-widget-config-form.tsx b/components/bal-widget/bal-widget-config-form.tsx index 7b5b06b..7dd2016 100644 --- a/components/bal-widget/bal-widget-config-form.tsx +++ b/components/bal-widget/bal-widget-config-form.tsx @@ -200,7 +200,7 @@ export const BALWidgetConfigForm = ({ label="Sources moissonnées caduques" value={formData.communes?.outdatedHarvestSources || []} options={harvestSources.map((source) => ({ - value: source._id, + value: source.id, label: source.title, }))} placeholder="Sélectionner les sources moissonnées caduques" diff --git a/components/communes/revisions-item-moissoneur.tsx b/components/communes/revisions-item-moissoneur.tsx index 0a8728a..5bb177c 100644 --- a/components/communes/revisions-item-moissoneur.tsx +++ b/components/communes/revisions-item-moissoneur.tsx @@ -1,40 +1,45 @@ -import Link from 'next/link' +import Link from "next/link"; -import type {RevisionMoissoneurType} from '../../types/moissoneur' -import UpdateStatusBadge from '@/components/update-status-badge' -import {RevisionPublication} from '@/components/revision-publication' -import MongoId from '@/components/mongo-id' -import Tooltip from '@/components/tooltip' +import type { RevisionMoissoneurType } from "../../types/moissoneur"; +import UpdateStatusBadge from "@/components/update-status-badge"; +import { RevisionPublication } from "@/components/revision-publication"; +import MongoId from "@/components/mongo-id"; +import Tooltip from "@/components/tooltip"; -export const RevisionItemMoissoneur = ( - {_id, sourceId, nbRows, nbRowsWithErrors, updateStatus, updateRejectionReason, publication}: RevisionMoissoneurType, -) => ( - - - +export const RevisionItemMoissoneur = ({ + id, + sourceId, + validation, + updateStatus, + updateRejectionReason, + publication, +}: RevisionMoissoneurType) => ( + + + - + - + {sourceId} - - {nbRows} - - - {nbRowsWithErrors} - + {validation.nbRows} + {validation.nbRowsWithErrors} - + - + -) +); diff --git a/components/moissonneur-bal/harvest-item.tsx b/components/moissonneur-bal/harvest-item.tsx index af944b3..971b978 100644 --- a/components/moissonneur-bal/harvest-item.tsx +++ b/components/moissonneur-bal/harvest-item.tsx @@ -1,70 +1,89 @@ -import Router from 'next/router' +import Router from "next/router"; -import Button from '@codegouvfr/react-dsfr/Button' -import Badge from '@codegouvfr/react-dsfr/Badge' +import Button from "@codegouvfr/react-dsfr/Button"; +import Badge from "@codegouvfr/react-dsfr/Badge"; -import Tooltip from '../tooltip' -import {formatDate} from '@/lib/util/date' -import {getFile} from '@/lib/api-moissonneur-bal' +import Tooltip from "../tooltip"; +import { formatDate } from "@/lib/util/date"; +import { getFile } from "@/lib/api-moissonneur-bal"; -import UpdateStatusBadge from '@/components/update-status-badge' -import MongoId from '@/components/mongo-id' -import { HarvestMoissonneurType, HarvestStatus } from 'types/moissoneur' +import UpdateStatusBadge from "@/components/update-status-badge"; +import MongoId from "@/components/mongo-id"; +import { HarvestMoissonneurType, HarvestStatus } from "types/moissoneur"; interface StatusBadgeProps { status: HarvestStatus; error: string; } -const StatusBadge = ({status, error}: StatusBadgeProps) => { +const StatusBadge = ({ status, error }: StatusBadgeProps) => { if (status === HarvestStatus.ACTIVE) { - return En cours… + return ( + + En cours… + + ); } if (status === HarvestStatus.FAILED) { return ( - Échec + + Échec + - ) + ); } if (status === HarvestStatus.COMPLETED) { - return Terminé + return ( + + Terminé + + ); } -} +}; -const HarvestItem = ({_id, startedAt, finishedAt, status, error, updateStatus, updateRejectionReason, fileId}: HarvestMoissonneurType) => { +const HarvestItem = ({ + id, + startedAt, + finishedAt, + status, + error, + updateStatus, + updateRejectionReason, + fileId, +}: HarvestMoissonneurType) => { const downloadFile = async () => { - const file = await getFile(fileId) - Router.push(file.url) - } + const file = await getFile(fileId); + Router.push(file.url); + }; return ( - - + + - + {formatDate(startedAt)} - - {finishedAt ? formatDate(finishedAt) : '…'} + + {finishedAt ? formatDate(finishedAt) : "…"} - + - + - + {fileId && ( @@ -242,7 +240,7 @@ const MoissoneurSource = ({ {harvests.map((harvest) => ( - + ))} @@ -314,10 +312,10 @@ const MoissoneurSource = ({ {revisions.map((revision) => ( - onForcePublishRevision(revision._id) + onForcePublishRevision(revision.id) } isForcePublishRevisionLoading={ forcePublishRevisionStatus === "loading" diff --git a/server/lib/mailer/service.js b/server/lib/mailer/service.js index 00be886..67b7b55 100644 --- a/server/lib/mailer/service.js +++ b/server/lib/mailer/service.js @@ -1,138 +1,151 @@ -const nodemailer = require('nodemailer') -const templates = require('./email.templates') -const {mailSchema, signalementOtherMailSchema, signalementMissingNumberMailSchema, signalementMissingStreetMailSchema} = require('./schemas') -const {validPayload} = require('../../utils/payload') -const {getCommuneEmail} = require('../../utils/api-annuaire') - -const apiDepotBaseUrl = process.env.NEXT_PUBLIC_API_DEPOT_URL || "https://plateforme-bal.adresse.data.gouv.fr/api-depot" +const nodemailer = require("nodemailer"); +const templates = require("./email.templates"); +const { + mailSchema, + signalementOtherMailSchema, + signalementMissingNumberMailSchema, + signalementMissingStreetMailSchema, +} = require("./schemas"); +const { validPayload } = require("../../utils/payload"); +const { getCommuneEmail } = require("../../utils/api-annuaire"); + +const apiDepotBaseUrl = + process.env.NEXT_PUBLIC_API_DEPOT_URL || + "https://plateforme-bal.adresse.data.gouv.fr/api-depot"; function createTransport() { // Use mailhog in development - if (process.env.NODE_ENV !== 'production') { + if (process.env.NODE_ENV !== "production") { return nodemailer.createTransport({ - host: 'localhost', - port: 587 - }) + host: "localhost", + port: 587, + }); } if (!process.env.SMTP_HOST) { - throw new Error('SMTP_HOST must be provided in production mode') + throw new Error("SMTP_HOST must be provided in production mode"); } return nodemailer.createTransport({ host: process.env.SMTP_HOST, port: process.env.SMTP_PORT || 587, - secure: process.env.SMTP_SECURE === 'YES', + secure: process.env.SMTP_SECURE === "YES", auth: { user: process.env.SMTP_USER, - pass: process.env.SMTP_PASS - } - }) + pass: process.env.SMTP_PASS, + }, + }); } -const transport = createTransport() +const transport = createTransport(); async function sendTemplateMail(templateKey) { - const template = templates[templateKey] + const template = templates[templateKey]; if (!template) { - throw new Error(`Le template ${templateKey} n'existe pas`) + throw new Error(`Le template ${templateKey} n'existe pas`); } - const response = await transport.sendMail(template) + const response = await transport.sendMail(template); if (!response) { - throw new Error('Une erreur est survenue lors de l\'envoi de l\'email') + throw new Error("Une erreur est survenue lors de l'envoi de l'email"); } - return true + return true; } async function checkCaptcha(captchaToken) { const response = await fetch(`https://api.hcaptcha.com/siteverify`, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/x-www-form-urlencoded' + "Content-Type": "application/x-www-form-urlencoded", }, body: `response=${captchaToken}&secret=${process.env.HCAPTCHA_SECRET_KEY}`, - }) + }); - const json = await response.json() + const json = await response.json(); if (!json.success) { - throw new Error('Le captcha est invalide') + throw new Error("Le captcha est invalide"); } - return json.success + return json.success; } async function sendFormContactMail(payload) { - const validatedPayload = validPayload(payload, mailSchema) - const {captchaToken, ...emailData } = validatedPayload + const validatedPayload = validPayload(payload, mailSchema); + const { captchaToken, ...emailData } = validatedPayload; - await checkCaptcha(captchaToken) + await checkCaptcha(captchaToken); - const contactTemplate = templates.contact(emailData) + const contactTemplate = templates.contact(emailData); - const response = await transport.sendMail(contactTemplate) + const response = await transport.sendMail(contactTemplate); if (!response) { - throw new Error('Une erreur est survenue lors de l\'envoi de l\'email') + throw new Error("Une erreur est survenue lors de l'envoi de l'email"); } - return true + return true; } async function sendSignalementToCommune(payload) { - let schema + let schema; switch (payload.subject) { - case 'Adresse non répertoriée': - schema = signalementMissingNumberMailSchema - break - case 'Voie non répertoriée': - schema = signalementMissingStreetMailSchema - break + case "Adresse non répertoriée": + schema = signalementMissingNumberMailSchema; + break; + case "Voie non répertoriée": + schema = signalementMissingStreetMailSchema; + break; default: - schema = signalementOtherMailSchema + schema = signalementOtherMailSchema; } - const validatedPayload = validPayload(payload, schema) - const {captchaToken, ...emailData } = validatedPayload + const validatedPayload = validPayload(payload, schema); + const { captchaToken, ...emailData } = validatedPayload; - await checkCaptcha(captchaToken) + await checkCaptcha(captchaToken); - const communeEmail = await getCommuneEmail(emailData.city) + const communeEmail = await getCommuneEmail(emailData.city); - const currentRevisionResponse = await fetch(`${apiDepotBaseUrl}/communes/${emailData.city}/current-revision`) - const currentRevision = await currentRevisionResponse.json() + const currentRevisionResponse = await fetch( + `${apiDepotBaseUrl}/communes/${emailData.city}/current-revision` + ); + const currentRevision = await currentRevisionResponse.json(); const publication = { client: currentRevision?.client?.nom, - } - if (currentRevision?.client?.nom === 'Mes Adresses') { - publication.balId = currentRevision?.context?.extras?.balId - } else if (currentRevision?.client?.nom === 'Moissonneur BAL') { - const sourceId = currentRevision?.context?.extras?.sourceId + }; + if (currentRevision?.client?.nom === "Mes Adresses") { + publication.balId = currentRevision?.context?.extras?.balId; + } else if (currentRevision?.client?.nom === "Moissonneur BAL") { + const sourceId = currentRevision?.context?.extras?.sourceId; if (sourceId) { - const ids = sourceId.split('-') - const response = await fetch(`https://www.data.gouv.fr/api/1/datasets/${ids[1]}`) - const dataset = await response.json() - publication.organization = dataset?.organization?.name + const response = await fetch( + `https://www.data.gouv.fr/api/1/datasets/${sourceId}` + ); + const dataset = await response.json(); + publication.organization = dataset?.organization?.name; } } - const template = templates.signalementToCommune(emailData, communeEmail, publication) + const template = templates.signalementToCommune( + emailData, + communeEmail, + publication + ); - const response = await transport.sendMail(template) + const response = await transport.sendMail(template); if (!response) { - throw new Error('Une erreur est survenue lors de l\'envoi de l\'email') + throw new Error("Une erreur est survenue lors de l'envoi de l'email"); } - return true + return true; } - module.exports = { sendTemplateMail, sendFormContactMail, - sendSignalementToCommune -} + sendSignalementToCommune, +}; diff --git a/server/migrations/24-10-14_change-outdatedHarvestSources.js b/server/migrations/24-10-14_change-outdatedHarvestSources.js new file mode 100644 index 0000000..43cd7fa --- /dev/null +++ b/server/migrations/24-10-14_change-outdatedHarvestSources.js @@ -0,0 +1,32 @@ +const mongoClient = require("../utils/mongo-client"); + +const collectionName = "bal-widget"; + +async function main() { + await mongoClient.connect(); + + const config = await mongoClient.db.collection(collectionName).findOne(); + const outdatedHarvestSources = config?.communes?.outdatedHarvestSources || []; + const newOutdatedHarvestSources = []; + for (const sourceId of outdatedHarvestSources) { + const [, id] = sourceId.split(/-(.*)/s); + newOutdatedHarvestSources.push(id.substring(0, 24)); + } + + await mongoClient.db + .collection(collectionName) + .updateOne( + { _id: config._id }, + { $set: { "communes.outdatedHarvestSources": newOutdatedHarvestSources } } + ); + await mongoClient.disconnect(); +} + +main() + .catch((error) => { + console.error(error); + process.exit(1); + }) + .then(() => { + process.exit(0); + }); diff --git a/types/moissoneur.ts b/types/moissoneur.ts index 2dd613f..460cf66 100644 --- a/types/moissoneur.ts +++ b/types/moissoneur.ts @@ -29,32 +29,32 @@ export enum HarvestStatus { } export type HarvestMoissonneurType = { - _id: string; - startedAt: Date; - finishedAt: Date; + id: string; + sourceId: string; + fileId: string; + fileHash: string; + dataHash: string; status: HarvestStatus; - error: string; updateStatus: UpdateStatusEnum; updateRejectionReason: string; - fileId: string; + error: string; + startedAt: Date; + finishedAt: Date; }; export type SourceMoissoneurType = { - _id: string; - url: string; - title: string; - page: string; + id: string; organizationId: string; + title: string; + url: string; license: string; enabled: boolean; description: string; - harvesting: { - lastHarvest: Date; - harvestingSince: Date | null; - }; - _updated: string; - _created: string; - _deleted: boolean; + lastHarvest: Date; + harvestingSince: Date | null; + updatedAt?: Date; + createdAt?: Date; + deletedAt?: Date; }; export interface ExtendedSourceMoissoneurType extends SourceMoissoneurType { @@ -70,32 +70,35 @@ export type PublicationMoissoneurType = { currentSourceId?: string; }; +export type ValidationMoissonneurType = { + nbRows?: number; + nbRowsWithErrors?: number; + uniqueErrors?: string[]; +}; + export type RevisionMoissoneurType = { - _id: string; + id: string; sourceId?: string; - codeCommune?: string; harvestId?: string; - updateStatus?: UpdateStatusEnum; - updateRejectionReason?: string | undefined; fileId?: string; dataHash?: string; - nbRows?: number; - nbRowsWithErrors?: number; - uniqueErrors?: string[]; + codeCommune?: string; + updateStatus?: UpdateStatusEnum; + updateRejectionReason?: string | undefined; + validation?: ValidationMoissonneurType; publication?: PublicationMoissoneurType; - current?: boolean; - _created?: Date; + createdAt?: Date; }; export interface OrganizationMoissoneurType { - _id: string; + id?: string; name?: string; page?: string; logo?: string; perimeters?: PerimeterType[]; - _updated?: Date; - _created?: Date; - _deleted?: boolean; + updatedAt?: Date; + createdAt?: Date; + deletedAt?: Date; } export interface OrganizationBalAdminType extends OrganizationMoissoneurType {