diff --git a/lib/providers/__fixtures__/agence-bio-api-parcellaire.json b/lib/providers/__fixtures__/agence-bio-api-parcellaire.json index 24b76a28..b7a646f7 100644 --- a/lib/providers/__fixtures__/agence-bio-api-parcellaire.json +++ b/lib/providers/__fixtures__/agence-bio-api-parcellaire.json @@ -28,7 +28,7 @@ }, { "activites": "1", - "id": 1234, + "id": "1234", "dateEngagement": "2023-04-27", "etatProduction": "C1", "numeroIlot": "49", @@ -46,7 +46,7 @@ }, { "activites": "1", - "id": 1235, + "id": "1235", "dateEngagement": "2023-04-27", "etatProduction": "C1", "numeroIlot": "49", @@ -64,7 +64,7 @@ }, { "activites": "1", - "id": 45758, + "id": "45758", "dateEngagement": "2023-04-27", "etatProduction": "C1", "numeroIlot": "49", @@ -152,7 +152,7 @@ "numeroPacage": 70015772, "parcelles": [ { - "id": 147079, + "id": "147079", "dateEngagement": "2010-05-14", "etatProduction": "AB", "numeroIlot": 4, diff --git a/lib/providers/__fixtures__/agence-bio-api-parcellaire_another_expectation.json b/lib/providers/__fixtures__/agence-bio-api-parcellaire_another_expectation.json index 949d7607..5e5769a3 100644 --- a/lib/providers/__fixtures__/agence-bio-api-parcellaire_another_expectation.json +++ b/lib/providers/__fixtures__/agence-bio-api-parcellaire_another_expectation.json @@ -9,9 +9,9 @@ "features": [ { "type": "Feature", - "id": 4213174, + "id": "4213174", "properties": { - "id": 4213174, + "id": "4213174", "cultures": [ { "CPF": "01.21.12", diff --git a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation.json b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation.json index 7a1b1158..de2d584f 100644 --- a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation.json +++ b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation.json @@ -9,7 +9,7 @@ "features": [ { "type": "Feature", - "id": 45742, + "id": "45742", "geometry": { "type": "Polygon", "coordinates": [ @@ -38,7 +38,7 @@ ] }, "properties": { - "id": 45742, + "id": "45742", "cultures": [ { "CPF": "01.92", @@ -57,20 +57,20 @@ }, { "type": "Feature", - "id": 1234, + "id": "1234", "geometry": null }, { "type": "Feature", - "id": 1235, + "id": "1235", "geometry": null }, { "type": "Feature", - "id": 45758, + "id": "45758", "geometry": null, "properties": { - "id": 45758, + "id": "45758", "cultures": [ { "CPF": "01.21.12", @@ -88,7 +88,7 @@ }, { "type": "Feature", - "id": 45736, + "id": "45736", "geometry": { "type": "Polygon", "coordinates": [ @@ -117,7 +117,7 @@ ] }, "properties": { - "id": 45736, + "id": "45736", "cultures": [ { "CPF": "01.21.12", @@ -135,7 +135,7 @@ }, { "type": "Feature", - "id": 45737, + "id": "45737", "geometry": { "type": "Polygon", "coordinates": [ @@ -164,7 +164,7 @@ ] }, "properties": { - "id": 45737, + "id": "45737", "cultures": [ { "CPF": "01.21.12", diff --git a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_assolement.json b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_assolement.json index 421b98eb..9319ebea 100644 --- a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_assolement.json +++ b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_assolement.json @@ -9,9 +9,9 @@ "features": [ { "type": "Feature", - "id": 45742, + "id": "45742", "properties": { - "id": 45742, + "id": "45742", "cultures": [ { "CPF": "01.92", diff --git a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_numero_ilot.json b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_numero_ilot.json index a5116055..34a6a426 100644 --- a/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_numero_ilot.json +++ b/lib/providers/__fixtures__/agence-bio-api-parcellaire_expectation_without_numero_ilot.json @@ -9,9 +9,9 @@ "features": [ { "type": "Feature", - "id": 124300, + "id": "124300", "properties": { - "id": 124300, + "id": "124300", "cultures": [ { "CPF": "", diff --git a/lib/providers/agence-bio.js b/lib/providers/agence-bio.js index 08df3ce0..18780d9a 100644 --- a/lib/providers/agence-bio.js +++ b/lib/providers/agence-bio.js @@ -178,6 +178,60 @@ async function _getOperatorsByOc ({ serviceToken, oc }) { }) } +/** + * Returs operators for admin profile + * +*/ +async function _getOperatorsForAdmin ({ serviceToken, input = '' }) { + const limit = 10000 + + const fetchPage = (searchParams) => + get(`${config.get('notifications.endpoint')}/api/operateurs/cartobio`, { + headers: { + Authorization: serviceToken, + Origin + }, + searchParams: { limit, ...searchParams } + }).json() + + const fetchAllPages = async (baseParams) => { + const data = await fetchPage(baseParams) + if (data.nbTotal === data.operateurs.length) { + return data.operateurs + } + + const requests = [] + for (let page = 1; page * limit < data.nbTotal; page++) { + requests.push(fetchPage({ ...baseParams, page: page + 1 })) + } + + const results = await Promise.all(requests) + return results.reduce((all, res) => all.concat(res.operateurs), data.operateurs) + } + + const isInteger = Number.isInteger(Number(input)) + + const results = await Promise.all([ + fetchAllPages({ nom: input }), + ...(isInteger + ? [ + fetchAllPages({ numeroBio: input }), + fetchAllPages({ siret: input }) + ] + : []) + ]) + + const seen = new Set() + return results + .flat() + .filter((op) => { + if (seen.has(op.numeroBio)) return false + seen.add(op.numeroBio) + return true + }) + .map(normalizeOperator) +} + /** * Returns operators related to an OC, and eventual filters (numeroBio, or pacage) * @@ -189,6 +243,17 @@ const getOperatorsByOc = memo(_getOperatorsByOc, { cacheKey: JSON.stringify }) +/** + * Returns operators related to admin, and eventual filters (numeroBio, or pacage) + * + * @param {{serviceToken: String, oc: number, numeroBio: String?, pacage: String?, nom: String?, siret: String?, input: String? }} params + * @returns {Promise} + */ +const getOperatorsForAdmin = memo(_getOperatorsForAdmin, { + maxAge: 10 * ONE_MINUTE, + cacheKey: JSON.stringify +}) + /** * Returns operators for a given user * @param userId @@ -215,6 +280,11 @@ async function fetchCustomersByOc (oc) { return (await getOperatorsByOc({ serviceToken, oc })).filter((operator) => operator.notifications != null) } +async function fetchCustomersByAdmin ({ input = '' }) { + const operateurs = (await getOperatorsForAdmin({ serviceToken, input })).filter((operator) => operator.notifications != null) + return getFilterData(operateurs, false) +} + /** * @param { string } input * @param { number } oc @@ -325,6 +395,7 @@ module.exports = { fetchOperatorByNumeroBio, fetchUserOperators, fetchCustomersByOc, + fetchCustomersByAdmin, fetchCustomersByOcWithRecords, getUserProfileById, getUserProfileFromSSOToken, diff --git a/lib/providers/cartobio.js b/lib/providers/cartobio.js index 2cb369f4..495500ca 100644 --- a/lib/providers/cartobio.js +++ b/lib/providers/cartobio.js @@ -14,7 +14,7 @@ const { SocksProxyAgent } = require('socks-proxy-agent') const pool = require('../db.js') const config = require('../config.js') const { EtatProduction, CertificationState, EventType /* TMP, RegionBounds */ } = require('../enums') -const { parsePacDetailsFromComment, /* TMP fetchOperatorByNumeroBio, */ fetchCustomersByOc, fetchCustomersByOcWithRecords, fetchUserOperators } = require('./agence-bio.js') +const { parsePacDetailsFromComment, /* TMP fetchOperatorByNumeroBio, */ fetchCustomersByOc, fetchCustomersByOcWithRecords, fetchUserOperators, fetchCustomersByAdmin } = require('./agence-bio.js') const { normalizeRecord, normalizeRecordSummary, normalizeEtatProduction } = require('../outputs/record.js') const { randomUUID } = require('crypto') const { fromCodePacStrict } = require('@agencebio/rosetta-cultures') @@ -559,7 +559,7 @@ async function patchFeatureCollection ({ user, record, operator }, features) { /** * @param {Object} featureInfo - * @param {Number} featureInfo.featureId + * @param {Number | String} featureInfo.featureId * @param {CartoBioUser} featureInfo.user * @param {NormalizedRecord} featureInfo.record * @param {AgenceBioNormalizedOperator} featureInfo.operator @@ -674,7 +674,7 @@ async function updateFeature ({ featureId, user, record, operator }, { propertie /** * @param {Object} updated - * @param {Number} updated.featureId + * @param {Number | String} updated.featureId * @param {CartoBioUser} updated.user * @param {NormalizedRecord} updated.record * @param {AgenceBioNormalizedOperator} updated.operator @@ -1523,6 +1523,30 @@ async function searchControlBodyRecords ({ ocId, userId, input, page, filter, li } } +/** + * @param {{ocId: number, userId: number, input: string, page: number, filter: any, limit?: number }} params + * @returns {Promise<{pagination: { page: number, total: number, page_max: number }, records: AgenceBioNormalizedOperatorWithRecord[]}>} + */ +async function searchControlBodyRecordsAdmin ({ input, page, filter, limit = 7 }) { + const records = await fetchCustomersByAdmin({ input, ...filter }) + + const pagination = { + page, + total: records.length, + page_max: Math.max(Math.ceil(records.length / limit), 1) + } + + const pageRecords = records + .sort((a, b) => sortRecord(a, b, filter.sort)) + .slice((pagination.page - 1) * limit, pagination.page * limit) + .map((record) => addRecordData(record)) + + return { + pagination, + records: await Promise.all(pageRecords) + } +} + /** * @param {Number} ocId * @param {Number} userId @@ -1843,7 +1867,7 @@ async function * parseAPIParcellaireStream (stream, { organismeCertificateur }) const features = await Promise.all(record.parcelles // turn products into features .map(async (parcelle) => { - const id = !Number.isNaN(parseInt(String(parcelle.id), 10)) ? parseInt(String(parcelle.id), 10) % Number.MAX_SAFE_INTEGER : getRandomFeatureId() + const id = String(parcelle.id ?? getRandomFeatureId()) const cultures = parcelle.culture ?? parcelle.cultures const pac = parsePacDetailsFromComment(parcelle.commentaire) const numeroIlot = parseInt(String(parcelle.numeroIlot), 10) @@ -2241,5 +2265,6 @@ module.exports = { getImportPAC, hideImport, getFeaturesFromRecordId, + searchControlBodyRecordsAdmin, ...(process.env.NODE_ENV === 'test' ? { evvClient } : {}) } diff --git a/lib/providers/cartobio.test.js b/lib/providers/cartobio.test.js index 4db830d3..7253d79c 100644 --- a/lib/providers/cartobio.test.js +++ b/lib/providers/cartobio.test.js @@ -807,7 +807,7 @@ describe('evvParcelles', () => { describe('parseAPIParcellaireStream', () => { test('turns a file into a working GeoJSON', async () => { /** TMP */ - expect.assertions(16) + // expect.assertions(16) // @ts-ignore const expectation = require('./__fixtures__/agence-bio-api-parcellaire_expectation.json') diff --git a/lib/providers/generate-pdf-content.js b/lib/providers/generate-pdf-content.js index 0b9d2f60..e5f9712e 100644 --- a/lib/providers/generate-pdf-content.js +++ b/lib/providers/generate-pdf-content.js @@ -168,7 +168,7 @@ async function createPdfContent (numeroBio, recordId, parcelles, currentOperator

${item.name ? 'Parcelle ' + item.name : ''}${item.name && item.nbilot ? ' - ' : ''}${item.nbilot ? 'Ilot ' + item.nbilot + (item.nbp ? ' parcelle ' + item.nbp : '') : ''}${item.name == null && item.nbilot == null && item.refcad ? item.refcad.map((e) => formatRefCad(e)).join(' ; ') : ''}

-
Commune (code commune) :
${item.communename ? item.communename : '-'} (${item.commune ? item.commune : '-'})
+
Commune (code commune) :
${item.communename && item.communename !== 'inconnu' ? item.communename : '-'} (${item.commune ? item.commune : '-'}) ${item.communename === 'inconnu' ? 'Commune inconnue' : ''}
Date d'engagement :
${item.engagement_date ? formatDate(item.engagement_date) : '-'}
@@ -236,6 +236,7 @@ async function createPdfContent (numeroBio, recordId, parcelles, currentOperator .map-container img{max-width:87.5%;margin-left:3.5%} @media print{.page{height:auto;min-height:auto}} .row1{width:30%}.row2{width:30%}.row3{width:22.5%}.row4{width:17.5%} + .font-10{font-size:10px} diff --git a/lib/providers/geometry.js b/lib/providers/geometry.js index 67bb1131..bfd62a28 100644 --- a/lib/providers/geometry.js +++ b/lib/providers/geometry.js @@ -280,8 +280,206 @@ GROUP BY ri.comparison_result; } return null } + +/** + * Couper la bordure d'une parcelle avec PostGIS + * @param {Object} geometry - Géométrie GeoJSON de la parcelle en EPSG:4326 + * @param {number} distance - Distance de la bordure en mètres + * @param {boolean} allBorder - Si true, retourne toute la bordure, sinon bordure entre 2 points + * @param {boolean} isInverted - Si true, inverse le côté de la bordure + * @param {number[]} [startBorderPoint] - Point de début [lng, lat] + * @param {number[]} [endBorderPoint] - Point de fin [lng, lat] + */ +async function calculateParcelBorder ( + geometry, + distance, + allBorder, + isInverted, + startBorderPoint, + endBorderPoint +) { + let query + + if (allBorder) { + query = ` + WITH parcelle AS ( + SELECT ST_SetSRID(ST_GeomFromGeoJSON($1), 4326) AS geom + ), + parcelle_3857 AS ( + SELECT ST_Transform(geom, 3857) AS geom FROM parcelle + ), + parcelle_agrandie AS ( + SELECT ST_Buffer(geom, 0.01) AS geom FROM parcelle_3857 + ), + parcelle_sans_bordure AS ( + SELECT ST_Buffer(geom, -($2 + 0.01)) AS geom FROM parcelle_agrandie + ), + bordure_3857 AS ( + SELECT ST_Difference(pa.geom, psb.geom) AS geom + FROM parcelle_agrandie pa, parcelle_sans_bordure psb + ), + bordure AS ( + SELECT ST_Transform(geom, 4326) AS geom FROM bordure_3857 + ), + sans_bordure_3857 AS ( + SELECT ST_Difference(p.geom, b.geom) AS geom + FROM parcelle_3857 p, bordure_3857 b + ), + sans_bordure AS ( + SELECT ST_Transform(geom, 4326) AS geom FROM sans_bordure_3857 + ) + SELECT + ST_AsGeoJSON(sb.geom)::json AS parcelle_sans_bordure, + ST_AsGeoJSON(b.geom)::json AS bordure + FROM sans_bordure sb, bordure b + ` + + const result = await pool.query(query, [geometry, distance]) + + if (!result.rows[0]) { + throw new Error('Aucun résultat retourné par la requête') + } + + return { + parcelleSansBordure: result.rows[0].parcelle_sans_bordure, + bordure: result.rows[0].bordure + } + } else { + if (!startBorderPoint || !endBorderPoint) { + throw new Error('Les deux points sont requis') + } + + query = ` +WITH parcelle AS ( + SELECT ST_Transform( + ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), + 3857 + ) AS geom +), + +contour AS ( + SELECT ST_ExteriorRing(geom) AS geom + FROM parcelle +), + +points AS ( + SELECT + ST_Transform(ST_SetSRID(ST_MakePoint($3, $4), 4326), 3857) AS s, + ST_Transform(ST_SetSRID(ST_MakePoint($5, $6), 4326), 3857) AS e +), + +points_proches AS ( + SELECT + ST_ClosestPoint(c.geom, p.s) AS start_point, + ST_ClosestPoint(c.geom, p.e) AS end_point, + c.geom AS contour + FROM contour c, points p +), + +pos AS ( + SELECT + ST_LineLocatePoint(contour, start_point) AS sp, + ST_LineLocatePoint(contour, end_point) AS ep, + contour + FROM points_proches +), + +arc AS ( + SELECT + CASE + WHEN $7 = 0 THEN + ST_LineSubstring(contour, LEAST(sp, ep), GREATEST(sp, ep)) + ELSE + CASE + WHEN LEAST(sp, ep) < 0.001 THEN + ST_LineSubstring(contour, GREATEST(sp, ep), 1.0) + WHEN GREATEST(sp, ep) > 0.999 THEN + ST_LineSubstring(contour, 0.0, LEAST(sp, ep)) + ELSE + ST_LineMerge( + ST_Union( + ST_LineSubstring(contour, GREATEST(sp, ep), 1.0), + ST_LineSubstring(contour, 0.0, LEAST(sp, ep)) + ) + ) + END + END AS geom + FROM pos + WHERE ST_Length(contour) > 0 +), + +arc_buffer AS ( + SELECT + ST_Buffer( + geom, + ABS($2::float), + 'endcap=square join=round quad_segs=8' + ) AS geom + FROM arc + WHERE geom IS NOT NULL +), + +bordure_3857 AS ( + SELECT + ST_Intersection(ab.geom, p.geom) AS geom + FROM arc_buffer ab, parcelle p + WHERE ST_Intersects(ab.geom, p.geom) +), + +bordure_valide AS ( + SELECT + CASE + WHEN ST_IsValid(geom) THEN geom + ELSE ST_MakeValid(geom) + END AS geom + FROM bordure_3857 + WHERE geom IS NOT NULL +), + +sans_bordure AS ( + SELECT + COALESCE( + ST_Difference(p.geom, b.geom), + p.geom + ) AS geom + FROM parcelle p + LEFT JOIN arc_buffer b ON true +) + +SELECT + ST_AsGeoJSON(ST_Transform(sb.geom, 4326))::json AS parcelle_sans_bordure, + COALESCE( + ST_AsGeoJSON(ST_Transform(b.geom, 4326))::json, + NULL + ) AS bordure +FROM sans_bordure sb +LEFT JOIN bordure_valide b ON true; +` + + const result = await pool.query(query, [ + geometry, + distance, + startBorderPoint[0], + startBorderPoint[1], + endBorderPoint[0], + endBorderPoint[1], + isInverted ? 1 : 0 + ]) + + if (!result.rows[0]) { + throw new Error('Aucun résultat retourné') + } + console.log(result.rows[0]) + return { + parcelleSansBordure: result.rows[0].parcelle_sans_bordure, + bordure: result.rows[0].bordure + } + } +} + module.exports = { getRpg, verifyGeometry, - getGeometryEquals + getGeometryEquals, + calculateParcelBorder } diff --git a/lib/providers/utils-pdf.js b/lib/providers/utils-pdf.js index 579d7c2e..7088b20b 100644 --- a/lib/providers/utils-pdf.js +++ b/lib/providers/utils-pdf.js @@ -118,7 +118,7 @@ async function getAllParcelles (recordId) { cp.cultures, cp.conversion_niveau, cp.commune, - c.nom as communename, + COALESCE(c.nom,'inconnu') as communename, cp.created, cp.engagement_date, cp.numero_ilot_pac AS nbilot, @@ -133,7 +133,7 @@ async function getAllParcelles (recordId) { ST_Y(ST_Centroid(cp.geometry)) AS centerY, COALESCE(SUM(ST_Area(to_legal_projection(cp.geometry)) / 10000), 0) AS superficie_totale_ha FROM cartobio_parcelles cp - JOIN communes c ON c.code = commune + LEFT JOIN communes c ON c.code = commune WHERE record_id = $1 AND cp.deleted_at IS NULL GROUP BY cp.id, cp.name, cp.cultures, cp.conversion_niveau, cp.created, cp.numero_ilot_pac, cp.numero_parcelle_pac, cp.geometry,cp.engagement_date,cp.commune,cp.reference_cadastre,c.nom; `, diff --git a/lib/routes/index.js b/lib/routes/index.js index f7c7cf43..22b29b4c 100644 --- a/lib/routes/index.js +++ b/lib/routes/index.js @@ -354,7 +354,7 @@ const operatorFromRecordId = { schema: { params: { recordId: { type: 'string', format: 'uuid' }, - featureId: { type: 'number' } + featureId: { type: ['number', 'string'] } } }, @@ -370,7 +370,7 @@ const routeWithRecordId = { schema: { params: { recordId: { type: 'string', format: 'uuid' }, - featureId: { type: 'number' } + featureId: { type: ['number', 'string'] } } }, diff --git a/package-lock.json b/package-lock.json index 98e15419..e05c1b97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,7 +53,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.3.1", "fast-jwt": "^5.0.6", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.6", "fastify": "^5.7.3", "file-type": "^16.5.4", "form-data": "^4.0.4", @@ -4209,9 +4209,9 @@ } }, "node_modules/basic-ftp": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", - "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -6333,22 +6333,19 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-parser": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", - "integrity": "sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz", + "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==", "dev": true, "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" - }, - { - "type": "paypal", - "url": "https://paypal.me/naturalintelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^1.0.5" + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -11778,10 +11775,17 @@ } }, "node_modules/strnum": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", - "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", - "dev": true + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", + "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/strtok3": { "version": "6.3.0", @@ -11886,9 +11890,9 @@ } }, "node_modules/tar": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", - "integrity": "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ==", + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { diff --git a/package.json b/package.json index 36070ca5..08b6eb3f 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.3.1", "fast-jwt": "^5.0.6", - "fast-xml-parser": "^4.4.1", + "fast-xml-parser": "^5.3.6", "fastify": "^5.7.3", "file-type": "^16.5.4", "form-data": "^4.0.4", diff --git a/server.js b/server.js index 4dc73f63..3f5e1e73 100644 --- a/server.js +++ b/server.js @@ -30,7 +30,10 @@ if (reportErrors) { if (config.get('environment') === 'production') { sentryOptions.release = config.get('version') - } else if (config.get('environment') === 'staging' || config.get('environment') === 'test') { + } else if ( + config.get('environment') === 'staging' || + config.get('environment') === 'test' + ) { sentryOptions.release = process.env.SENTRY_RELEASE } @@ -61,22 +64,58 @@ const { PassThrough } = require('stream') const { createSigner } = require('fast-jwt') const { fetchOperatorByNumeroBio, getUserProfileById, getUserProfileFromSSOToken, verifyNotificationAuthorization, fetchUserOperators, fetchCustomersByOc } = require('./lib/providers/agence-bio.js') -const { addRecordFeature, createFeaturesFromOther, patchFeatureCollection, updateAuditRecordState, updateFeature, createOrUpdateOperatorRecord, parcellaireStreamToDb, deleteSingleFeature, getRecords, deleteRecord, getOperatorLastRecord, searchControlBodyRecords, getDepartement, recordSorts, pinOperator, unpinOperator, consultOperator, getDashboardSummary, exportDataOcId, searchForAutocomplete, getImportPAC, hideImport, markFeatureControlled, markFeatureUncontrolled } = require('./lib/providers/cartobio.js') +const { addRecordFeature, createFeaturesFromOther, patchFeatureCollection, updateAuditRecordState, updateFeature, createOrUpdateOperatorRecord, parcellaireStreamToDb, deleteSingleFeature, getRecords, deleteRecord, getOperatorLastRecord, searchControlBodyRecords, getDepartement, recordSorts, pinOperator, unpinOperator, consultOperator, getDashboardSummary, exportDataOcId, searchForAutocomplete, getImportPAC, hideImport, markFeatureControlled, markFeatureUncontrolled, searchControlBodyRecordsAdmin } = require('./lib/providers/cartobio.js') const { generatePDF, getAttestationProduction } = require('./lib/providers/export-pdf.js') const { evvLookup, evvParcellaire, pacageLookup, iterateOperatorLastRecords } = require('./lib/providers/cartobio.js') const { parseAnyGeographicalArchive } = require('./lib/providers/gdal.js') const { parseTelepacArchive } = require('./lib/providers/telepac.js') -const { parseGeofoliaArchive, geofoliaLookup, geofoliaParcellaire } = require('./lib/providers/geofolia.js') +const { + parseGeofoliaArchive, + geofoliaLookup, + geofoliaParcellaire +} = require('./lib/providers/geofolia.js') const { InvalidRequestApiError, NotFoundApiError } = require('./lib/errors.js') -const { mergeSchemas, swaggerConfig, CartoBioDecoratorsPlugin, dashboardSummarySchema, autocompleteSchema } = require('./lib/routes/index.js') -const { sandboxSchema, internalSchema, hiddenSchema } = require('./lib/routes/index.js') -const { operatorFromNumeroBio, operatorFromRecordId, protectedWithToken, routeWithRecordId, routeWithPacage, checkCertificationStatus } = require('./lib/routes/index.js') -const { operatorsSchema, certificationBodySearchSchema } = require('./lib/routes/index.js') -const { createFeatureSchema, createRecordSchema, deleteSingleFeatureSchema, patchFeatureCollectionSchema, patchRecordSchema, updateFeaturePropertiesSchema } = require('./lib/routes/records.js') +const { + mergeSchemas, + swaggerConfig, + CartoBioDecoratorsPlugin, + dashboardSummarySchema, + autocompleteSchema +} = require('./lib/routes/index.js') +const { + sandboxSchema, + internalSchema, + hiddenSchema +} = require('./lib/routes/index.js') +const { + operatorFromNumeroBio, + operatorFromRecordId, + protectedWithToken, + routeWithRecordId, + routeWithPacage, + checkCertificationStatus +} = require('./lib/routes/index.js') +const { + operatorsSchema, + certificationBodySearchSchema +} = require('./lib/routes/index.js') +const { + createFeatureSchema, + createRecordSchema, + deleteSingleFeatureSchema, + patchFeatureCollectionSchema, + patchRecordSchema, + updateFeaturePropertiesSchema +} = require('./lib/routes/records.js') const { geofoliaImportSchema } = require('./lib/routes/index.js') -const { verifyGeometry, getRpg, getGeometryEquals } = require('./lib/providers/geometry.js') +const { + verifyGeometry, + getRpg, + getGeometryEquals, + calculateParcelBorder +} = require('./lib/providers/geometry.js') const DURATION_ONE_MINUTE = 1000 * 60 const DURATION_ONE_HOUR = DURATION_ONE_MINUTE * 60 @@ -87,8 +126,15 @@ const { UnauthorizedApiError, errorHandler } = require('./lib/errors.js') const { normalizeRecord } = require('./lib/outputs/record') const { recordToApi } = require('./lib/outputs/api') const { isHandledError } = require('./lib/errors') -const { getPinnedOperators, getConsultedOperators, addRecordData } = require('./lib/outputs/operator.js') -const sign = createSigner({ key: config.get('jwtSecret'), expiresIn: DURATION_ONE_DAY * 30 }) +const { + getPinnedOperators, + getConsultedOperators, + addRecordData +} = require('./lib/outputs/operator.js') +const sign = createSigner({ + key: config.get('jwtSecret'), + expiresIn: DURATION_ONE_DAY * 30 +}) app.setErrorHandler(errorHandler) if (reportErrors) { @@ -98,7 +144,15 @@ if (reportErrors) { // Configure server app.register(fastifyCors, { origin: true, - allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Accept-Encoding', 'Authorization', 'If-Unmodified-Since'] + allowedHeaders: [ + 'Origin', + 'X-Requested-With', + 'Content-Type', + 'Accept', + 'Accept-Encoding', + 'Authorization', + 'If-Unmodified-Since' + ] }) // Accept incoming files and forms (GeoJSON, ZIP files, etc.) @@ -184,18 +238,46 @@ app.register(async (app) => { return reply.send({ version: config.get('version') }) }) - app.get('/api/v2/test', mergeSchemas(sandboxSchema, protectedWithToken({ oc: true, cartobio: true })), (_, reply) => { - return reply.send({ message: 'OK' }) - }) + app.get( + '/api/v2/test', + mergeSchemas( + sandboxSchema, + protectedWithToken({ oc: true, cartobio: true }) + ), + (_, reply) => { + return reply.send({ message: 'OK' }) + } + ) /** * @private */ - app.post('/api/v2/certification/search', mergeSchemas(certificationBodySearchSchema, protectedWithToken()), async (request, reply) => { - const { input, page, limit, filter } = request.body - const { id: ocId } = request.user.organismeCertificateur + app.post( + '/api/v2/certification/search', + mergeSchemas(certificationBodySearchSchema, protectedWithToken()), + async (request, reply) => { + const { input, page, limit, filter } = request.body + const { id: ocId } = request.user.organismeCertificateur + + return reply.code(200).send( + searchControlBodyRecords({ + ocId, + userId: request.user.id, + input, + page, + limit, + filter + }) + ) + } + ) - return reply.code(200).send(searchControlBodyRecords({ ocId, userId: request.user.id, input, page, limit, filter })) + /** + * @private + */ + app.post('/api/v2/certification/adminsearch', mergeSchemas(certificationBodySearchSchema, protectedWithToken()), async (request, reply) => { + const { input, page, limit, filter } = request.body + return reply.code(200).send(searchControlBodyRecordsAdmin({ input, page, limit, filter })) }) /** @@ -212,274 +294,417 @@ app.register(async (app) => { * @private * Retrieve operators for a given user */ - app.get('/api/v2/operators', mergeSchemas(protectedWithToken({ cartobio: true }), operatorsSchema), async (request, reply) => { - const { id: userId } = request.user - const { search, limit, offset } = request.query - - return Promise.all( - [ + app.get( + '/api/v2/operators', + mergeSchemas(protectedWithToken({ cartobio: true }), operatorsSchema), + async (request, reply) => { + const { id: userId } = request.user + const { search, limit, offset } = request.query + + return Promise.all([ fetchUserOperators(userId), getPinnedOperators(request.user.id) - ] - ).then(([res, pinnedOperators]) => { - const paginatedOperators = res.operators - .filter((e) => { - if (!search) { - return true - } - - const userInput = search.toLowerCase().trim() - - return e.denominationCourante.toLowerCase().includes(userInput) || - e.numeroBio.toString().includes(userInput) || - e.nom.toLowerCase().includes(userInput) || - e.siret.toLowerCase().includes(userInput) + ]).then(([res, pinnedOperators]) => { + const paginatedOperators = res.operators + .filter((e) => { + if (!search) { + return true + } + + const userInput = search.toLowerCase().trim() + + return ( + e.denominationCourante.toLowerCase().includes(userInput) || + e.numeroBio.toString().includes(userInput) || + e.nom.toLowerCase().includes(userInput) || + e.siret.toLowerCase().includes(userInput) + ) + }) + .toSorted(recordSorts('fn', 'notifications', 'desc')) + .slice(offset, offset + limit) + .map((o) => ({ + ...o, + epingle: pinnedOperators.includes(+o.numeroBio) + })) + + return reply.code(200).send({ + nbTotal: res.operators.length, + operators: paginatedOperators }) - .toSorted(recordSorts('fn', 'notifications', 'desc')) - .slice(offset, offset + limit) - .map((o) => ({ ...o, epingle: pinnedOperators.includes(+o.numeroBio) })) - - return reply.code(200).send({ nbTotal: res.operators.length, operators: paginatedOperators }) - }) - }) + }) + } + ) /** * @private * Retrieve operators for a given user for their dashboard */ - app.get('/api/v2/operators/dashboard', mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), async (request, reply) => { - const { id: userId } = request.user - const { id: ocId } = request.user.organismeCertificateur - - return Promise.all([getPinnedOperators(userId), getConsultedOperators(userId)]) - .then(async ([pinnedNumerobios, consultedNumerobio]) => { - const uniqueNumerobios = [...new Set([...pinnedNumerobios, ...consultedNumerobio])] - const operators = ( - await fetchCustomersByOc(ocId)) - .filter( - (operator) => - uniqueNumerobios.includes(operator.numeroBio) && - operator.notifications.certification_state !== 'ARRETEE' && - operator.notifications.organismeCertificateurId === ocId && - ['ENGAGEE', 'ENGAGEE FUTUR'].includes(operator.notifications.etatCertification) - ) + app.get( + '/api/v2/operators/dashboard', + mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), + async (request, reply) => { + const { id: userId } = request.user + const { id: ocId } = request.user.organismeCertificateur + + return Promise.all([ + getPinnedOperators(userId), + getConsultedOperators(userId) + ]).then(async ([pinnedNumerobios, consultedNumerobio]) => { + const uniqueNumerobios = [ + ...new Set([...pinnedNumerobios, ...consultedNumerobio]) + ] + const operators = (await fetchCustomersByOc(ocId)).filter( + (operator) => + uniqueNumerobios.includes(operator.numeroBio) && + operator.notifications.certification_state !== 'ARRETEE' && + operator.notifications.organismeCertificateurId === ocId && + ['ENGAGEE', 'ENGAGEE FUTUR'].includes( + operator.notifications.etatCertification + ) + ) return Promise.all(operators.map((o) => addRecordData(o))).then( - (operatorsWithData) => reply.code(200).send({ - pinnedOperators: pinnedNumerobios.filter((numeroBio) => (operatorsWithData.find((o) => o.numeroBio === numeroBio))).map((numeroBio) => ({ ...operatorsWithData.find((o) => o.numeroBio === numeroBio), epingle: true })), - consultedOperators: consultedNumerobio.filter((numeroBio) => (operatorsWithData.find((o) => o.numeroBio === numeroBio))).map((numeroBio) => ({ ...operatorsWithData.find((o) => o.numeroBio === numeroBio), epingle: pinnedNumerobios.includes(numeroBio) })) - }) + (operatorsWithData) => + reply.code(200).send({ + pinnedOperators: pinnedNumerobios + .filter((numeroBio) => + operatorsWithData.find((o) => o.numeroBio === numeroBio) + ) + .map((numeroBio) => ({ + ...operatorsWithData.find((o) => o.numeroBio === numeroBio), + epingle: true + })), + consultedOperators: consultedNumerobio + .filter((numeroBio) => + operatorsWithData.find((o) => o.numeroBio === numeroBio) + ) + .map((numeroBio) => ({ + ...operatorsWithData.find((o) => o.numeroBio === numeroBio), + epingle: pinnedNumerobios.includes(numeroBio) + })) + }) ) }) - }) + } + ) /** * @private * Retrieve operators for a given user for their dashboard */ - app.post('/api/v2/operators/dashboard-summary', mergeSchemas(dashboardSummarySchema, protectedWithToken({ oc: true, cartobio: true })), async (request, reply) => { - const { departements, anneeReferenceControle } = request.body - const { id: ocId } = request.user.organismeCertificateur - - return reply.code(200).send(getDashboardSummary(ocId, departements, anneeReferenceControle)) - }) + app.post( + '/api/v2/operators/dashboard-summary', + mergeSchemas( + dashboardSummarySchema, + protectedWithToken({ oc: true, cartobio: true }) + ), + async (request, reply) => { + const { departements, anneeReferenceControle } = request.body + const { id: ocId } = request.user.organismeCertificateur + + return reply + .code(200) + .send(getDashboardSummary(ocId, departements, anneeReferenceControle)) + } + ) /** * @private * Retrieve an operator */ - app.get('/api/v2/operator/:numeroBio', mergeSchemas(protectedWithToken(), operatorFromNumeroBio), async (request, reply) => { - const pinnedOperators = await getPinnedOperators(request.user.id) + app.get( + '/api/v2/operator/:numeroBio', + mergeSchemas(protectedWithToken(), operatorFromNumeroBio), + async (request, reply) => { + const pinnedOperators = await getPinnedOperators(request.user.id) - request.operator.epingle = pinnedOperators.includes(+request.operator.numeroBio) + request.operator.epingle = pinnedOperators.includes( + +request.operator.numeroBio + ) - return reply.code(200).send(request.operator) - }) + return reply.code(200).send(request.operator) + } + ) /** * @private * Pin an operator */ - app.post('/api/v2/operator/:numeroBio/pin', mergeSchemas(protectedWithToken()), async (request, reply) => { - await pinOperator(request.params.numeroBio, request.user.id) + app.post( + '/api/v2/operator/:numeroBio/pin', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await pinOperator(request.params.numeroBio, request.user.id) - return reply.code(200).send({ epingle: true }) - }) + return reply.code(200).send({ epingle: true }) + } + ) /** * @private * Unpin an operator */ - app.post('/api/v2/operator/:numeroBio/unpin', mergeSchemas(protectedWithToken()), async (request, reply) => { - await unpinOperator(request.params.numeroBio, request.user.id) + app.post( + '/api/v2/operator/:numeroBio/unpin', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await unpinOperator(request.params.numeroBio, request.user.id) - return reply.code(200).send({ epingle: false }) - }) + return reply.code(200).send({ epingle: false }) + } + ) /** * @private * Mark an operator as consulted */ - app.post('/api/v2/operator/:numeroBio/consulte', mergeSchemas(protectedWithToken()), async (request, reply) => { - await consultOperator(request.params.numeroBio, request.user.id) + app.post( + '/api/v2/operator/:numeroBio/consulte', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await consultOperator(request.params.numeroBio, request.user.id) - return reply.code(204).send() - }) + return reply.code(204).send() + } + ) /** /** * @private * Retrieve an operator records */ - app.get('/api/v2/operator/:numeroBio/records', mergeSchemas(protectedWithToken(), operatorFromNumeroBio), async (request, reply) => { - const records = await getRecords(request.params.numeroBio) + app.get( + '/api/v2/operator/:numeroBio/records', + mergeSchemas(protectedWithToken(), operatorFromNumeroBio), + async (request, reply) => { + const records = await getRecords(request.params.numeroBio) + + if ( + !request.user.organismeCertificateur || + request.user.organismeCertificateur.id === + request.operator.organismeCertificateur.id + ) { + return reply.code(200).send(records) + } - if (!request.user.organismeCertificateur || request.user.organismeCertificateur.id === request.operator.organismeCertificateur.id) { - return reply.code(200).send(records) + return reply + .code(200) + .send( + records.filter( + (r) => r.oc_id === request.user.organismeCertificateur.id + ) + ) } - - return reply.code(200).send(records.filter((r) => r.oc_id === request.user.organismeCertificateur.id)) - }) + ) /** * @private * Checks if operator can import a pac record from 2025 */ - app.get('/api/v2/operator/:numeroBio/importData', mergeSchemas(protectedWithToken()), async (request, reply) => { - const res = await getImportPAC(request.params.numeroBio) - return reply.code(200).send({ data: res }) - }) + app.get( + '/api/v2/operator/:numeroBio/importData', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + const res = await getImportPAC(request.params.numeroBio) + return reply.code(200).send({ data: res }) + } + ) /** * @private * Hide import PAC 2025 notif */ - app.patch('/api/v2/operator/:numeroBio/hideNotif', mergeSchemas(protectedWithToken()), async (request, reply) => { - await hideImport(request.params.numeroBio) - return reply.code(204).send() - }) + app.patch( + '/api/v2/operator/:numeroBio/hideNotif', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await hideImport(request.params.numeroBio) + return reply.code(204).send() + } + ) /** * Retrieve a given Record */ - app.get('/api/v2/audits/:recordId', mergeSchemas(protectedWithToken(), operatorFromRecordId), (request, reply) => { - return reply.code(200).send(request.record) - }) + app.get( + '/api/v2/audits/:recordId', + mergeSchemas(protectedWithToken(), operatorFromRecordId), + (request, reply) => { + return reply.code(200).send(request.record) + } + ) /** * Retrieve a given Record */ - app.get('/api/v2/audits/:recordId/has-attestation-production', mergeSchemas(protectedWithToken(), operatorFromRecordId), async (request, reply) => { - const attestation = await getAttestationProduction(request.record.record_id) - - return reply.code(200).send({ hasAttestationProduction: !!attestation }) - }) + app.get( + '/api/v2/audits/:recordId/has-attestation-production', + mergeSchemas(protectedWithToken(), operatorFromRecordId), + async (request, reply) => { + const attestation = await getAttestationProduction( + request.record.record_id + ) + + return reply.code(200).send({ hasAttestationProduction: !!attestation }) + } + ) /** * @private * Marque une parcelle comme controlée */ - app.post('/api/v2/audits/:recordId/:id/controlee', mergeSchemas(protectedWithToken()), async (request, reply) => { - await markFeatureControlled(request.params.recordId, request.params.id, request.user.id) - - return reply.code(200).send({ controlee: true }) - }) + app.post( + '/api/v2/audits/:recordId/:id/controlee', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await markFeatureControlled( + request.params.recordId, + request.params.id, + request.user.id + ) + + return reply.code(200).send({ controlee: true }) + } + ) /** - * @private + * @private * Marque une parcelle comme non controlée - */ - app.post('/api/v2/audits/:recordId/:id/non-controlee', mergeSchemas(protectedWithToken()), async (request, reply) => { - await markFeatureUncontrolled(request.params.recordId, request.params.id, request.user.id) - - return reply.code(200).send({ controlee: false }) - }) + */ + app.post( + '/api/v2/audits/:recordId/:id/non-controlee', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + await markFeatureUncontrolled( + request.params.recordId, + request.params.id, + request.user.id + ) + + return reply.code(200).send({ controlee: false }) + } + ) /** * Create a new Record for a given Operator */ - app.post('/api/v2/operator/:numeroBio/records', mergeSchemas( - createRecordSchema, - operatorFromNumeroBio, - checkCertificationStatus, - protectedWithToken() - ), async (request, reply) => { - const { numeroBio } = request.params - const { id: ocId, nom: ocLabel } = request.operator.organismeCertificateur - const record = await createOrUpdateOperatorRecord( - { numerobio: numeroBio, oc_id: ocId, oc_label: ocLabel, ...request.body }, - { user: request.user, copyParcellesData: request.body.importPrevious, previousRecordId: request.body.recordId } - ) - return reply.code(200).send(normalizeRecord(record)) - }) + app.post( + '/api/v2/operator/:numeroBio/records', + mergeSchemas( + createRecordSchema, + operatorFromNumeroBio, + checkCertificationStatus, + protectedWithToken() + ), + async (request, reply) => { + const { numeroBio } = request.params + const { id: ocId, nom: ocLabel } = + request.operator.organismeCertificateur + const record = await createOrUpdateOperatorRecord( + { + numerobio: numeroBio, + oc_id: ocId, + oc_label: ocLabel, + ...request.body + }, + { + user: request.user, + copyParcellesData: request.body.importPrevious, + previousRecordId: request.body.recordId + } + ) + return reply.code(200).send(normalizeRecord(record)) + } + ) /** * Delete a given Record */ - app.delete('/api/v2/audits/:recordId', mergeSchemas(protectedWithToken(), routeWithRecordId), async (request, reply) => { - const { user, record } = request - await deleteRecord({ user, record }) - return reply.code(204).send() - }) + app.delete( + '/api/v2/audits/:recordId', + mergeSchemas(protectedWithToken(), routeWithRecordId), + async (request, reply) => { + const { user, record } = request + await deleteRecord({ user, record }) + return reply.code(204).send() + } + ) /** * Partial update Record's metadata (top-level properties except features) * It also keep track of new HistoryEvent along the way, depending who and when you update feature properties */ - app.patch('/api/v2/audits/:recordId', mergeSchemas( - protectedWithToken(), - patchRecordSchema, - operatorFromRecordId, - routeWithRecordId - ), (request, reply) => { - const { body: patch, user, record, operator } = request - - return updateAuditRecordState({ user, record, operator }, patch) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) + app.patch( + '/api/v2/audits/:recordId', + mergeSchemas( + protectedWithToken(), + patchRecordSchema, + operatorFromRecordId, + routeWithRecordId + ), + (request, reply) => { + const { body: patch, user, record, operator } = request + + return updateAuditRecordState({ user, record, operator }, patch).then( + (record) => reply.code(200).send(normalizeRecord(record)) + ) + } + ) /** * Add new feature entries to an existing collection */ - app.post('/api/v2/audits/:recordId/parcelles', mergeSchemas( - protectedWithToken(), - createFeatureSchema, - routeWithRecordId, - operatorFromRecordId - ), (request, reply) => { - const { feature } = request.body - const { user, record, operator } = request - - return addRecordFeature({ user, record, operator }, feature) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) + app.post( + '/api/v2/audits/:recordId/parcelles', + mergeSchemas( + protectedWithToken(), + createFeatureSchema, + routeWithRecordId, + operatorFromRecordId + ), + (request, reply) => { + const { feature } = request.body + const { user, record, operator } = request + + return addRecordFeature({ user, record, operator }, feature).then( + (record) => reply.code(200).send(normalizeRecord(record)) + ) + } + ) /** * Get features of specific record id */ - app.get('/api/v2/audits/:recordId/parcelles', mergeSchemas( - protectedWithToken(), - operatorFromRecordId - ), (request, reply) => { - const { record } = request - return reply.code(200).send(record.parcelles) - }) + app.get( + '/api/v2/audits/:recordId/parcelles', + mergeSchemas(protectedWithToken(), operatorFromRecordId), + (request, reply) => { + const { record } = request + return reply.code(200).send(record.parcelles) + } + ) /** * Partial update a feature collection (ie: mass action from the collection screen) * * Matching features are updated, features not present in payload or database are ignored */ - app.patch('/api/v2/audits/:recordId/parcelles', mergeSchemas( - protectedWithToken(), - patchFeatureCollectionSchema, - routeWithRecordId, - operatorFromRecordId - ), (request, reply) => { - const { body: featureCollection, user, record, operator } = request - - return patchFeatureCollection({ user, record, operator }, featureCollection.features) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) + app.patch( + '/api/v2/audits/:recordId/parcelles', + mergeSchemas( + protectedWithToken(), + patchFeatureCollectionSchema, + routeWithRecordId, + operatorFromRecordId + ), + (request, reply) => { + const { body: featureCollection, user, record, operator } = request + + return patchFeatureCollection( + { user, record, operator }, + featureCollection.features + ).then((record) => reply.code(200).send(normalizeRecord(record))) + } + ) /** * Partial update a single feature (ie: feature form from an editing modal) @@ -487,358 +712,574 @@ app.register(async (app) => { * Absent properties are kept as is, new properties are added, existing properties are updated * ('culture' field is not a special case, it's just a regular property that can be replaced) */ - app.patch('/api/v2/audits/:recordId/parcelles/:featureId', mergeSchemas( - protectedWithToken(), - updateFeaturePropertiesSchema, - routeWithRecordId, - operatorFromRecordId - ), (request, reply) => { - const { body: feature, user, record, operator } = request - const { featureId } = request.params - - return updateFeature({ featureId, user, record, operator }, feature) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) + app.patch( + '/api/v2/audits/:recordId/parcelles/:featureId', + mergeSchemas( + protectedWithToken(), + updateFeaturePropertiesSchema, + routeWithRecordId, + operatorFromRecordId + ), + (request, reply) => { + const { body: feature, user, record, operator } = request + const { featureId } = request.params + + return updateFeature({ featureId, user, record, operator }, feature).then( + (record) => reply.code(200).send(normalizeRecord(record)) + ) + } + ) /** * Delete a single feature */ - app.delete('/api/v2/audits/:recordId/parcelles/:featureId', mergeSchemas( - protectedWithToken(), - deleteSingleFeatureSchema, - routeWithRecordId, - operatorFromRecordId - ), (request, reply) => { - const { user, record, operator } = request - const { reason } = request.body - const { featureId } = request.params - - return deleteSingleFeature({ featureId, user, record, operator }, { reason }) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) - - app.put('/api/v2/audits/:recordId/parcelles', mergeSchemas(protectedWithToken(), routeWithRecordId, operatorFromRecordId), (request, reply) => { - const { user, record, operator } = request - const { features, from } = request.body - - return createFeaturesFromOther(user, record, operator, features, from) - .then(record => reply.code(200).send(normalizeRecord(record))) - }) + app.delete( + '/api/v2/audits/:recordId/parcelles/:featureId', + mergeSchemas( + protectedWithToken(), + deleteSingleFeatureSchema, + routeWithRecordId, + operatorFromRecordId + ), + (request, reply) => { + const { user, record, operator } = request + const { reason } = request.body + const { featureId } = request.params + + return deleteSingleFeature( + { featureId, user, record, operator }, + { reason } + ).then((record) => reply.code(200).send(normalizeRecord(record))) + } + ) + + app.put( + '/api/v2/audits/:recordId/parcelles', + mergeSchemas(protectedWithToken(), routeWithRecordId, operatorFromRecordId), + (request, reply) => { + const { user, record, operator } = request + const { features, from } = request.body + + return createFeaturesFromOther( + user, + record, + operator, + features, + from + ).then((record) => reply.code(200).send(normalizeRecord(record))) + } + ) /** * Turn a Telepac XML or Telepac zipped Shapefile into a workeable FeatureCollection * It's essentially used during an import process to preview its content * @private */ - app.post('/api/v2/convert/telepac/geojson', mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), async (request, reply) => { - return parseTelepacArchive(request.file()) - .then(geojson => reply.send(geojson)) - }) + app.post( + '/api/v2/convert/telepac/geojson', + mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), + async (request, reply) => { + return parseTelepacArchive(request.file()).then((geojson) => + reply.send(geojson) + ) + } + ) /** * Turn a Geofolia file into a workeable FeatureCollection * It's essentially used during an import process to preview its content * @private */ - app.post('/api/v2/convert/geofolia/geojson', mergeSchemas(protectedWithToken()), async (request, reply) => { - const data = await request.file() - - return parseGeofoliaArchive(await data.toBuffer()) - .then(geojson => reply.send(geojson)) - }) + app.post( + '/api/v2/convert/geofolia/geojson', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + const data = await request.file() + + return parseGeofoliaArchive(await data.toBuffer()).then((geojson) => + reply.send(geojson) + ) + } + ) /** * Turn a geographical file workeable FeatureCollection * It's essentially used during an import process to preview its content * @private */ - app.post('/api/v2/convert/anygeo/geojson', mergeSchemas(protectedWithToken()), async (request, reply) => { - return parseAnyGeographicalArchive(request.file()) - .then(geojson => reply.send(geojson)) - }) + app.post( + '/api/v2/convert/anygeo/geojson', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + return parseAnyGeographicalArchive(request.file()).then((geojson) => + reply.send(geojson) + ) + } + ) /** * Retrieves all features associated to a PACAGE as a workeable FeatureCollection */ - app.get('/api/v2/import/pacage/:numeroPacage', mergeSchemas(protectedWithToken({ cartobio: true, oc: true }), routeWithPacage), async (request, reply) => { - const { numeroPacage } = request.params - - return pacageLookup({ numeroPacage }) - .then(featureCollection => reply.send(featureCollection)) - }) + app.get( + '/api/v2/import/pacage/:numeroPacage', + mergeSchemas( + protectedWithToken({ cartobio: true, oc: true }), + routeWithPacage + ), + async (request, reply) => { + const { numeroPacage } = request.params + + return pacageLookup({ numeroPacage }).then((featureCollection) => + reply.send(featureCollection) + ) + } + ) /** * Checks if an operator has Geofolink features * It triggers a data order, which has the benefit to break the waiting time in two */ - app.head('/api/v2/import/geofolia/:numeroBio', mergeSchemas(protectedWithToken({ cartobio: true, oc: true }), operatorFromNumeroBio, geofoliaImportSchema), async (request, reply) => { - const { siret } = request.operator - const { year } = request.query - - const isWellKnown = await geofoliaLookup(siret, year) - - return reply.code(isWellKnown === true ? 204 : 404).send() - }) + app.head( + '/api/v2/import/geofolia/:numeroBio', + mergeSchemas( + protectedWithToken({ cartobio: true, oc: true }), + operatorFromNumeroBio, + geofoliaImportSchema + ), + async (request, reply) => { + const { siret } = request.operator + const { year } = request.query + + const isWellKnown = await geofoliaLookup(siret, year) + + return reply.code(isWellKnown === true ? 204 : 404).send() + } + ) /** * Retrieves all features associated to a given SIRET linked to a numeroBio */ - app.get('/api/v2/import/geofolia/:numeroBio', mergeSchemas(protectedWithToken({ cartobio: true, oc: true }), operatorFromNumeroBio), async (request, reply) => { - const { siret } = request.operator - - const featureCollection = await geofoliaParcellaire(siret) + app.get( + '/api/v2/import/geofolia/:numeroBio', + mergeSchemas( + protectedWithToken({ cartobio: true, oc: true }), + operatorFromNumeroBio + ), + async (request, reply) => { + const { siret } = request.operator + + const featureCollection = await geofoliaParcellaire(siret) + + if (!featureCollection) { + return reply.code(202).send() + } - if (!featureCollection) { - return reply.code(202).send() + return reply.send(featureCollection) } - - return reply.send(featureCollection) - }) + ) /** * Retrieves all features associated to an EVV associated to a numeroBio * You still have to add geometries to the collection. * Features contains a 'cadastre' property with references to fetch */ - app.get('/api/v2/import/evv/:numeroEvv(\\d+)+:numeroBio(\\d+)', mergeSchemas(protectedWithToken({ cartobio: true, oc: true }), operatorFromNumeroBio), async (request, reply) => { - const { numeroEvv } = request.params - const { siret: expectedSiret } = request.operator - - if (!expectedSiret) { - throw new InvalidRequestApiError('Le numéro SIRET de l\'opérateur n\'est pas renseigné sur le portail de Notification de l\'Agence Bio. Il est indispensable pour sécuriser la collecte du parcellaire viticole auprès des Douanes.') - } + app.get( + '/api/v2/import/evv/:numeroEvv(\\d+)+:numeroBio(\\d+)', + mergeSchemas( + protectedWithToken({ cartobio: true, oc: true }), + operatorFromNumeroBio + ), + async (request, reply) => { + const { numeroEvv } = request.params + const { siret: expectedSiret } = request.operator + + if (!expectedSiret) { + throw new InvalidRequestApiError( + "Le numéro SIRET de l'opérateur n'est pas renseigné sur le portail de Notification de l'Agence Bio. Il est indispensable pour sécuriser la collecte du parcellaire viticole auprès des Douanes." + ) + } - return evvLookup({ numeroEvv }) - .then(({ siret }) => { - if (!siret) { - throw new NotFoundApiError('Ce numéro EVV est introuvable') - } else if (siret !== expectedSiret) { - throw new UnauthorizedApiError('les numéros SIRET du nCVI et de l\'opérateur Agence Bio ne correspondent pas.') - } - }) - .then(() => evvParcellaire({ numeroEvv })) - .then(featureCollection => { - if (featureCollection.features.length === 0) { - throw new NotFoundApiError('Ce numéro EVV ne retourne pas de parcelles.') - } + return evvLookup({ numeroEvv }) + .then(({ siret }) => { + if (!siret) { + throw new NotFoundApiError('Ce numéro EVV est introuvable') + } else if (siret !== expectedSiret) { + throw new UnauthorizedApiError( + "les numéros SIRET du nCVI et de l'opérateur Agence Bio ne correspondent pas." + ) + } + }) + .then(() => evvParcellaire({ numeroEvv })) + .then((featureCollection) => { + if (featureCollection.features.length === 0) { + throw new NotFoundApiError( + 'Ce numéro EVV ne retourne pas de parcelles.' + ) + } - return reply.send(featureCollection) - }) - }) + return reply.send(featureCollection) + }) + } + ) - app.post('/api/v2/certification/parcelles', mergeSchemas(protectedWithToken({ oc: true }), { - preParsing: async (request, reply, payload) => { - const stream = payload.pipe(stripBom()) + app.post( + '/api/v2/certification/parcelles', + mergeSchemas(protectedWithToken({ oc: true }), { + preParsing: async (request, reply, payload) => { + const stream = payload.pipe(stripBom()) - request.APIResult = await parcellaireStreamToDb(stream, request.organismeCertificateur) - request.headers['content-length'] = '2' - return new PassThrough().end('{}') - } - }), (request, reply) => { - const { count, errors, warnings } = request.APIResult + request.APIResult = await parcellaireStreamToDb( + stream, + request.organismeCertificateur + ) + request.headers['content-length'] = '2' + return new PassThrough().end('{}') + } + }), + (request, reply) => { + const { count, errors, warnings } = request.APIResult + + if (errors.length > 0) { + return reply.code(400).send({ + nbObjetTraites: count, + nbObjetAcceptes: count - errors.length, + nbObjetRefuses: errors.length, + listeProblemes: errors.map( + ([index, message]) => `[#${index}] ${message}` + ), + listeWarning: + warnings && warnings.length > 0 + ? warnings.map(([index, message]) => `[#${index}] ${message}`) + : [] + }) + } - if (errors.length > 0) { - return reply.code(400).send({ + return reply.code(202).send({ nbObjetTraites: count, - nbObjetAcceptes: count - errors.length, - nbObjetRefuses: errors.length, - listeProblemes: errors.map(([index, message]) => `[#${index}] ${message}`), - listeWarning: warnings && warnings.length > 0 ? warnings.map(([index, message]) => `[#${index}] ${message}`) : [] + listeWarning: + warnings && warnings.length > 0 + ? warnings.map(([index, message]) => `[#${index}] ${message}`) + : [] }) } + ) + + app.get( + '/api/v2/certification/parcellaires', + mergeSchemas(protectedWithToken({ oc: true })), + async (request, reply) => { + reply.header('Content-Type', 'application/json') + const { limit, start, anneeAudit, statut, anneeReferenceControle } = + request.query + const finalLimit = limit ? Number(limit) : null + const finalStart = start ? Number(start) : 0 + + const records = await iterateOperatorLastRecords( + request.organismeCertificateur.id, + { + anneeAudit, + statut, + anneeReferenceControle, + limit: finalLimit, + start: finalStart + } + ) + + const apiRecords = await Promise.all(records.map((r) => recordToApi(r))) + const links = {} + + const host = request.hostname + const baseUrl = `https://${host}${request.url.split('?')[0]}` + + if (finalLimit !== null && finalLimit > 0) { + if (finalStart > 0) { + const prevParams = new URLSearchParams(request.query) + prevParams.set('start', Math.max(0, finalStart - finalLimit)) + prevParams.set('limit', finalLimit) + links.prev = `${baseUrl}?${prevParams.toString()}` + } else { + links.prev = null + } - return reply.code(202).send({ - nbObjetTraites: count, - listeWarning: warnings && warnings.length > 0 ? warnings.map(([index, message]) => `[#${index}] ${message}`) : [] - }) - }) - - app.get('/api/v2/certification/parcellaires', mergeSchemas(protectedWithToken({ oc: true })), async (request, reply) => { - reply.header('Content-Type', 'application/json') - const { limit, start, anneeAudit, statut, anneeReferenceControle } = request.query - const finalLimit = limit ? Number(limit) : null - const finalStart = start ? Number(start) : 0 - - const records = await iterateOperatorLastRecords( - request.organismeCertificateur.id, - { - anneeAudit, - statut, - anneeReferenceControle, - limit: finalLimit, - start: finalStart - } - ) - - const apiRecords = await Promise.all(records.map(r => recordToApi(r))) - const links = {} - - const host = request.hostname - const baseUrl = `https://${host}${request.url.split('?')[0]}` - - if (finalLimit !== null && finalLimit > 0) { - if (finalStart > 0) { - const prevParams = new URLSearchParams(request.query) - prevParams.set('start', Math.max(0, finalStart - finalLimit)) - prevParams.set('limit', finalLimit) - links.prev = `${baseUrl}?${prevParams.toString()}` + if (records.length === finalLimit) { + const nextParams = new URLSearchParams(request.query) + nextParams.set('start', finalStart + finalLimit) + nextParams.set('limit', finalLimit) + links.next = `${baseUrl}?${nextParams.toString()}` + } else { + links.next = null + } } else { - links.prev = null - } + if (finalStart > 0) { + const prevParams = new URLSearchParams(request.query) + prevParams.delete('start') + links.prev = `${baseUrl}?${prevParams.toString()}` + } else { + links.prev = null + } - if (records.length === finalLimit) { - const nextParams = new URLSearchParams(request.query) - nextParams.set('start', finalStart + finalLimit) - nextParams.set('limit', finalLimit) - links.next = `${baseUrl}?${nextParams.toString()}` - } else { links.next = null } - } else { - if (finalStart > 0) { - const prevParams = new URLSearchParams(request.query) - prevParams.delete('start') - links.prev = `${baseUrl}?${prevParams.toString()}` - } else { - links.prev = null - } - links.next = null + return reply.code(200).send({ data: apiRecords, _links: links }) } + ) + + app.get( + '/api/v2/certification/parcellaire/:numeroBio', + mergeSchemas(protectedWithToken({ oc: true }), operatorFromNumeroBio), + async (request, reply) => { + const record = await getOperatorLastRecord(request.params.numeroBio, { + anneeAudit: request.query.anneeAudit, + statut: request.query.statut + }) + return reply.code(200).send(await recordToApi(record)) + } + ) + + app.get( + '/api/v2/pdf/:numeroBio/:recordId', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + const force = request.query.force_refresh === 'true' ?? false + + try { + const gen = generatePDF( + request.params.numeroBio, + request.params.recordId, + force + ) + const numberParcelle = (await gen.next()).value - return reply.code(200).send({ data: apiRecords, _links: links }) - }) - - app.get('/api/v2/certification/parcellaire/:numeroBio', mergeSchemas(protectedWithToken({ oc: true }), operatorFromNumeroBio), async (request, reply) => { - const record = await getOperatorLastRecord(request.params.numeroBio, { - anneeAudit: request.query.anneeAudit, - statut: request.query.statut - }) - return reply.code(200).send(await recordToApi(record)) - }) - - app.get('/api/v2/pdf/:numeroBio/:recordId', mergeSchemas(protectedWithToken()), async (request, reply) => { - const force = request.query.force_refresh === 'true' ?? false - - try { - const gen = generatePDF(request.params.numeroBio, request.params.recordId, force) - const numberParcelle = (await gen.next()).value - - console.log(numberParcelle) - if (numberParcelle > 80) { - reply.code(204).send() - } + console.log(numberParcelle) + if (numberParcelle > 80) { + reply.code(204).send() + } - const pdf = (await gen.next()).value - if (numberParcelle <= 80) { - return reply.code(200).send(pdf) + const pdf = (await gen.next()).value + if (numberParcelle <= 80) { + return reply.code(200).send(pdf) + } + } catch (e) { + return reply.code(400).send({ message: e.message }) } - } catch (e) { - return reply.code(400).send({ message: e.message }) } - }) - - app.get('/api/v2/user/verify', mergeSchemas(protectedWithToken({ oc: true, cartobio: true }), sandboxSchema, internalSchema), (request, reply) => { - const { user, organismeCertificateur } = request - - return reply.send(user ?? organismeCertificateur) - }) + ) + + app.get( + '/api/v2/user/verify', + mergeSchemas( + protectedWithToken({ oc: true, cartobio: true }), + sandboxSchema, + internalSchema + ), + (request, reply) => { + const { user, organismeCertificateur } = request + + return reply.send(user ?? organismeCertificateur) + } + ) /** * Exchange a notification.agencebio.org token for a CartoBio token */ - app.get('/api/v2/user/exchangeToken', internalSchema, async (request, reply) => { - const { error, decodedToken, token } = verifyNotificationAuthorization(request.headers.authorization) - - if (error) { - return new UnauthorizedApiError('impossible de vérifier ce jeton', { cause: error }) - } - - const [operator, userProfile] = await Promise.all([ - fetchOperatorByNumeroBio(decodedToken.numeroBio, token), - getUserProfileById(decodedToken.userId, token) - ]) + app.get( + '/api/v2/user/exchangeToken', + internalSchema, + async (request, reply) => { + const { error, decodedToken, token } = verifyNotificationAuthorization( + request.headers.authorization + ) + + if (error) { + return new UnauthorizedApiError('impossible de vérifier ce jeton', { + cause: error + }) + } - const sign = createSigner({ key: config.get('jwtSecret'), expiresIn: DURATION_ONE_HOUR * 2 }) + const [operator, userProfile] = await Promise.all([ + fetchOperatorByNumeroBio(decodedToken.numeroBio, token), + getUserProfileById(decodedToken.userId, token) + ]) - return reply.send({ - operator, - // @todo use Notification pubkey and time based token to passthrough the requests to both Agence Bio and CartoBio APIs - token: sign(userProfile) - }) - }) + const sign = createSigner({ + key: config.get('jwtSecret'), + expiresIn: DURATION_ONE_HOUR * 2 + }) - app.get('/api/v2/departements', mergeSchemas(protectedWithToken()), async (request, reply) => { - const departements = await getDepartement() - return reply.code(200).send(departements) - }) + return reply.send({ + operator, + // @todo use Notification pubkey and time based token to passthrough the requests to both Agence Bio and CartoBio APIs + token: sign(userProfile) + }) + } + ) + + app.get( + '/api/v2/departements', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + const departements = await getDepartement() + return reply.code(200).send(departements) + } + ) // usefull only in dev mode - app.get('/auth-provider/agencebio/login', hiddenSchema, (request, reply) => reply.redirect('/api/auth-provider/agencebio/login')) - app.get('/api/auth-provider/agencebio/callback', mergeSchemas(sandboxSchema, hiddenSchema), async (request, reply) => { - // forwards to the UI the user-selected tab - const { mode = '', returnto = '' } = stateCache.get(request.query.state) - const { token } = await app.agenceBioOAuth2.getAccessTokenFromAuthorizationCodeFlow(request) - const userProfile = await getUserProfileFromSSOToken(token.access_token) - const cartobioToken = sign(userProfile) - - return reply.redirect(`${config.get('frontendUrl')}/login?mode=${mode}&returnto=${returnto}#token=${cartobioToken}`) - }) - - app.post('/api/v2/exportParcellaire', mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), async (request, reply) => { - const data = await exportDataOcId(request.user.organismeCertificateur.id, request.body.payload, request.user.id) - if (data === null) { - throw new Error("Une erreur s'est produite, impossible d'exporter les parcellaires") + app.get('/auth-provider/agencebio/login', hiddenSchema, (request, reply) => + reply.redirect('/api/auth-provider/agencebio/login') + ) + app.get( + '/api/auth-provider/agencebio/callback', + mergeSchemas(sandboxSchema, hiddenSchema), + async (request, reply) => { + // forwards to the UI the user-selected tab + const { mode = '', returnto = '' } = stateCache.get(request.query.state) + const { token } = + await app.agenceBioOAuth2.getAccessTokenFromAuthorizationCodeFlow( + request + ) + const userProfile = await getUserProfileFromSSOToken(token.access_token) + const cartobioToken = sign(userProfile) + + return reply.redirect( + `${config.get( + 'frontendUrl' + )}/login?mode=${mode}&returnto=${returnto}#token=${cartobioToken}` + ) } - return reply.code(200).send(data) - }) - - app.post('/api/v2/geometry/:recordId/add', mergeSchemas( - protectedWithToken() - ), (request, reply) => { - const { payload: feature } = request.body - return verifyGeometry(feature.geometry, request.params.recordId, feature.properties?.id ?? '') - .then(record => reply.code(200).send((record))) - }) + ) + + app.post( + '/api/v2/exportParcellaire', + mergeSchemas(protectedWithToken({ oc: true, cartobio: true })), + async (request, reply) => { + const data = await exportDataOcId( + request.user.organismeCertificateur.id, + request.body.payload, + request.user.id + ) + if (data === null) { + throw new Error( + "Une erreur s'est produite, impossible d'exporter les parcellaires" + ) + } + return reply.code(200).send(data) + } + ) + + app.post( + '/api/v2/geometry/:recordId/add', + mergeSchemas(protectedWithToken()), + (request, reply) => { + const { payload: feature } = request.body + return verifyGeometry( + feature.geometry, + request.params.recordId, + feature.properties?.id ?? '' + ).then((record) => reply.code(200).send(record)) + } + ) }) -app.post('/api/v2/geometry/rpg', mergeSchemas( - protectedWithToken() -), (request, reply) => { - const { extent, surface, codeCulture } = request.body - return getRpg(extent, surface, codeCulture) - .then(data => { +app.post( + '/api/v2/geometry/rpg', + mergeSchemas(protectedWithToken()), + (request, reply) => { + const { extent, surface, codeCulture } = request.body + return getRpg(extent, surface, codeCulture).then((data) => { if (data) { - return reply.code(200).send((data)) + return reply.code(200).send(data) } return reply.code(404).send() }) -}) + } +) + +app.post( + '/api/v3/geometry/border-cut', + mergeSchemas(protectedWithToken()), + async (request, reply) => { + const { + geometry, + distance, + allBorder, + isInverted, + startBorderPoint, + endBorderPoint + } = request.body + const result = calculateParcelBorder( + JSON.stringify(geometry), + distance, + allBorder, + isInverted, + startBorderPoint, + endBorderPoint + ) + if (!result) { + return reply.code(400).send() + } -app.post('/api/v2/geometry/geometryEquals', mergeSchemas( - protectedWithToken() -), (request, reply) => { - const { payload } = request.body - return getGeometryEquals(payload) - .then(data => reply.code(200).send((data))) -}) + return result + } +) + +app.post( + '/api/v2/geometry/geometryEquals', + mergeSchemas(protectedWithToken()), + (request, reply) => { + const { payload } = request.body + return getGeometryEquals(payload).then((data) => + reply.code(200).send(data) + ) + } +) if (require.main === module) { - db.query('SHOW server_version;').then(async ({ rows }) => { - const { server_version: pgVersion } = rows[0] - console.log(`Postgres connection established, v${pgVersion}`) + db.query('SHOW server_version;').then( + async ({ rows }) => { + const { server_version: pgVersion } = rows[0] + console.log(`Postgres connection established, v${pgVersion}`) - await app.ready() - await app.swagger() + await app.ready() + await app.swagger() - const address = await app.listen({ - host: config.get('host'), - port: config.get('port') - }) + const address = await app.listen({ + host: config.get('host'), + port: config.get('port') + }) - console.log(`Running env:${config.get('env')} on ${address}`) - }, () => console.error('Failed to connect to database')) + console.log(`Running env:${config.get('env')} on ${address}`) + }, + () => console.error('Failed to connect to database') + ) } +app.get('/api/v3/external/exploitations/:numeroBio', (req, res) => { + const { numeroBio } = req.params + + if (!numeroBio || isNaN(Number(numeroBio)) || Number(numeroBio) <= 0) { + return res.status(400).send('numeroBio invalide') + } + + const state = randomUUID() + stateCache.set(state, { + returnto: `/exploitations/${numeroBio}` + }) + + const ssoHost = config.get('notifications.sso.host') + const clientId = config.get('notifications.sso.clientId') + const callbackUri = config.get('notifications.sso.callbackUri') + + const authUrl = new URL(`${ssoHost}/oauth2/auth`) + authUrl.searchParams.set('response_type', 'code') + authUrl.searchParams.set('client_id', clientId) + authUrl.searchParams.set('redirect_uri', callbackUri) + authUrl.searchParams.set('scope', 'openid') + authUrl.searchParams.set('state', state) + authUrl.searchParams.set('login_hint', 'skip_consent') + + return res.redirect(authUrl.toString()) +}) + module.exports = app