diff --git a/cron.json b/cron.json
index 2f3531ce..da6d52f5 100644
--- a/cron.json
+++ b/cron.json
@@ -1,7 +1,8 @@
{
"jobs": [
{
- "command": "0 2 * * * node outils/sync-démarches-simplifiées.js"
+ "command": "0 2 * * * node outils/sync-démarches-simplifiées.js",
+ "size": "S"
}
]
}
\ No newline at end of file
diff --git a/index.html b/index.html
index 0870dd40..0eb31685 100644
--- a/index.html
+++ b/index.html
@@ -10,13 +10,18 @@
+
+
+
+
+
-
+
-
+
diff --git a/package-lock.json b/package-lock.json
index 27da9b60..8267b51c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"dependencies": {
"@fastify/static": "^7.0.1",
+ "baredux": "github:DavidBruant/baredux#v1.0.8",
"d3-fetch": "^3.0.1",
"date-fns": "^3.6.0",
"db-migrate": "^0.11.14",
@@ -16,7 +17,9 @@
"fastify": "^4.26.2",
"knex": "^3.1.0",
"ky": "^1.2.2",
- "pg": "^8.11.5"
+ "page": "^1.11.6",
+ "pg": "^8.11.5",
+ "remember": "github:DavidBruant/remember#v1.0.2"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
@@ -34,7 +37,7 @@
"ts-to-jsdoc": "^2.1.0"
},
"engines": {
- "node": ">20"
+ "node": "20"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -891,6 +894,10 @@
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
+ "node_modules/baredux": {
+ "version": "1.0.8",
+ "resolved": "git+ssh://git@github.com/DavidBruant/baredux.git#1b62e43d892aaff0ff5e94e5b7ce51b24cc2e074"
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3519,6 +3526,14 @@
"node": ">=6"
}
},
+ "node_modules/page": {
+ "version": "1.11.6",
+ "resolved": "https://registry.npmjs.org/page/-/page-1.11.6.tgz",
+ "integrity": "sha512-P6e2JfzkBrPeFCIPplLP7vDDiU84RUUZMrWdsH4ZBGJ8OosnwFkcUkBHp1DTIjuipLliw9yQn/ZJsXZvarsO+g==",
+ "dependencies": {
+ "path-to-regexp": "~1.2.1"
+ }
+ },
"node_modules/parse-database-url": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/parse-database-url/-/parse-database-url-0.3.0.tgz",
@@ -3603,6 +3618,19 @@
"node": "14 || >=16.14"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.2.1.tgz",
+ "integrity": "sha512-DBw9IhWfevR2zCVwEZURTuQNseCvu/Q9f5ZgqMCK0Rh61bDa4uyjPAOy9b55yKiPT59zZn+7uYKxmWwsguInwg==",
+ "dependencies": {
+ "isarray": "0.0.1"
+ }
+ },
+ "node_modules/path-to-regexp/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
+ },
"node_modules/path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
@@ -4038,6 +4066,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/remember": {
+ "version": "1.0.2",
+ "resolved": "git+ssh://git@github.com/DavidBruant/remember.git#4fad3c861473e9c56c410c18b79e45d4e718548e"
+ },
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
diff --git a/package.json b/package.json
index 46ed9cb6..e92a1779 100644
--- a/package.json
+++ b/package.json
@@ -33,14 +33,16 @@
},
"dependencies": {
"@fastify/static": "^7.0.1",
+ "baredux": "github:DavidBruant/baredux#v1.0.8",
"d3-fetch": "^3.0.1",
"date-fns": "^3.6.0",
+ "db-migrate": "^0.11.14",
+ "db-migrate-pg": "^1.5.2",
"fastify": "^4.26.2",
"knex": "^3.1.0",
"ky": "^1.2.2",
+ "page": "^1.11.6",
"pg": "^8.11.5",
- "db-migrate": "^0.11.14",
- "db-migrate-pg": "^1.5.2"
-
+ "remember": "github:DavidBruant/remember#v1.0.2"
}
}
diff --git a/rollup.config.js b/rollup.config.js
index 717b6f9b..2c0cbbe9 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -37,25 +37,13 @@ const plugins = ({cssOutput}) => ([
export default [
{
- input: 'scripts/front-end/saisie-especes.js',
+ input: 'scripts/front-end/main.js',
output: {
sourcemap: true,
format: 'es',
- file: 'build/bundle-saisie-especes.js'
+ file: 'build/rollup-bundle-pitchou.js'
},
- plugins: plugins({cssOutput: 'bundle-saisie-especes.css'}),
- watch: {
- clearScreen: false
- }
- },
- {
- input: 'scripts/front-end/suivi.js',
- output: {
- sourcemap: true,
- format: 'es',
- file: 'build/bundle-suivi.js'
- },
- plugins: plugins({cssOutput: 'bundle-suivi.css'}),
+ plugins: plugins({cssOutput: 'rollup-bundle-pitchou.css'}),
watch: {
clearScreen: false
}
diff --git a/saisie-especes.html b/saisie-especes.html
deleted file mode 100644
index cd67f900..00000000
--- a/saisie-especes.html
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
- Saisie espèces protégées
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/scripts/commun/constantes.js b/scripts/commun/constantes.js
new file mode 100644
index 00000000..5cc7cf20
--- /dev/null
+++ b/scripts/commun/constantes.js
@@ -0,0 +1,4 @@
+export const authorizedEmailDomains = new Set([
+ 'developpement-durable.gouv.fr',
+ 'beta.gouv.fr'
+])
\ No newline at end of file
diff --git a/scripts/front-end/actions/main.js b/scripts/front-end/actions/main.js
new file mode 100644
index 00000000..ab46338e
--- /dev/null
+++ b/scripts/front-end/actions/main.js
@@ -0,0 +1,31 @@
+//@ts-check
+
+import store from '../store.js';
+import remember from 'remember'
+
+const PITCHOU_SECRET_STORAGE_KEY = 'secret-pitchou'
+
+export function init(){
+
+ return remember(PITCHOU_SECRET_STORAGE_KEY)
+ .then(secret => {
+ if(secret){
+ // @ts-ignore
+ store.mutations.setSecret(secret)
+ }
+ })
+}
+
+export async function secretFromURL(){
+ const secret = new URLSearchParams(location.search).get("secret")
+
+ if(secret){
+ const newURL = new URL(location.href)
+ newURL.searchParams.delete("secret")
+
+ history.replaceState(null, "", newURL)
+ store.mutations.setSecret(secret)
+
+ return remember(PITCHOU_SECRET_STORAGE_KEY, secret)
+ }
+}
\ No newline at end of file
diff --git a/scripts/front-end/components/LoginViaEmail.svelte b/scripts/front-end/components/LoginViaEmail.svelte
new file mode 100644
index 00000000..72c80bf6
--- /dev/null
+++ b/scripts/front-end/components/LoginViaEmail.svelte
@@ -0,0 +1,33 @@
+
+
+
+Connexion par email
+
+Saisissez votre adresse email ici et vous recevrez un email avec un lien secret pour vous connecter à Pitchou.
+⚠️ Seules les adresses emails venant d'un de ces domaine peuvent recevoir un lien de connexion :
+ {#each [...authorizedEmailDomains] as authorizedEmailDomain, i}
+ {#if i !== 0} , {/if}
+ {authorizedEmailDomain}
+ {/each}
+
+
+
+
+
+
diff --git a/scripts/front-end/components/SuiviInstructeur.svelte b/scripts/front-end/components/SuiviInstructeur.svelte
index e650cd01..77b0bc12 100644
--- a/scripts/front-end/components/SuiviInstructeur.svelte
+++ b/scripts/front-end/components/SuiviInstructeur.svelte
@@ -1,7 +1,5 @@
+
+ Saisie espèces
+
+
Suivi instructeur
diff --git a/scripts/front-end/main.js b/scripts/front-end/main.js
new file mode 100644
index 00000000..64805f99
--- /dev/null
+++ b/scripts/front-end/main.js
@@ -0,0 +1,279 @@
+//@ts-check
+
+import page from 'page'
+
+import {json, dsv} from 'd3-fetch'
+
+import LoginViaEmail from './components/LoginViaEmail.svelte';
+import SuiviInstructeur from './components/SuiviInstructeur.svelte';
+import SaisieEspèces from './components/SaisieEspèces.svelte';
+import { replaceComponent } from './routeComponentLifeCycle.js'
+import store from './store.js'
+
+import {init, secretFromURL} from './actions/main.js'
+import {envoiEmailConnexion} from './serveur.js'
+
+import { authorizedEmailDomains } from '../commun/constantes.js';
+
+import '../types.js'
+
+const svelteTarget = document.querySelector('.svelte-main')
+
+
+page('/', async () => {
+ console.info('route', '/')
+ const secret = await secretFromURL().then(() => store.state.secret)
+
+ if(secret){
+ json(`/dossiers?secret=${secret}`).then(dossiers => {
+ function mapStateToProps(){
+ return {dossiers}
+ }
+
+ console.log('dossiers', dossiers)
+
+ const suiviInstructeur = new SuiviInstructeur({
+ target: svelteTarget,
+ props: mapStateToProps()
+ });
+
+ replaceComponent(suiviInstructeur, mapStateToProps)
+ })
+ }
+ else{
+ function mapStateToProps(){
+ return {
+ authorizedEmailDomains,
+ envoiEmailConnexion: envoiEmailConnexion
+ }
+ }
+
+ const loginViaEmail = new LoginViaEmail({
+ target: svelteTarget,
+ props: mapStateToProps()
+ });
+
+ replaceComponent(loginViaEmail, mapStateToProps)
+ }
+
+})
+
+page('/saisie-especes', async () => {
+ /**
+ * @param {string} x
+ * @returns {x is ClassificationEtreVivant}
+ */
+ function isClassif(x){
+ // @ts-expect-error indeed
+ return classificationEtreVivants.includes(x)
+ }
+
+ function getURL(selector){
+ const element = document.head.querySelector(selector)
+
+ if(!element){
+ throw new TypeError(`Élément ${selector} manquant`)
+ }
+
+ const hrefAttribute = element.getAttribute('href')
+
+ if(!hrefAttribute){
+ throw new TypeError(`Attribut "href" manquant sur ${selector}`)
+ }
+
+ return hrefAttribute
+ }
+
+ /** @type { [any[], ActivitéMenançante[], MéthodeMenançante[], TransportMenançant[]] } */
+ // @ts-ignore
+ const [dataEspèces, activites, methodes, transports] = await Promise.all([
+ dsv(";", getURL('link#especes-data')),
+ dsv(";", getURL('link#activites-data')),
+ dsv(";", getURL('link#methodes-data')),
+ dsv(";", getURL('link#transports-data')),
+ ])
+
+ console.log(dataEspèces, activites, methodes, transports)
+
+ /** @type {readonly ClassificationEtreVivant[]} */
+ const classificationEtreVivants = Object.freeze(["oiseau", "faune non-oiseau", "flore"])
+
+
+
+ /** @type {Map} */
+ const activitesParClassificationEtreVivant = new Map()
+ for(const activite of activites){
+ const classif = activite['Espèces']
+
+ if(!classif.trim() && !activite['Code']){
+ // ignore empty lines (certainly comments)
+ break;
+ }
+
+ if(!isClassif(classif)){
+ throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
+ }
+
+ const classifActivz = activitesParClassificationEtreVivant.get(classif) || []
+ classifActivz.push(activite)
+ activitesParClassificationEtreVivant.set(classif, classifActivz)
+ }
+
+ /** @type {Map} */
+ const méthodesParClassificationEtreVivant = new Map()
+ for(const methode of methodes){
+ const classif = methode['Espèces']
+
+ if(!classif.trim() && !methode['Code']){
+ // ignore empty lines (certainly comments)
+ break;
+ }
+
+ if(!isClassif(classif)){
+ throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
+ }
+
+ const classifMeth = méthodesParClassificationEtreVivant.get(classif) || []
+ classifMeth.push(methode)
+ méthodesParClassificationEtreVivant.set(classif, classifMeth)
+ }
+
+ /** @type {Map} */
+ const transportsParClassificationEtreVivant = new Map()
+ for(const transport of transports){
+ const classif = transport['Espèces']
+
+ if(!classif.trim() && !transport['Code']){
+ // ignore empty lines (certainly comments)
+ break;
+ }
+
+ if(!isClassif(classif)){
+ throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
+ }
+
+ const classifTrans = transportsParClassificationEtreVivant.get(classif) || []
+ classifTrans.push(transport)
+ transportsParClassificationEtreVivant.set(classif, classifTrans)
+ }
+
+
+
+ const espèceByCD_NOM = new Map()
+ dataEspèces.forEach(d => {
+ espèceByCD_NOM.set(d["CD_NOM"], d)
+ })
+ console.log(espèceByCD_NOM)
+
+ /** @type { Espèce[] } */
+ const listeEspècesProtégées = [...espèceByCD_NOM.values()]
+
+ const filtreParClassification = new Map([
+ ["oiseau", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE, CLASSE}) => {
+ return REGNE === 'Animalia' && CLASSE === 'Aves'
+ })],
+ ["faune non-oiseau", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE, CLASSE}) => {
+ return REGNE === 'Animalia' && CLASSE !== 'Aves'
+ })],
+ ["flore", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE}) => {
+ return REGNE === 'Plantae'
+ })]
+ ])
+
+ const espècesProtégéesParClassification = new Map(
+ [...filtreParClassification].map(([classif, filtre]) => ([classif, listeEspècesProtégées.filter(filtre)]))
+ )
+
+ console.log('espècesProtégéesParClassification', espècesProtégéesParClassification)
+
+ /**
+ *
+ * @param {string} s // utf-8-encoded base64 string
+ * @returns {string} // cleartext string
+ */
+ function b64ToUTF8(s) {
+ return decodeURIComponent(escape(atob(s)))
+ }
+
+
+ /**
+ *
+ * @param { DescriptionMenaceEspècesJSON } descriptionMenacesEspècesJSON
+ * @returns { DescriptionMenaceEspèce[] }
+ */
+ function descriptionMenacesEspècesFromJSON(descriptionMenacesEspècesJSON){
+ return descriptionMenacesEspècesJSON.map(({classification, etresVivantsAtteints}) => {
+ console.log('classification, etresVivantsAtteints', classification, etresVivantsAtteints)
+ return {
+ classification,
+ etresVivantsAtteints: etresVivantsAtteints.map(({espece, activité, méthode, transport, ...rest}) => ({
+ espece: espèceByCD_NOM.get(espece),
+ activité: activites.find((a) => a.Code === activité),
+ méthode: methodes.find((m) => m.Code === méthode),
+ transport: transports.find((t) => t.Espèces === classification && t.Code === transport),
+ ...rest
+ })),
+
+ }
+ })
+ }
+
+ function importDescriptionMenacesEspècesFromURL(){
+ const urlData = new URLSearchParams(location.search).get('data')
+ if(urlData){
+ try{
+ const data = JSON.parse(b64ToUTF8(urlData))
+ const desc = descriptionMenacesEspècesFromJSON(data)
+ console.log('desc', desc)
+ return desc
+ }
+ catch(e){
+ console.error('Parsing error', e, urlData)
+ return undefined
+ }
+ }
+ }
+
+ function mapStateToProps(){
+ return {
+ espècesProtégéesParClassification,
+ activitesParClassificationEtreVivant,
+ méthodesParClassificationEtreVivant,
+ transportsParClassificationEtreVivant,
+ /** @type {DescriptionMenaceEspèce[]} */
+ // @ts-ignore
+ descriptionMenacesEspèces: importDescriptionMenacesEspècesFromURL() || [
+ {
+ classification: "oiseau", // Type d'espèce menacée
+ etresVivantsAtteints: [],
+ activité: undefined, // Activité menaçante
+ méthode: undefined, // Méthode menaçante
+ transport: undefined // Transport impliqué dans la menace
+ },
+ {
+ classification: "faune non-oiseau",
+ etresVivantsAtteints: [],
+ activité: undefined, // Activité menaçante
+ méthode: undefined, // Méthode menaçante
+ transport: undefined // Transport impliqué dans la menace
+ },
+ {
+ classification: "flore",
+ etresVivantsAtteints: [],
+ activité: undefined, // Activité menaçante
+ }
+ ]
+ }
+ }
+
+
+ const saisieEspèces = new SaisieEspèces({
+ target: svelteTarget,
+ props: mapStateToProps()
+ });
+
+ replaceComponent(saisieEspèces, mapStateToProps)
+})
+
+init()
+.then(() => page.start())
\ No newline at end of file
diff --git a/scripts/front-end/routeComponentLifeCycle.js b/scripts/front-end/routeComponentLifeCycle.js
new file mode 100644
index 00000000..8afcacc7
--- /dev/null
+++ b/scripts/front-end/routeComponentLifeCycle.js
@@ -0,0 +1,42 @@
+//@ts-check
+
+import { SvelteComponent } from "svelte";
+import store from "./store.js";
+
+/** @typedef {import("./store.js").PitchouState} PitchouState */
+/** @typedef {(state: PitchouState) => any} MapStateToPropsFunction */
+
+/** @type {SvelteComponent} */
+let currentComponent;
+/** @type {MapStateToPropsFunction} */
+let currentMapStateToProps = (_) => {};
+
+/**
+ *
+ * @param {SvelteComponent} newComponent
+ * @param {MapStateToPropsFunction} newMapStateToProps
+ */
+export function replaceComponent(newComponent, newMapStateToProps) {
+ if (!newMapStateToProps) {
+ throw new Error("Missing _mapStateToProps in replaceComponent");
+ }
+
+ if (currentComponent) currentComponent.$destroy();
+
+ currentComponent = newComponent;
+ currentMapStateToProps = newMapStateToProps;
+}
+
+/**
+ *
+ * @param {PitchouState} state
+ */
+function render(state) {
+ const props = currentMapStateToProps(state);
+ // @ts-ignore
+ if (props) {
+ currentComponent.$set(props);
+ }
+}
+
+store.subscribe(render);
diff --git a/scripts/front-end/saisie-especes.js b/scripts/front-end/saisie-especes.js
deleted file mode 100644
index f4cfa251..00000000
--- a/scripts/front-end/saisie-especes.js
+++ /dev/null
@@ -1,216 +0,0 @@
-//@ts-check
-
-import { dsv } from 'd3-fetch';
-
-import SaisieEspèces from './components/SaisieEspèces.svelte';
-
-import '../types.js'
-
-/**
- * @param {string} x
- * @returns {x is ClassificationEtreVivant}
- */
-function isClassif(x){
- // @ts-expect-error indeed
- return classificationEtreVivants.includes(x)
-}
-
-function getURL(selector){
- const element = document.head.querySelector(selector)
-
- if(!element){
- throw new TypeError(`Élément ${selector} manquant`)
- }
-
- const hrefAttribute = element.getAttribute('href')
-
- if(!hrefAttribute){
- throw new TypeError(`Attribut "href" manquant sur ${selector}`)
- }
-
- return hrefAttribute
-}
-
-/** @type { [any[], ActivitéMenançante[], MéthodeMenançante[], TransportMenançant[]] } */
-// @ts-ignore
-const [dataEspèces, activites, methodes, transports] = await Promise.all([
- dsv(";", getURL('link#especes-data')),
- dsv(";", getURL('link#activites-data')),
- dsv(";", getURL('link#methodes-data')),
- dsv(";", getURL('link#transports-data')),
-])
-
-console.log(dataEspèces, activites, methodes, transports)
-
-/** @type {readonly ClassificationEtreVivant[]} */
-const classificationEtreVivants = Object.freeze(["oiseau", "faune non-oiseau", "flore"])
-
-
-
-/** @type {Map} */
-const activitesParClassificationEtreVivant = new Map()
-for(const activite of activites){
- const classif = activite['Espèces']
-
- if(!classif.trim() && !activite['Code']){
- // ignore empty lines (certainly comments)
- break;
- }
-
- if(!isClassif(classif)){
- throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
- }
-
- const classifActivz = activitesParClassificationEtreVivant.get(classif) || []
- classifActivz.push(activite)
- activitesParClassificationEtreVivant.set(classif, classifActivz)
-}
-
-/** @type {Map} */
-const méthodesParClassificationEtreVivant = new Map()
-for(const methode of methodes){
- const classif = methode['Espèces']
-
- if(!classif.trim() && !methode['Code']){
- // ignore empty lines (certainly comments)
- break;
- }
-
- if(!isClassif(classif)){
- throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
- }
-
- const classifMeth = méthodesParClassificationEtreVivant.get(classif) || []
- classifMeth.push(methode)
- méthodesParClassificationEtreVivant.set(classif, classifMeth)
-}
-
-/** @type {Map} */
-const transportsParClassificationEtreVivant = new Map()
-for(const transport of transports){
- const classif = transport['Espèces']
-
- if(!classif.trim() && !transport['Code']){
- // ignore empty lines (certainly comments)
- break;
- }
-
- if(!isClassif(classif)){
- throw new TypeError(`Classification d'espèce non reconnue: ${classif}. Les choix sont : ${classificationEtreVivants.join(', ')}`)
- }
-
- const classifTrans = transportsParClassificationEtreVivant.get(classif) || []
- classifTrans.push(transport)
- transportsParClassificationEtreVivant.set(classif, classifTrans)
-}
-
-
-
-const espèceByCD_NOM = new Map()
-dataEspèces.forEach(d => {
- espèceByCD_NOM.set(d["CD_NOM"], d)
-})
-console.log(espèceByCD_NOM)
-
-/** @type { Espèce[] } */
-const listeEspècesProtégées = [...espèceByCD_NOM.values()]
-
-const filtreParClassification = new Map([
- ["oiseau", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE, CLASSE}) => {
- return REGNE === 'Animalia' && CLASSE === 'Aves'
- })],
- ["faune non-oiseau", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE, CLASSE}) => {
- return REGNE === 'Animalia' && CLASSE !== 'Aves'
- })],
- ["flore", ((/** @type {{REGNE: Règne, CLASSE: Classe}} */ {REGNE}) => {
- return REGNE === 'Plantae'
- })]
-])
-
-const espècesProtégéesParClassification = new Map(
- [...filtreParClassification].map(([classif, filtre]) => ([classif, listeEspècesProtégées.filter(filtre)]))
-)
-
-console.log('espècesProtégéesParClassification', espècesProtégéesParClassification)
-
-/**
- *
- * @param {string} s // utf-8-encoded base64 string
- * @returns {string} // cleartext string
- */
-function b64ToUTF8(s) {
- return decodeURIComponent(escape(atob(s)))
-}
-
-
-/**
- *
- * @param { DescriptionMenaceEspècesJSON } descriptionMenacesEspècesJSON
- * @returns { DescriptionMenaceEspèce[] }
- */
-function descriptionMenacesEspècesFromJSON(descriptionMenacesEspècesJSON){
- return descriptionMenacesEspècesJSON.map(({classification, etresVivantsAtteints}) => {
- console.log('classification, etresVivantsAtteints', classification, etresVivantsAtteints)
- return {
- classification,
- etresVivantsAtteints: etresVivantsAtteints.map(({espece, activité, méthode, transport, ...rest}) => ({
- espece: espèceByCD_NOM.get(espece),
- activité: activites.find((a) => a.Code === activité),
- méthode: methodes.find((m) => m.Code === méthode),
- transport: transports.find((t) => t.Espèces === classification && t.Code === transport),
- ...rest
- })),
-
- }
- })
-}
-
-function importDescriptionMenacesEspècesFromURL(){
- const urlData = new URLSearchParams(location.search).get('data')
- if(urlData){
- try{
- const data = JSON.parse(b64ToUTF8(urlData))
- const desc = descriptionMenacesEspècesFromJSON(data)
- console.log('desc', desc)
- return desc
- }
- catch(e){
- console.error('Parsing error', e, urlData)
- return undefined
- }
- }
-}
-
-
-const app = new SaisieEspèces({
- target: document.querySelector('.svelte-main'),
- props: {
- espècesProtégéesParClassification,
- activitesParClassificationEtreVivant,
- méthodesParClassificationEtreVivant,
- transportsParClassificationEtreVivant,
- /** @type {DescriptionMenaceEspèce[]} */
- // @ts-ignore
- descriptionMenacesEspèces: importDescriptionMenacesEspècesFromURL() || [
- {
- classification: "oiseau", // Type d'espèce menacée
- etresVivantsAtteints: [],
- activité: undefined, // Activité menaçante
- méthode: undefined, // Méthode menaçante
- transport: undefined // Transport impliqué dans la menace
- },
- {
- classification: "faune non-oiseau",
- etresVivantsAtteints: [],
- activité: undefined, // Activité menaçante
- méthode: undefined, // Méthode menaçante
- transport: undefined // Transport impliqué dans la menace
- },
- {
- classification: "flore",
- etresVivantsAtteints: [],
- activité: undefined, // Activité menaçante
- }
- ]
- }
-});
diff --git a/scripts/front-end/serveur.js b/scripts/front-end/serveur.js
new file mode 100644
index 00000000..1655a94f
--- /dev/null
+++ b/scripts/front-end/serveur.js
@@ -0,0 +1,13 @@
+
+import {json, dsv} from 'd3-fetch'
+
+
+/**
+ *
+ * @param {string} email
+ */
+export function envoiEmailConnexion(email){
+ return json(`/envoi-email-connexion?email=${encodeURIComponent(email)}`, {
+ method: 'POST'
+ })
+}
\ No newline at end of file
diff --git a/scripts/front-end/store.js b/scripts/front-end/store.js
new file mode 100644
index 00000000..a383a59e
--- /dev/null
+++ b/scripts/front-end/store.js
@@ -0,0 +1,45 @@
+//@ts-check
+
+import Store from 'baredux'
+/**
+ * Un store baredux a pour vocation de refléter notamment le modèle mental de la
+ * personne face à notre application. Le store stocke donc principalement des données (et parfois des singletons)
+ * Il stocke aussi parfois des promesses (pour permettre d'afficher des loaders dans les composants)
+ *
+ * Dans un store Baredux, les mutations sont synchrones
+ * S'il manque des informations, attendre la résolution de la promesse avant d'appeler une mutation
+ * (à moins que la valeur soit délibérément une promesse)
+ *
+ */
+// DO NOT import x from 'remember' // do it in an action instead
+// DO NOT import x from './actions/*.js' // you're making an action, so add an action instead
+
+import '../types.js'
+
+
+/**
+ * @typedef {Object} PitchouState
+ * @property {string} [secret]
+ */
+
+/** @type {PitchouState} */
+const state = {
+ secret: undefined
+}
+
+const mutations = {
+ /**
+ * @param {PitchouState} state
+ * @param {PitchouState['secret']} secret
+ */
+ setSecret(state, secret) {
+ state.secret = secret
+ }
+}
+
+/** @typedef { typeof mutations } PitchouMutations */
+
+/** @type { import('baredux').BareduxStore } */
+const store = Store({ state, mutations })
+
+export default store
diff --git a/scripts/front-end/suivi.js b/scripts/front-end/suivi.js
deleted file mode 100644
index 06b8a99e..00000000
--- a/scripts/front-end/suivi.js
+++ /dev/null
@@ -1,26 +0,0 @@
-//@ts-check
-
-import {json} from 'd3-fetch'
-
-import SuiviInstructeur from './components/SuiviInstructeur.svelte';
-
-import '../types.js'
-const secret = new URLSearchParams(location.search).get("secret")
-
-json(`/dossiers?secret=${secret}`).then(dossiers => {
- const newURL = new URL(location.href)
- newURL.searchParams.delete("secret")
-
- history.replaceState(null, "", newURL)
-
- console.log('dossiers', dossiers)
- const app = new SuiviInstructeur({
- target: document.querySelector('.svelte-main'),
- props: {
- dossiers
- }
- });
-})
-
-
-
diff --git "a/scripts/server/cr\303\251er-premi\303\250re-personne.js" "b/scripts/server/cr\303\251er-premi\303\250re-personne.js"
deleted file mode 100644
index f551b947..00000000
--- "a/scripts/server/cr\303\251er-premi\303\250re-personne.js"
+++ /dev/null
@@ -1,19 +0,0 @@
-import { créerPersonne } from "./database.js";
-
-const personneData = {
- nom: 'Rispal',
- prénoms: 'Vanessa',
- email: 'vanessa.rispal@developpement-durable.gouv.fr',
- code_accès: Math.random().toString(36).slice(2)
-}
-
-export default function(){
- return créerPersonne(personneData)
- .then(() => {
- console.log('Première personne créée 🎉')
- console.log(`URL d'accès:`, `https://especes-protegees.osc-fr1.scalingo.io/?secret=${personneData.code_accès}`)
- })
- .catch(err => {
- console.error('Erreur lors de la création de la première personne', err)
- })
-}
\ No newline at end of file
diff --git a/scripts/server/database.js b/scripts/server/database.js
index c1896037..a26e8338 100644
--- a/scripts/server/database.js
+++ b/scripts/server/database.js
@@ -2,6 +2,9 @@
import knex from 'knex';
+/** @typedef {import('../types/database/public/Personne.js').default} Personne */
+/** @typedef {import('../types/database/public/Dossier.js').default} Dossier */
+
const DATABASE_URL = process.env.DATABASE_URL
if(!DATABASE_URL){
throw new TypeError(`Variable d'environnement DATABASE_URL manquante`)
@@ -19,15 +22,12 @@ const database = knex({
export function créerPersonne(personne){
return database('personne')
.insert(personne)
- .catch(err => {
- console.error('Error trying to create a person', err)
- throw err
- })
}
+
/**
*
- * @param {string} code_accès
- * @returns {Promise | Promise}
+ * @param {Personne['code_accès']} code_accès
+ * @returns {Promise | Promise}
*/
export function getPersonneByCode(code_accès) {
return database('personne')
@@ -36,7 +36,56 @@ export function getPersonneByCode(code_accès) {
.first()
}
+/**
+ *
+ * @param {Personne['email']} email
+ * @returns {Promise | Promise}
+ */
+export function getPersonneByEmail(email) {
+ return database('personne')
+ .where({ email })
+ .select()
+ .first()
+}
+
+/**
+ *
+ * @returns {Promise}
+ */
export function getAllDossier() {
return database('dossier')
.select()
+}
+
+/**
+ *
+ * @param {Personne['email']} email
+ * @param {Personne['code_accès']} code_accès
+ * @returns
+ */
+function updateCodeAccès(email, code_accès){
+ return database('personne')
+ .where({ email })
+ .update({code_accès})
+}
+
+/**
+ *
+ * @param {Personne['email']} email
+ * @returns {Promise}
+ */
+export function créerPersonneOuMettreÀJourCodeAccès(email){
+ const codeAccès = Math.random().toString(36).slice(2)
+
+ return créerPersonne({
+ nom: '',
+ prénoms: '',
+ email,
+ code_accès: codeAccès
+ })
+ .catch(err => {
+ // suppose qu'il y a une erreur parce qu'une personne avec cette adresse email existe déjà
+ return updateCodeAccès(email, codeAccès)
+ })
+ .then(() => codeAccès)
}
\ No newline at end of file
diff --git a/scripts/server/emails.js b/scripts/server/emails.js
new file mode 100644
index 00000000..595a7fc2
--- /dev/null
+++ b/scripts/server/emails.js
@@ -0,0 +1,15 @@
+/**
+ * Pour le moment, cette fonction est complètement fake, mais à terme,
+ * elle enverra un email
+ *
+ * @param {string} email
+ * @param {string} lienConnexion
+ * @returns {Promise}
+ */
+export function envoyerEmailConnexion(email, lienConnexion){
+
+ return Promise.resolve({
+ email,
+ lienConnexion
+ })
+}
\ No newline at end of file
diff --git a/scripts/server/main.js b/scripts/server/main.js
index b4d2a485..efbb853e 100644
--- a/scripts/server/main.js
+++ b/scripts/server/main.js
@@ -5,11 +5,10 @@ import path from 'node:path'
import Fastify from 'fastify'
import fastatic from '@fastify/static'
-import créerPremièrePersonne from './créer-première-personne.js'
+import { getPersonneByCode, getAllDossier, créerPersonneOuMettreÀJourCodeAccès } from './database.js'
-import { getPersonneByCode, getAllDossier } from './database.js'
-
-const fastify = Fastify({logger: true})
+import { authorizedEmailDomains } from '../commun/constantes.js'
+import { envoyerEmailConnexion } from './emails.js'
const PORT = parseInt(process.env.PORT || '')
if(!PORT){
@@ -27,15 +26,21 @@ if(!DEMARCHE_NUMBER){
}
+
+
+const fastify = Fastify({logger: true})
+
fastify.register(fastatic, {
root: path.resolve(import.meta.dirname, '..', '..'),
extensions: ['html']
})
+fastify.get('/saisie-especes', (request, reply) => {
+ reply.sendFile('index.html')
+})
-créerPremièrePersonne()
// Privileged routes
-fastify.get('/dossiers', async function handler (request, reply) {
+fastify.get('/dossiers', async function (request, reply) {
// @ts-ignore
const code_accès = request.query.secret
if (code_accès) {
@@ -46,10 +51,36 @@ fastify.get('/dossiers', async function handler (request, reply) {
reply.code(403).send("Code d'accès non valide.")
}
} else {
- reply.code(400).send("Paramètre secret manquant dans l'URL.")
+ reply.code(400).send(`Paramètre 'secret' manquant dans l'URL`)
+ }
+})
+
+fastify.post('/envoi-email-connexion', async function (request, reply) {
+ // @ts-ignore
+ const email = decodeURIComponent(request.query.email)
+
+ if(!email){
+ return reply.code(400).send(`Paramètre 'email' manquant dans l'URL`)
}
+
+ const [name, domain] = email.split('@')
+
+ if(!authorizedEmailDomains.has(domain)){
+ return reply.code(403).send(`Le domaine '${domain}' ne fait pas partie des domaines autorisés`)
+ }
+ else{
+ // le domaine est autorisé
+ return créerPersonneOuMettreÀJourCodeAccès(email)
+ .then(codeAccès => {
+ const lienConnexion = `${request.headers.origin}/?secret=${codeAccès}`
+ // PPP enlever le return quand on enverra pour de vrai un email
+ return envoyerEmailConnexion(email, lienConnexion)
+ })
+ }
+
})
+
// Run the server!
try {
await fastify.listen({ port: PORT, host: '0.0.0.0' })