From d3cf130c23f28fa2ae78c1b1007a62fa643da62b Mon Sep 17 00:00:00 2001 From: horek Date: Thu, 23 May 2024 17:50:21 +0200 Subject: [PATCH 1/7] feat: add forgot and reset password pages, with token register in an index --- .../components/login/FormForgotPassword.tsx | 123 ++++ webapp-next/components/login/FormLogin.tsx | 16 +- .../components/login/FormResetPassword.tsx | 183 +++++ webapp-next/package.json | 1 + webapp-next/pages/api/auth/forgot-password.ts | 94 +++ webapp-next/pages/api/auth/reset-password.ts | 74 ++ webapp-next/pages/login/forgot-password.tsx | 16 + webapp-next/pages/login/reset-password.tsx | 16 + webapp-next/utils/tools.ts | 654 ++++++++++-------- webapp-next/yarn.lock | 23 +- 10 files changed, 876 insertions(+), 324 deletions(-) create mode 100644 webapp-next/components/login/FormForgotPassword.tsx create mode 100644 webapp-next/components/login/FormResetPassword.tsx create mode 100644 webapp-next/pages/api/auth/forgot-password.ts create mode 100644 webapp-next/pages/api/auth/reset-password.ts create mode 100644 webapp-next/pages/login/forgot-password.tsx create mode 100644 webapp-next/pages/login/reset-password.tsx diff --git a/webapp-next/components/login/FormForgotPassword.tsx b/webapp-next/components/login/FormForgotPassword.tsx new file mode 100644 index 0000000..86a4e5d --- /dev/null +++ b/webapp-next/components/login/FormForgotPassword.tsx @@ -0,0 +1,123 @@ +import { + Box, + Button, + Divider, + FormControl, + FormErrorMessage, + FormLabel, + Heading, + Image, + Input, + InputGroup, + InputLeftElement, + Link, + Text, +} from "@chakra-ui/react"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import { useForm, type SubmitHandler } from "react-hook-form"; +import useSWRMutation from "swr/mutation"; + +type FormForgotPassword = { + username: string; +}; + +export async function auth(url: string, { arg }: { arg: T }) { + return fetch(url, { + method: "POST", + body: JSON.stringify(arg), + headers: { "Content-Type": "application/json" }, + }); +} + +export const FormForgotPassword = () => { + const { trigger: triggerForgotPassword, isMutating } = useSWRMutation( + "/api/auth/forgot-password", + auth<{ username: string }> + ); + + const { + handleSubmit, + register, + formState: { errors, isSubmitting }, + } = useForm(); + + const onSubmit: SubmitHandler = ({ username }) => { + triggerForgotPassword({ username }); + }; + + return ( + + + + Mot de passe oublié + +
+ + + Identifiant + + + + User Icon + + + + + + {errors.username && errors.username.message} + + +
+ + + + Retour à la connexion + + +
+
+ ); +}; diff --git a/webapp-next/components/login/FormLogin.tsx b/webapp-next/components/login/FormLogin.tsx index b8ed1df..f6e9900 100644 --- a/webapp-next/components/login/FormLogin.tsx +++ b/webapp-next/components/login/FormLogin.tsx @@ -5,6 +5,7 @@ import { AlertTitle, Box, Button, + Divider, FormControl, FormLabel, Heading, @@ -26,6 +27,7 @@ import { useDisclosure, } from "@chakra-ui/react"; import cookie from "js-cookie"; +import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; import useSWRMutation from "swr/mutation"; @@ -253,8 +255,7 @@ export const FormLogin = () => { + + + + Mot de passe oublié ? + + ); @@ -395,7 +401,7 @@ export const FormLogin = () => { mx={"auto"} mt={[8, 0]} > - + (url: string, { arg }: { arg: T }) { + return fetch(url, { + method: "POST", + body: JSON.stringify(arg), + headers: { "Content-Type": "application/json" }, + }); +} + +export const FormResetPassword = () => { + const router = useRouter(); + + const [errorForm, setErrorForm] = useState(null); + + const { trigger: triggerResetPassword } = useSWRMutation( + "/api/auth/reset-password", + auth<{ password: string; token: string }> + ); + + const { + handleSubmit, + register, + setError, + watch, + formState: { errors, isSubmitting }, + } = useForm(); + + const onSubmit: SubmitHandler = async ({ password }) => { + if (!router.query.token) setError("password", { message: "Invalid token" }); + const response = await triggerResetPassword({ + password, + token: router.query.token as string, + }); + if (response && response.ok) { + router.push("/login"); + } else { + if (response?.status === 404) { + setErrorForm("Le token n'est pas valide"); + } else if (response?.status === 401) { + setErrorForm("Le token a expiré"); + } + } + }; + + return ( + + + + Mot de passe oublié + +
+ + + Nouveau mot de passe + + + + User Icon + + + + + {errors.password && errors.password.message} + + + + + Confirmer le mot de passe + + + + User Icon + + + value === watch("password") || + "Les mots de passe ne correspondent pas", + })} + /> + + + {errors.confirmPassword && errors.confirmPassword.message} + + + {errorForm && ( + + + {errorForm} + + )} + +
+
+
+ ); +}; diff --git a/webapp-next/package.json b/webapp-next/package.json index b54d27c..5e38956 100644 --- a/webapp-next/package.json +++ b/webapp-next/package.json @@ -38,6 +38,7 @@ "react-chartjs-2": "^5.2.0", "react-datepicker": "^6.0.0", "react-dom": "18.3.1", + "react-hook-form": "^7.51.5", "superjson": "^2.0.0", "swr": "^2.1.5", "typescript": "5.4.5" diff --git a/webapp-next/pages/api/auth/forgot-password.ts b/webapp-next/pages/api/auth/forgot-password.ts new file mode 100644 index 0000000..c3224d8 --- /dev/null +++ b/webapp-next/pages/api/auth/forgot-password.ts @@ -0,0 +1,94 @@ +import { sendMail } from "@/utils/mailter"; +import { + getCodeEmailHtml, + ELASTIC_API_KEY_NAME, + getResetPasswordEmailHtml, +} from "@/utils/tools"; +import { Client } from "@elastic/elasticsearch"; +import fs from "fs"; +import type { NextApiRequest, NextApiResponse } from "next"; +import path from "path"; +import rateLimit from "@/utils/rate-limit"; +import crypto from "crypto"; + +const limiter = rateLimit({ + interval: 60 * 1000, // 60 seconds + uniqueTokenPerInterval: 50, // Max 50 users per second +}); + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === "POST") { + const { username } = req.body; + + // const forwarded = req.headers["x-forwarded-for"]; + + // const userIp = + // typeof forwarded === "string" + // ? forwarded.split(/, /)[0] + // : req.socket.remoteAddress; + + // // Rate limiting to prevent brute force auth + // try { + // await limiter.check(res, 5, userIp as string); // 5 requests max per minute + // } catch (e: any) { + // return res.status(e.statusCode).end(e.message); + // } + + const adminClient = new Client({ + node: process.env.ELASTIC_HOST, + auth: { + username: process.env.ELASTIC_USERNAME as string, + password: process.env.ELASTIC_PASSWORD as string, + }, + tls: { + ca: fs.readFileSync(path.resolve(process.cwd(), "./certs/ca/ca.crt")), + rejectUnauthorized: false, + }, + }); + + try { + const currentUser = await adminClient.security.getUser({ username }); + + if (currentUser[username] === undefined) { + res.status(401).end(); + } + + const resetToken = crypto.randomBytes(32).toString("hex"); + + const baseUrl = `${req.headers["x-forwarded-proto"]}://${req.headers.host}`; + + await sendMail( + "CM2D - Réinitialisation de votre mot de passe", + username, + getResetPasswordEmailHtml( + `${baseUrl}/login/reset-password?token=${resetToken}` + ), + `Le lien de réinitialisation de votre mot de passe est : ${baseUrl}/login/reset-password?token=${resetToken}` + ); + + adminClient.create({ + index: "cm2d_reset_tokens", + id: resetToken, + body: { + username, + token: resetToken, + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + }, + }); + + res.status(200).send({ response: "ok" }); + } catch (error: any) { + if (error.statusCode === 401) { + res.status(401).end(); + } else { + res.status(500).end(); + } + } + } else { + res.setHeader("Allow", "POST"); + res.status(405).end("Method Not Allowed"); + } +} diff --git a/webapp-next/pages/api/auth/reset-password.ts b/webapp-next/pages/api/auth/reset-password.ts new file mode 100644 index 0000000..03c3a1b --- /dev/null +++ b/webapp-next/pages/api/auth/reset-password.ts @@ -0,0 +1,74 @@ +import { Client } from "@elastic/elasticsearch"; +import fs from "fs"; +import type { NextApiRequest, NextApiResponse } from "next"; +import path from "path"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === "POST") { + const { token, password } = req.body as { + token: string; + password: string; + }; + + const adminClient = new Client({ + node: process.env.ELASTIC_HOST, + auth: { + username: process.env.ELASTIC_USERNAME as string, + password: process.env.ELASTIC_PASSWORD as string, + }, + tls: { + ca: fs.readFileSync(path.resolve(process.cwd(), "./certs/ca/ca.crt")), + rejectUnauthorized: false, + }, + }); + + try { + const currentResetToken = await adminClient.get<{ + username: string; + token: string; + expiresAt: Date; + }>({ + index: "cm2d_reset_tokens", + id: token, + }); + + if (!currentResetToken || !currentResetToken._source) { + res.status(404).end(); + return; + } + + if ( + new Date(currentResetToken._source?.expiresAt) < new Date(Date.now()) + ) { + res.status(401).end(); + return; + } + + await adminClient.security.changePassword({ + username: currentResetToken._source.username, + body: { + password, + }, + }); + + adminClient.delete({ + index: "cm2d_reset_tokens", + id: token, + }); + + res.status(200).send({ response: "ok" }); + } catch (error: any) { + if (error.statusCode === 401) { + res.status(401).end(); + } else { + res.status(500).end(); + } + } + } else { + res.setHeader("Allow", "POST"); + res.status(405).end("Method Not Allowed"); + } +} diff --git a/webapp-next/pages/login/forgot-password.tsx b/webapp-next/pages/login/forgot-password.tsx new file mode 100644 index 0000000..2c38cdc --- /dev/null +++ b/webapp-next/pages/login/forgot-password.tsx @@ -0,0 +1,16 @@ +import { FormForgotPassword } from "@/components/login/FormForgotPassword"; +import { ColumnWithImage } from "@/components/login/ColumnWithImage"; +import { Flex } from "@chakra-ui/react"; + +export default function ForgotPassword() { + return ( + + + + + + + + + ); +} diff --git a/webapp-next/pages/login/reset-password.tsx b/webapp-next/pages/login/reset-password.tsx new file mode 100644 index 0000000..d25e922 --- /dev/null +++ b/webapp-next/pages/login/reset-password.tsx @@ -0,0 +1,16 @@ +import { FormResetPassword } from "@/components/login/FormResetPassword"; +import { ColumnWithImage } from "@/components/login/ColumnWithImage"; +import { Flex } from "@chakra-ui/react"; + +export default function ForgotPassword() { + return ( + + + + + + + + + ); +} diff --git a/webapp-next/utils/tools.ts b/webapp-next/utils/tools.ts index 34d3cc7..19f3dfe 100644 --- a/webapp-next/utils/tools.ts +++ b/webapp-next/utils/tools.ts @@ -1,228 +1,228 @@ -import moment from 'moment'; -import { Filters, SearchCategory, View } from './cm2d-provider'; +import moment from "moment"; +import { Filters, SearchCategory, View } from "./cm2d-provider"; export const viewRefs: { label: string; value: View }[] = [ - { label: 'Vue courbe', value: 'line' }, - { label: 'Vue carte', value: 'map' }, - { label: 'Vue histogramme', value: 'histogram' }, - { label: 'Vue donut', value: 'doughnut' }, - { label: 'Vue tableau', value: 'table' } + { label: "Vue courbe", value: "line" }, + { label: "Vue carte", value: "map" }, + { label: "Vue histogramme", value: "histogram" }, + { label: "Vue donut", value: "doughnut" }, + { label: "Vue tableau", value: "table" }, ]; export const departmentRefs: { [key: string]: string } = { - '01': 'Ain', - '02': 'Aisne', - '03': 'Allier', - '04': 'Alpes-de-Haute-Provence', - '05': 'Hautes-Alpes', - '06': 'Alpes-Maritimes', - '07': 'Ardèche', - '08': 'Ardennes', - '09': 'Ariège', - '10': 'Aube', - '11': 'Aude', - '12': 'Aveyron', - '13': 'Bouches-du-Rhône', - '14': 'Calvados', - '15': 'Cantal', - '16': 'Charente', - '17': 'Charente-Maritime', - '18': 'Cher', - '19': 'Corrèze', - '21': "Côte-d'Or", - '22': "Côtes-d'Armor", - '23': 'Creuse', - '24': 'Dordogne', - '25': 'Doubs', - '26': 'Drôme', - '27': 'Eure', - '28': 'Eure-et-Loir', - '29': 'Finistère', - '30': 'Gard', - '31': 'Haute-Garonne', - '32': 'Gers', - '33': 'Gironde', - '34': 'Hérault', - '35': 'Ille-et-Vilaine', - '36': 'Indre', - '37': 'Indre-et-Loire', - '38': 'Isère', - '39': 'Jura', - '40': 'Landes', - '41': 'Loir-et-Cher', - '42': 'Loire', - '43': 'Haute-Loire', - '44': 'Loire-Atlantique', - '45': 'Loiret', - '46': 'Lot', - '47': 'Lot-et-Garonne', - '48': 'Lozère', - '49': 'Maine-et-Loire', - '50': 'Manche', - '51': 'Marne', - '52': 'Haute-Marne', - '53': 'Mayenne', - '54': 'Meurthe-et-Moselle', - '55': 'Meuse', - '56': 'Morbihan', - '57': 'Moselle', - '58': 'Nièvre', - '59': 'Nord', - '60': 'Oise', - '61': 'Orne', - '62': 'Pas-de-Calais', - '63': 'Puy-de-Dôme', - '64': 'Pyrénées-Atlantiques', - '65': 'Hautes-Pyrénées', - '66': 'Pyrénées-Orientales', - '67': 'Bas-Rhin', - '68': 'Haut-Rhin', - '69': 'Rhône', - '70': 'Haute-Saône', - '71': 'Saône-et-Loire', - '72': 'Sarthe', - '73': 'Savoie', - '74': 'Haute-Savoie', - '75': 'Paris', - '76': 'Seine-Maritime', - '77': 'Seine-et-Marne', - '78': 'Yvelines', - '79': 'Deux-Sèvres', - '80': 'Somme', - '81': 'Tarn', - '82': 'Tarn-et-Garonne', - '83': 'Var', - '84': 'Vaucluse', - '85': 'Vendée', - '86': 'Vienne', - '87': 'Haute-Vienne', - '88': 'Vosges', - '89': 'Yonne', - '90': 'Territoire de Belfort', - '91': 'Essonne', - '92': 'Hauts-de-Seine', - '93': 'Seine-Saint-Denis', - '94': 'Val-de-Marne', - '95': "Val-d'Oise" + "01": "Ain", + "02": "Aisne", + "03": "Allier", + "04": "Alpes-de-Haute-Provence", + "05": "Hautes-Alpes", + "06": "Alpes-Maritimes", + "07": "Ardèche", + "08": "Ardennes", + "09": "Ariège", + "10": "Aube", + "11": "Aude", + "12": "Aveyron", + "13": "Bouches-du-Rhône", + "14": "Calvados", + "15": "Cantal", + "16": "Charente", + "17": "Charente-Maritime", + "18": "Cher", + "19": "Corrèze", + "21": "Côte-d'Or", + "22": "Côtes-d'Armor", + "23": "Creuse", + "24": "Dordogne", + "25": "Doubs", + "26": "Drôme", + "27": "Eure", + "28": "Eure-et-Loir", + "29": "Finistère", + "30": "Gard", + "31": "Haute-Garonne", + "32": "Gers", + "33": "Gironde", + "34": "Hérault", + "35": "Ille-et-Vilaine", + "36": "Indre", + "37": "Indre-et-Loire", + "38": "Isère", + "39": "Jura", + "40": "Landes", + "41": "Loir-et-Cher", + "42": "Loire", + "43": "Haute-Loire", + "44": "Loire-Atlantique", + "45": "Loiret", + "46": "Lot", + "47": "Lot-et-Garonne", + "48": "Lozère", + "49": "Maine-et-Loire", + "50": "Manche", + "51": "Marne", + "52": "Haute-Marne", + "53": "Mayenne", + "54": "Meurthe-et-Moselle", + "55": "Meuse", + "56": "Morbihan", + "57": "Moselle", + "58": "Nièvre", + "59": "Nord", + "60": "Oise", + "61": "Orne", + "62": "Pas-de-Calais", + "63": "Puy-de-Dôme", + "64": "Pyrénées-Atlantiques", + "65": "Hautes-Pyrénées", + "66": "Pyrénées-Orientales", + "67": "Bas-Rhin", + "68": "Haut-Rhin", + "69": "Rhône", + "70": "Haute-Saône", + "71": "Saône-et-Loire", + "72": "Sarthe", + "73": "Savoie", + "74": "Haute-Savoie", + "75": "Paris", + "76": "Seine-Maritime", + "77": "Seine-et-Marne", + "78": "Yvelines", + "79": "Deux-Sèvres", + "80": "Somme", + "81": "Tarn", + "82": "Tarn-et-Garonne", + "83": "Var", + "84": "Vaucluse", + "85": "Vendée", + "86": "Vienne", + "87": "Haute-Vienne", + "88": "Vosges", + "89": "Yonne", + "90": "Territoire de Belfort", + "91": "Essonne", + "92": "Hauts-de-Seine", + "93": "Seine-Saint-Denis", + "94": "Val-de-Marne", + "95": "Val-d'Oise", }; export const departmentsCodes: { [key: string]: string } = { - '01': 'FRA5262', // Ain - '02': 'FRA5263', // Aisne - '03': 'FRA5264', // Allier - '04': 'FRA5265', // Alpes-de-Haute-Provence - '06': 'FRA5266', // Alpes-Maritimes - '07': 'FRA5267', // Ardèche - '08': 'FRA5268', // Ardennes - '09': 'FRA5269', // Ariège - '10': 'FRA5270', // Aube - '11': 'FRA5271', // Aude - '12': 'FRA5272', // Aveyron - '67': 'FRA5273', // Bas-Rhin - '13': 'FRA5274', // Bouches-du-Rhône - '14': 'FRA5275', // Calvados - '15': 'FRA5276', // Cantal - '16': 'FRA5277', // Charente - '17': 'FRA5278', // Charente-Maritime - '18': 'FRA5279', // Cher - '19': 'FRA5280', // Corrèze - '2A': 'FRA5281', // Corse-du-Sud - '21': 'FRA5282', // Côte-d'Or - '22': 'FRA5283', // Côtes-d'Armor - '23': 'FRA5284', // Creuse - '79': 'FRA5285', // Deux-Sèvres - '24': 'FRA5286', // Dordogne - '25': 'FRA5287', // Doubs - '26': 'FRA5288', // Drôme - '91': 'FRA5289', // Essonne - '27': 'FRA5290', // Eure - '28': 'FRA5291', // Eure-et-Loir - '29': 'FRA5292', // Finistère - '30': 'FRA5293', // Gard - '32': 'FRA5294', // Gers - '33': 'FRA5295', // Gironde - '68': 'FRA5296', // Haute-Rhin - '2B': 'FRA5297', // Haute-Corse - '31': 'FRA5298', // Haute-Garonne - '43': 'FRA5299', // Haute-Loire - '52': 'FRA5300', // Haute-Marne - '70': 'FRA5301', // Haute-Saône - '74': 'FRA5302', // Haute-Savoie - '87': 'FRA5303', // Haute-Vienne - '05': 'FRA5304', // Hautes-Alpes - '65': 'FRA5305', // Hautes-Pyrénées - '92': 'FRA5306', // Hauts-de-Seine - '34': 'FRA5307', // Hérault - '35': 'FRA5308', // Ille-et-Vilaine - '36': 'FRA5309', // Indre - '37': 'FRA5310', // Indre-et-Loire - '38': 'FRA5311', // Isère - '39': 'FRA5312', // Jura - '40': 'FRA5313', // Landes - '41': 'FRA5314', // Loir-et-Cher - '42': 'FRA5315', // Loire - '44': 'FRA5316', // Loire-Atlantique - '45': 'FRA5317', // Loiret - '46': 'FRA5318', // Lot - '47': 'FRA5319', // Lot-et-Garonne - '48': 'FRA5320', // Lozère - '49': 'FRA5321', // Maine-et-Loire - '50': 'FRA5322', // Manche - '51': 'FRA5323', // Marne - '53': 'FRA5324', // Mayenne - '54': 'FRA5325', // Meurthe-et-Moselle - '55': 'FRA5326', // Meuse - '56': 'FRA5327', // Morbihan - '57': 'FRA5328', // Moselle - '58': 'FRA5329', // Nièvre - '59': 'FRA5330', // Nord - '60': 'FRA5331', // Oise - '61': 'FRA5332', // Orne - '75': 'FRA5333', // Paris - '62': 'FRA5334', // Pas-de-Calais - '63': 'FRA5335', // Puy-de-Dôme - '64': 'FRA5336', // Pyrénées-Atlantiques - '66': 'FRA5337', // Pyrénées-Orientales - '69': 'FRA5338', // Rhône - '71': 'FRA5339', // Saône-et-Loire - '72': 'FRA5340', // Sarthe - '73': 'FRA5341', // Savoie - '77': 'FRA5342', // Seine-et-Marne - '76': 'FRA5343', // Seine-Maritime - '93': 'FRA5344', // Seine-Saint-Denis - '80': 'FRA5345', // Somme - '81': 'FRA5346', // Tarn - '82': 'FRA5347', // Tarn-et-Garonne - '90': 'FRA5348', // Territoire de Belfort - '95': 'FRA5349', // Val-d'Oise - '94': 'FRA5350', // Val-de-Marne - '83': 'FRA5351', // Var - '84': 'FRA5352', // Vaucluse - '85': 'FRA5353', // Vendée - '86': 'FRA5354', // Vienne - '88': 'FRA5355', // Vosges - '89': 'FRA5356', // Yonne - '78': 'FRA5357' // Yvelines + "01": "FRA5262", // Ain + "02": "FRA5263", // Aisne + "03": "FRA5264", // Allier + "04": "FRA5265", // Alpes-de-Haute-Provence + "06": "FRA5266", // Alpes-Maritimes + "07": "FRA5267", // Ardèche + "08": "FRA5268", // Ardennes + "09": "FRA5269", // Ariège + "10": "FRA5270", // Aube + "11": "FRA5271", // Aude + "12": "FRA5272", // Aveyron + "67": "FRA5273", // Bas-Rhin + "13": "FRA5274", // Bouches-du-Rhône + "14": "FRA5275", // Calvados + "15": "FRA5276", // Cantal + "16": "FRA5277", // Charente + "17": "FRA5278", // Charente-Maritime + "18": "FRA5279", // Cher + "19": "FRA5280", // Corrèze + "2A": "FRA5281", // Corse-du-Sud + "21": "FRA5282", // Côte-d'Or + "22": "FRA5283", // Côtes-d'Armor + "23": "FRA5284", // Creuse + "79": "FRA5285", // Deux-Sèvres + "24": "FRA5286", // Dordogne + "25": "FRA5287", // Doubs + "26": "FRA5288", // Drôme + "91": "FRA5289", // Essonne + "27": "FRA5290", // Eure + "28": "FRA5291", // Eure-et-Loir + "29": "FRA5292", // Finistère + "30": "FRA5293", // Gard + "32": "FRA5294", // Gers + "33": "FRA5295", // Gironde + "68": "FRA5296", // Haute-Rhin + "2B": "FRA5297", // Haute-Corse + "31": "FRA5298", // Haute-Garonne + "43": "FRA5299", // Haute-Loire + "52": "FRA5300", // Haute-Marne + "70": "FRA5301", // Haute-Saône + "74": "FRA5302", // Haute-Savoie + "87": "FRA5303", // Haute-Vienne + "05": "FRA5304", // Hautes-Alpes + "65": "FRA5305", // Hautes-Pyrénées + "92": "FRA5306", // Hauts-de-Seine + "34": "FRA5307", // Hérault + "35": "FRA5308", // Ille-et-Vilaine + "36": "FRA5309", // Indre + "37": "FRA5310", // Indre-et-Loire + "38": "FRA5311", // Isère + "39": "FRA5312", // Jura + "40": "FRA5313", // Landes + "41": "FRA5314", // Loir-et-Cher + "42": "FRA5315", // Loire + "44": "FRA5316", // Loire-Atlantique + "45": "FRA5317", // Loiret + "46": "FRA5318", // Lot + "47": "FRA5319", // Lot-et-Garonne + "48": "FRA5320", // Lozère + "49": "FRA5321", // Maine-et-Loire + "50": "FRA5322", // Manche + "51": "FRA5323", // Marne + "53": "FRA5324", // Mayenne + "54": "FRA5325", // Meurthe-et-Moselle + "55": "FRA5326", // Meuse + "56": "FRA5327", // Morbihan + "57": "FRA5328", // Moselle + "58": "FRA5329", // Nièvre + "59": "FRA5330", // Nord + "60": "FRA5331", // Oise + "61": "FRA5332", // Orne + "75": "FRA5333", // Paris + "62": "FRA5334", // Pas-de-Calais + "63": "FRA5335", // Puy-de-Dôme + "64": "FRA5336", // Pyrénées-Atlantiques + "66": "FRA5337", // Pyrénées-Orientales + "69": "FRA5338", // Rhône + "71": "FRA5339", // Saône-et-Loire + "72": "FRA5340", // Sarthe + "73": "FRA5341", // Savoie + "77": "FRA5342", // Seine-et-Marne + "76": "FRA5343", // Seine-Maritime + "93": "FRA5344", // Seine-Saint-Denis + "80": "FRA5345", // Somme + "81": "FRA5346", // Tarn + "82": "FRA5347", // Tarn-et-Garonne + "90": "FRA5348", // Territoire de Belfort + "95": "FRA5349", // Val-d'Oise + "94": "FRA5350", // Val-de-Marne + "83": "FRA5351", // Var + "84": "FRA5352", // Vaucluse + "85": "FRA5353", // Vendée + "86": "FRA5354", // Vienne + "88": "FRA5355", // Vosges + "89": "FRA5356", // Yonne + "78": "FRA5357", // Yvelines }; const elkFields = [ - { value: 'sex', label: 'Sexe' }, - { value: 'age', label: 'Age' }, - { value: 'categories_level_1', label: 'Cause' }, - { value: 'categories', label: 'Cause' }, - { value: 'categories_associate', label: 'Cause associée' }, - { value: 'categories_level_2', label: 'Comorbidité' }, - { value: 'death_location', label: 'Lieu de décès' }, - { value: 'home_department', label: 'Département' }, - { value: 'cert_type', label: 'Format' }, - { value: 'start_date', label: 'Période' }, - { value: 'end_date', label: 'Période' }, - { value: 'years', label: 'Année' }, - { value: 'months', label: 'Mois' } + { value: "sex", label: "Sexe" }, + { value: "age", label: "Age" }, + { value: "categories_level_1", label: "Cause" }, + { value: "categories", label: "Cause" }, + { value: "categories_associate", label: "Cause associée" }, + { value: "categories_level_2", label: "Comorbidité" }, + { value: "death_location", label: "Lieu de décès" }, + { value: "home_department", label: "Département" }, + { value: "cert_type", label: "Format" }, + { value: "start_date", label: "Période" }, + { value: "end_date", label: "Période" }, + { value: "years", label: "Année" }, + { value: "months", label: "Mois" }, ]; export function getLabelFromElkField(key: string): string { - const match = elkFields.find(elkf => elkf.value === key); + const match = elkFields.find((elkf) => elkf.value === key); if (match) return match.label; @@ -231,17 +231,17 @@ export function getLabelFromElkField(key: string): string { export const getLabelFromKey = ( key: string, - dateFormat: 'year' | 'month' | 'week' = 'year' + dateFormat: "year" | "month" | "week" = "year" ): string => { if (key in departmentRefs) return `${departmentRefs[key as keyof typeof departmentRefs]} (${key})`; if (isStringContainingDate(key)) { - if (dateFormat === 'year') + if (dateFormat === "year") return capitalizeString(new Date(key).getFullYear().toString()); - if (dateFormat === 'week') + if (dateFormat === "week") return capitalizeString(dateToWeekYear(new Date(key))); - if (dateFormat === 'month') + if (dateFormat === "month") return capitalizeString(dateToMonthYear(new Date(key))); } @@ -262,37 +262,37 @@ export function transformFilters(filters: Filters): any[] { if (filters.categories.length > 0) { switch (filters.categories_search) { - case 'full': + case "full": transformed.push({ bool: { should: [ { terms: { - categories_level_1: filters.categories - } + categories_level_1: filters.categories, + }, }, { terms: { - categories_level_2: filters.categories - } - } + categories_level_2: filters.categories, + }, + }, ], - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }); break; - case 'category_1': + case "category_1": transformed.push({ terms: { - categories_level_1: filters.categories - } + categories_level_1: filters.categories, + }, }); break; - case 'category_2': + case "category_2": transformed.push({ terms: { - categories_level_2: filters.categories - } + categories_level_2: filters.categories, + }, }); break; } @@ -301,49 +301,49 @@ export function transformFilters(filters: Filters): any[] { if (filters.categories_associate.length > 0) { transformed.push({ bool: { - should: filters.categories_associate.map(ca => { + should: filters.categories_associate.map((ca) => { return { match: { - categories_associate: ca - } + categories_associate: ca, + }, }; }), - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }); } if (filters.age.length > 0) { - const ageShouldClauses = filters.age.map(age => ({ + const ageShouldClauses = filters.age.map((age) => ({ range: { age: { gte: age.min, - lte: age.max - } - } + lte: age.max, + }, + }, })); transformed.push({ bool: { should: ageShouldClauses, - minimum_should_match: 1 - } + minimum_should_match: 1, + }, }); } if (filters.sex.length > 0) { transformed.push({ terms: { - sex: filters.sex - } + sex: filters.sex, + }, }); } if (filters.death_location.length > 0) { transformed.push({ terms: { - death_location: filters.death_location - } + death_location: filters.death_location, + }, }); } @@ -352,8 +352,8 @@ export function transformFilters(filters: Filters): any[] { home_department: filters.department.length > 0 ? filters.department - : filters.region_departments - } + : filters.region_departments, + }, }); if (filters.start_date && filters.end_date) { @@ -361,9 +361,9 @@ export function transformFilters(filters: Filters): any[] { range: { date: { gte: filters.start_date, - lte: filters.end_date - } - } + lte: filters.end_date, + }, + }, }); } @@ -386,30 +386,30 @@ export function getCSVDataFromDatasets( if (datasets.length === 1) { csvData.push( - datasets[0].hits.map(hit => { + datasets[0].hits.map((hit) => { return getLabelFromKey( hit.key, - view === 'line' ? 'week' : view === 'table' ? 'month' : 'year' + view === "line" ? "week" : view === "table" ? "month" : "year" ); }) ); csvData.push( - datasets[0].hits.map(hit => { + datasets[0].hits.map((hit) => { return hit.doc_count; }) ); } else { csvData.push([ - '', - ...datasets[0].hits.map(h => + "", + ...datasets[0].hits.map((h) => getLabelFromKey( h.key, - view === 'line' ? 'week' : view === 'table' ? 'month' : 'year' + view === "line" ? "week" : view === "table" ? "month" : "year" ) - ) + ), ]); - datasets.forEach(ds => { - csvData.push([ds.label, ...ds.hits.map(h => h.doc_count)]); + datasets.forEach((ds) => { + csvData.push([ds.label, ...ds.hits.map((h) => h.doc_count)]); }); } @@ -417,20 +417,20 @@ export function getCSVDataFromDatasets( } export function getViewDatasets(data: any, view: View): Datasets[] { - if (view === 'line') { + if (view === "line") { if (data.result.aggregations.aggregated_date) { return [{ hits: data.result.aggregations.aggregated_date.buckets }]; } else if (data.result.aggregations.aggregated_parent) { return data.result.aggregations.aggregated_parent.buckets .map((apb: any) => ({ hits: apb.aggregated_date.buckets, - label: getLabelFromKey(apb.key) + label: getLabelFromKey(apb.key), })) .filter((apb: any) => !!apb.hits.length); } } - if (view === 'table') { + if (view === "table") { if ( data.result.aggregations.aggregated_x && !!data.result.aggregations.aggregated_x.buckets.length && @@ -439,25 +439,25 @@ export function getViewDatasets(data: any, view: View): Datasets[] { return data.result.aggregations.aggregated_x.buckets .map((apb: any) => ({ hits: apb.aggregated_y.buckets.filter((b: any) => !!b.doc_count), - label: getLabelFromKey(apb.key, 'month') + label: getLabelFromKey(apb.key, "month"), })) .filter((apb: any) => !!apb.hits.length); } } - if (view === 'histogram' || view === 'doughnut') { + if (view === "histogram" || view === "doughnut") { if (data.result.aggregations.aggregated_x) { return [ { hits: data.result.aggregations.aggregated_x.buckets.filter( (b: any) => !!b.doc_count - ) - } + ), + }, ]; } } - if (view === 'map') { + if (view === "map") { if (data.result.aggregations.aggregated_x) { return [ { @@ -470,15 +470,15 @@ export function getViewDatasets(data: any, view: View): Datasets[] { doc_count: x.doc_count, children: x.aggregated_y.buckets.filter( (y: any) => !!y.doc_count - ) + ), } : x ), total: data.result.aggregations.aggregated_x.buckets.reduce( (acc: number, b: any) => acc + b.doc_count, 0 - ) - } + ), + }, ]; } } @@ -503,13 +503,13 @@ export function concatAdditionnalFields( categories_search: SearchCategory, categories_associate: string[] ): { label: string; value: T }[] { - if (categories_search === 'full' && categories_associate.length > 1) { + if (categories_search === "full" && categories_associate.length > 1) { return [ ...availableFields, { - label: 'Cause associée', - value: 'categories_associate' as T - } + label: "Cause associée", + value: "categories_associate" as T, + }, ]; } else { return availableFields; @@ -528,16 +528,16 @@ export function ISODateToMonthYear(isoDateString: string): string { } export function dateToDayMonth(date: Date): string { - const options = { day: '2-digit', month: 'long' }; - const formatter = new Intl.DateTimeFormat('fr-FR', options as any); + const options = { day: "2-digit", month: "long" }; + const formatter = new Intl.DateTimeFormat("fr-FR", options as any); const parts = formatter.formatToParts(date); const formattedDate = `${parts[2].value.trim()} ${parts[0].value.trim()}`; return formattedDate; } export function dateToMonthYear(date: Date): string { - const options = { month: 'long', year: 'numeric' }; - const formatter = new Intl.DateTimeFormat('fr-FR', options as any); + const options = { month: "long", year: "numeric" }; + const formatter = new Intl.DateTimeFormat("fr-FR", options as any); const formattedDate = formatter.format(date); return formattedDate; } @@ -556,7 +556,7 @@ function getWeekNumber(date: Date): number { export function dateToWeekYear(inputDate: Date): string { const year = inputDate.getFullYear(); const weekNumber = getWeekNumber(inputDate); - const formattedString = `S${weekNumber.toString().padStart(2, '0')} ${year}`; + const formattedString = `S${weekNumber.toString().padStart(2, "0")} ${year}`; return formattedString; } @@ -570,26 +570,26 @@ export function getLastDayOfMonth(date: Date): Date { } export const chartsAvailableColors = [ - '#e41a1c', // Bright red - '#377eb8', // Vivid blue - '#4daf4a', // Strong green - '#984ea3', // Deep purple - '#ff7f00', // Bright orange - '#000000', // Neon yellow - '#a65628', // Dark tan - '#999999', // Dark gray - '#7fc97f', // Faded green - '#beaed4', // Soft purple - '#fdc086', // Peach - '#fb9a99', // Soft red - '#e31a1c', // Another shade of red - '#fdbf6f', // Light orange - '#cab2d6', // Lilac - '#1b9e77', // Jade green - '#d95f02', // Dark orange - '#6a3d9a', // Plum - '#33a02c', // Dark green - '#b15928' // Sienna + "#e41a1c", // Bright red + "#377eb8", // Vivid blue + "#4daf4a", // Strong green + "#984ea3", // Deep purple + "#ff7f00", // Bright orange + "#000000", // Neon yellow + "#a65628", // Dark tan + "#999999", // Dark gray + "#7fc97f", // Faded green + "#beaed4", // Soft purple + "#fdc086", // Peach + "#fb9a99", // Soft red + "#e31a1c", // Another shade of red + "#fdbf6f", // Light orange + "#cab2d6", // Lilac + "#1b9e77", // Jade green + "#d95f02", // Dark orange + "#6a3d9a", // Plum + "#33a02c", // Dark green + "#b15928", // Sienna ]; export function getRandomColor(index?: number): string { let sIndex = index; @@ -628,7 +628,7 @@ export function isRangeContainsLastSixMonths( if (!startDate) return true; const now = moment(); - const sixMonthsAgo = moment().subtract(6, 'months'); + const sixMonthsAgo = moment().subtract(6, "months"); const start = new Date(startDate); let end; @@ -650,7 +650,7 @@ export function isRangeContainsLastSixMonths( } export function getSixMonthAgoDate() { - return moment().subtract(6, 'months').format('DD/MM/YYYY'); + return moment().subtract(6, "months").format("DD/MM/YYYY"); } export function generateCode(): string { @@ -716,6 +716,48 @@ export function getCodeEmailHtml(code: string) { `; } +export function getResetPasswordEmailHtml(link: string) { + return ` + + + + + + +
+

Bonjour,

+ +

+ Vous avez récemment demandé à réinitialiser votre mot de passe. Pour terminer le processus, + veuillez entrer le code de vérification suivant dans le formulaire de réinitialisation : +

+ + Votre lien de réinitialisation + +

+ Ce code est valable pour les 10 prochaines minutes. Si vous n'avez pas demandé ce + code, veuillez ignorer cet e-mail. +

+ +

+ Merci,
+ L'équipe CM2D +

+
+ + + `; +} + export function capitalizeString(str: string): string { if (str.length <= 1) return str; @@ -724,14 +766,14 @@ export function capitalizeString(str: string): string { // NOT USED ANYMORE export function addMissingSizes(obj: any, size: number): any { - if (typeof obj === 'object') { - if (obj.terms && obj.terms.field === 'categories_associate') { + if (typeof obj === "object") { + if (obj.terms && obj.terms.field === "categories_associate") { return { ...obj, terms: { ...obj.terms, - size: size - } + size: size, + }, }; } const newObj: typeof obj = {}; @@ -748,8 +790,8 @@ export function addMissingSizes(obj: any, size: number): any { } export function removeAccents(str: string) { - return str.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); + return str.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); } export const ELASTIC_API_KEY_NAME = - (process.env.NEXT_PUBLIC_ELASTIC_API_KEY_NAME as string) || 'cm2d_api_key'; + (process.env.NEXT_PUBLIC_ELASTIC_API_KEY_NAME as string) || "cm2d_api_key"; diff --git a/webapp-next/yarn.lock b/webapp-next/yarn.lock index ace6ec4..738a03e 100644 --- a/webapp-next/yarn.lock +++ b/webapp-next/yarn.lock @@ -1860,9 +1860,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001579: - version "1.0.30001616" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001616.tgz#4342712750d35f71ebba9fcac65e2cf8870013c3" - integrity sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw== + version "1.0.30001621" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz" + integrity sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA== chalk@3.0.0: version "3.0.0" @@ -3820,6 +3820,11 @@ react-focus-lock@^2.9.2: use-callback-ref "^1.3.0" use-sidecar "^1.1.2" +react-hook-form@^7.51.5: + version "7.51.5" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.5.tgz#4afbfb819312db9fea23e8237a3a0d097e128b43" + integrity sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q== + react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" @@ -4071,16 +4076,8 @@ streamsearch@^1.1.0: resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== From 1eb4a09e89defc566a60c2a9231d153499cb3478 Mon Sep 17 00:00:00 2001 From: horek Date: Tue, 28 May 2024 12:10:10 +0200 Subject: [PATCH 2/7] fix: add toast on password reset submission, add message on forgot password submission --- .../components/login/FormForgotPassword.tsx | 105 ++++++++++-------- .../components/login/FormResetPassword.tsx | 12 +- webapp-next/pages/api/auth/reset-password.ts | 4 +- webapp-next/utils/chakra-theme.ts | 12 +- webapp-next/utils/tools.ts | 6 +- 5 files changed, 79 insertions(+), 60 deletions(-) diff --git a/webapp-next/components/login/FormForgotPassword.tsx b/webapp-next/components/login/FormForgotPassword.tsx index 86a4e5d..48da31d 100644 --- a/webapp-next/components/login/FormForgotPassword.tsx +++ b/webapp-next/components/login/FormForgotPassword.tsx @@ -1,3 +1,4 @@ +import { CheckCircleIcon } from "@chakra-ui/icons"; import { Box, Button, @@ -14,7 +15,6 @@ import { Text, } from "@chakra-ui/react"; import NextLink from "next/link"; -import { useRouter } from "next/router"; import { useForm, type SubmitHandler } from "react-hook-form"; import useSWRMutation from "swr/mutation"; @@ -39,7 +39,7 @@ export const FormForgotPassword = () => { const { handleSubmit, register, - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, isSubmitSuccessful }, } = useForm(); const onSubmit: SubmitHandler = ({ username }) => { @@ -64,53 +64,62 @@ export const FormForgotPassword = () => { > Mot de passe oublié
-
- - - Identifiant - - - - User Icon + + + Identifiant + + + + User Icon + + - - - - - - {errors.username && errors.username.message} - - -
+ + + + {errors.username && errors.username.message} + + + + ) : ( + + Un email de + réinitialisation de mot de passe a été envoyé à +
+ l'adresse email associée à votre compte. +
+ )} diff --git a/webapp-next/components/login/FormResetPassword.tsx b/webapp-next/components/login/FormResetPassword.tsx index 2625785..15c0822 100644 --- a/webapp-next/components/login/FormResetPassword.tsx +++ b/webapp-next/components/login/FormResetPassword.tsx @@ -15,6 +15,7 @@ import { InputLeftElement, Link, Text, + useToast, } from "@chakra-ui/react"; import NextLink from "next/link"; import { useRouter } from "next/router"; @@ -38,6 +39,8 @@ export async function auth(url: string, { arg }: { arg: T }) { export const FormResetPassword = () => { const router = useRouter(); + const toast = useToast(); + const [errorForm, setErrorForm] = useState(null); const { trigger: triggerResetPassword } = useSWRMutation( @@ -60,6 +63,13 @@ export const FormResetPassword = () => { token: router.query.token as string, }); if (response && response.ok) { + toast({ + title: "Mot de passe réinitialisé", + description: "Vous pouvez maintenant vous connecter", + status: "success", + duration: 9000, + isClosable: true, + }); router.push("/login"); } else { if (response?.status === 404) { @@ -159,7 +169,7 @@ export const FormResetPassword = () => { {errorForm && ( - + {errorForm} diff --git a/webapp-next/pages/api/auth/reset-password.ts b/webapp-next/pages/api/auth/reset-password.ts index 03c3a1b..a3b8298 100644 --- a/webapp-next/pages/api/auth/reset-password.ts +++ b/webapp-next/pages/api/auth/reset-password.ts @@ -61,8 +61,8 @@ export default async function handler( res.status(200).send({ response: "ok" }); } catch (error: any) { - if (error.statusCode === 401) { - res.status(401).end(); + if (error.statusCode) { + res.status(error.statusCode).end(); } else { res.status(500).end(); } diff --git a/webapp-next/utils/chakra-theme.ts b/webapp-next/utils/chakra-theme.ts index ce75788..285c638 100644 --- a/webapp-next/utils/chakra-theme.ts +++ b/webapp-next/utils/chakra-theme.ts @@ -125,16 +125,16 @@ const CM2DAlert: ComponentStyleConfig = { baseStyle: { container: { borderRadius: 'lg', - bg: 'highlight.50', - color: 'orange.500' + // bg: 'highlight.50', + // color: 'orange.500' }, description: { fontWeight: 500 }, - icon: { - bg: 'highlight.50', - color: 'highlight.500' - } + // icon: { + // bg: 'highlight.50', + // color: 'highlight.500' + // } } }; diff --git a/webapp-next/utils/tools.ts b/webapp-next/utils/tools.ts index 19f3dfe..3dee12b 100644 --- a/webapp-next/utils/tools.ts +++ b/webapp-next/utils/tools.ts @@ -738,14 +738,14 @@ export function getResetPasswordEmailHtml(link: string) {

Vous avez récemment demandé à réinitialiser votre mot de passe. Pour terminer le processus, - veuillez entrer le code de vérification suivant dans le formulaire de réinitialisation : + veuillez cliquer sur le lien ci-dessous :

Votre lien de réinitialisation

- Ce code est valable pour les 10 prochaines minutes. Si vous n'avez pas demandé ce - code, veuillez ignorer cet e-mail. + Ce lien est valable pendant 1 heure. Si vous n'avez pas demandé cette réinitialisation, + veuillez ignorer cet e-mail.

From bfece7d4dc53d9f2b1796be6cd6730c7c37764b3 Mon Sep 17 00:00:00 2001 From: horek Date: Tue, 28 May 2024 14:27:15 +0200 Subject: [PATCH 3/7] fix: width on login forms and add validation on reset password --- webapp-next/components/login/FormForgotPassword.tsx | 2 +- webapp-next/components/login/FormLogin.tsx | 2 +- webapp-next/components/login/FormResetPassword.tsx | 12 +++++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/webapp-next/components/login/FormForgotPassword.tsx b/webapp-next/components/login/FormForgotPassword.tsx index 48da31d..2a58bf0 100644 --- a/webapp-next/components/login/FormForgotPassword.tsx +++ b/webapp-next/components/login/FormForgotPassword.tsx @@ -54,7 +54,7 @@ export const FormForgotPassword = () => { mx={"auto"} mt={[8, 0]} > - + { mx={"auto"} mt={[8, 0]} > - + { mx={"auto"} mt={[8, 0]} > - + { bg={"secondary.500"} {...register("password", { required: "Ce champ est obligatoire", + minLength: { + value: 12, + message: + "Le mot de passe doit contenir au moins 12 caractères", + }, + pattern: { + value: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).+$/, + message: + "Le mot de passe doit contenir au moins une lettre minuscule, une lettre majuscule, un chiffre et un caractère spécial", + }, })} /> From 9169d73f2d24ce574bef36a43c368746c66bd91c Mon Sep 17 00:00:00 2001 From: horek Date: Tue, 28 May 2024 15:03:42 +0200 Subject: [PATCH 4/7] fix: replace baseStyle of alert by a custom alert component --- webapp-next/components/chakra/Alert.ts | 44 ++++++++++++++++++++++++++ webapp-next/middleware.ts | 2 +- webapp-next/utils/chakra-theme.ts | 18 +---------- 3 files changed, 46 insertions(+), 18 deletions(-) create mode 100644 webapp-next/components/chakra/Alert.ts diff --git a/webapp-next/components/chakra/Alert.ts b/webapp-next/components/chakra/Alert.ts new file mode 100644 index 0000000..1ea18b2 --- /dev/null +++ b/webapp-next/components/chakra/Alert.ts @@ -0,0 +1,44 @@ +import { AlertProps, createMultiStyleConfigHelpers } from "@chakra-ui/react"; +import { alertAnatomy } from "@chakra-ui/anatomy"; + +const { definePartsStyle, defineMultiStyleConfig } = + createMultiStyleConfigHelpers(alertAnatomy.keys); + +const baseStyle = definePartsStyle((props: AlertProps) => { + const { status } = props; + + const base = { + container: { + borderRadius: "lg", + }, + description: { + fontWeight: 500, + }, + }; + + const statusBases = { + success: base, + info: base, + warning: { + container: { + borderRadius: "lg", + bg: "highlight.50", + color: "orange.500", + }, + icon: { + bg: "highlight.50", + color: "highlight.500", + }, + description: { + fontWeight: 500, + }, + }, + error: base, + }; + + const baseStyle = statusBases[status as keyof typeof statusBases]; + + return baseStyle; +}); + +export const alertTheme = defineMultiStyleConfig({ baseStyle }); diff --git a/webapp-next/middleware.ts b/webapp-next/middleware.ts index 2e239b5..8eb2bdf 100644 --- a/webapp-next/middleware.ts +++ b/webapp-next/middleware.ts @@ -10,7 +10,7 @@ export function middleware(request: NextRequest) { if (!cookie) { return NextResponse.redirect(new URL('/', request.url)); } - } else if (request.nextUrl.pathname === '/' || request.nextUrl.pathname === '/login') { + } else if (request.nextUrl.pathname === '/' || request.nextUrl.pathname.startsWith('/login')) { if (cookie) { return NextResponse.redirect(new URL('/bo', request.url)); } diff --git a/webapp-next/utils/chakra-theme.ts b/webapp-next/utils/chakra-theme.ts index 285c638..931a212 100644 --- a/webapp-next/utils/chakra-theme.ts +++ b/webapp-next/utils/chakra-theme.ts @@ -1,5 +1,6 @@ import { extendTheme } from '@chakra-ui/react'; import { ComponentStyleConfig } from '@chakra-ui/react'; +import { alertTheme as CM2DAlert } from "@/components/chakra/Alert"; const colors = { primaryOverlay: 'rgb(36, 108, 249, 0.8)', @@ -121,23 +122,6 @@ const CM2DButton: ComponentStyleConfig = { } }; -const CM2DAlert: ComponentStyleConfig = { - baseStyle: { - container: { - borderRadius: 'lg', - // bg: 'highlight.50', - // color: 'orange.500' - }, - description: { - fontWeight: 500 - }, - // icon: { - // bg: 'highlight.50', - // color: 'highlight.500' - // } - } -}; - const theme = extendTheme({ colors, fonts: { From 6c6be0affe833861bf73117df40185218b3eff55 Mon Sep 17 00:00:00 2001 From: horek Date: Wed, 29 May 2024 09:44:56 +0200 Subject: [PATCH 5/7] fix: fetch for swr mutation in tools instead of duplicate --- .../components/login/FormForgotPassword.tsx | 11 ++--------- webapp-next/components/login/FormLogin.tsx | 16 ++++------------ .../components/login/FormResetPassword.tsx | 11 ++--------- webapp-next/utils/tools.ts | 8 ++++++++ 4 files changed, 16 insertions(+), 30 deletions(-) diff --git a/webapp-next/components/login/FormForgotPassword.tsx b/webapp-next/components/login/FormForgotPassword.tsx index 2a58bf0..a69f8aa 100644 --- a/webapp-next/components/login/FormForgotPassword.tsx +++ b/webapp-next/components/login/FormForgotPassword.tsx @@ -1,3 +1,4 @@ +import { swrPOSTFetch } from "@/utils/tools"; import { CheckCircleIcon } from "@chakra-ui/icons"; import { Box, @@ -22,18 +23,10 @@ type FormForgotPassword = { username: string; }; -export async function auth(url: string, { arg }: { arg: T }) { - return fetch(url, { - method: "POST", - body: JSON.stringify(arg), - headers: { "Content-Type": "application/json" }, - }); -} - export const FormForgotPassword = () => { const { trigger: triggerForgotPassword, isMutating } = useSWRMutation( "/api/auth/forgot-password", - auth<{ username: string }> + swrPOSTFetch<{ username: string }> ); const { diff --git a/webapp-next/components/login/FormLogin.tsx b/webapp-next/components/login/FormLogin.tsx index 78464c9..ff42045 100644 --- a/webapp-next/components/login/FormLogin.tsx +++ b/webapp-next/components/login/FormLogin.tsx @@ -31,17 +31,9 @@ import NextLink from "next/link"; import { useRouter } from "next/router"; import { useEffect, useRef, useState } from "react"; import useSWRMutation from "swr/mutation"; -import { ELASTIC_API_KEY_NAME } from "@/utils/tools"; +import { ELASTIC_API_KEY_NAME, swrPOSTFetch } from "@/utils/tools"; import { ContentCGU } from "@/pages/legals/cgu"; -export async function auth(url: string, { arg }: { arg: T }) { - return fetch(url, { - method: "POST", - body: JSON.stringify(arg), - headers: { "Content-Type": "application/json" }, - }); -} - export const FormLogin = () => { const router = useRouter(); @@ -70,15 +62,15 @@ export const FormLogin = () => { const { trigger: triggerLogin } = useSWRMutation( "/api/auth", - auth<{ username: string; password: string }> + swrPOSTFetch<{ username: string; password: string }> ); const { trigger: triggerVerify } = useSWRMutation( "/api/auth/verify-code", - auth<{ username: string; code: string }> + swrPOSTFetch<{ username: string; code: string }> ); const { trigger: triggerCreateUser } = useSWRMutation( "/api/auth/create-user", - auth<{ username: string; versionCGU: string }> + swrPOSTFetch<{ username: string; versionCGU: string }> ); const startTimer = () => { diff --git a/webapp-next/components/login/FormResetPassword.tsx b/webapp-next/components/login/FormResetPassword.tsx index c708226..925cf34 100644 --- a/webapp-next/components/login/FormResetPassword.tsx +++ b/webapp-next/components/login/FormResetPassword.tsx @@ -1,3 +1,4 @@ +import { swrPOSTFetch } from "@/utils/tools"; import { Alert, AlertIcon, @@ -28,14 +29,6 @@ type FormResetPassword = { confirmPassword: string; }; -export async function auth(url: string, { arg }: { arg: T }) { - return fetch(url, { - method: "POST", - body: JSON.stringify(arg), - headers: { "Content-Type": "application/json" }, - }); -} - export const FormResetPassword = () => { const router = useRouter(); @@ -45,7 +38,7 @@ export const FormResetPassword = () => { const { trigger: triggerResetPassword } = useSWRMutation( "/api/auth/reset-password", - auth<{ password: string; token: string }> + swrPOSTFetch<{ password: string; token: string }> ); const { diff --git a/webapp-next/utils/tools.ts b/webapp-next/utils/tools.ts index 3dee12b..9cae1ce 100644 --- a/webapp-next/utils/tools.ts +++ b/webapp-next/utils/tools.ts @@ -795,3 +795,11 @@ export function removeAccents(str: string) { export const ELASTIC_API_KEY_NAME = (process.env.NEXT_PUBLIC_ELASTIC_API_KEY_NAME as string) || "cm2d_api_key"; + +export async function swrPOSTFetch(url: string, { arg }: { arg: T }) { + return fetch(url, { + method: "POST", + body: JSON.stringify(arg), + headers: { "Content-Type": "application/json" }, + }); +} \ No newline at end of file From 61e86ee24f6bafcd7f3ffb6aa1d0f494ac70b5c6 Mon Sep 17 00:00:00 2001 From: horek Date: Wed, 29 May 2024 09:55:24 +0200 Subject: [PATCH 6/7] fix: missed one swr mutation fetch typing in menu component --- webapp-next/components/layouts/Menu.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/webapp-next/components/layouts/Menu.tsx b/webapp-next/components/layouts/Menu.tsx index c59b9ec..754bf93 100644 --- a/webapp-next/components/layouts/Menu.tsx +++ b/webapp-next/components/layouts/Menu.tsx @@ -12,10 +12,9 @@ import { UserCard } from './UserCard'; import { FilterDates } from '../filters/Dates'; import { FiltersDepartments } from '../filters/Departments'; import cookie from 'js-cookie'; -import { hasAtLeastOneFilter, ELASTIC_API_KEY_NAME } from '@/utils/tools'; +import { hasAtLeastOneFilter, ELASTIC_API_KEY_NAME, swrPOSTFetch } from '@/utils/tools'; import { FilterAssociateCauses } from '../filters/AssociateCauses'; import { RegionFilter } from '../filters/Regions'; -import { auth } from '../login/FormLogin'; import useSWRMutation from 'swr/mutation'; export const ageRanges = [ @@ -40,7 +39,7 @@ export function Menu() { const { trigger: triggerInvalidateApiKey } = useSWRMutation( "/api/auth/invalidate-api-key", - auth<{ username: string }> + swrPOSTFetch<{ username: string }> ); const { filters, setFilters, user } = context; From 65845fc8281ac3bbdbe3a0864e95fef4ccbd38ca Mon Sep 17 00:00:00 2001 From: horek Date: Wed, 29 May 2024 10:15:21 +0200 Subject: [PATCH 7/7] fix: add wrapper for multiple forms with same layout and correct fontSize from px to chakra size --- .../components/login/FormForgotPassword.tsx | 154 ++++++------- webapp-next/components/login/FormLogin.tsx | 58 ++--- .../components/login/FormResetPassword.tsx | 216 ++++++++---------- webapp-next/components/login/WrapperForm.tsx | 33 +++ 4 files changed, 227 insertions(+), 234 deletions(-) create mode 100644 webapp-next/components/login/WrapperForm.tsx diff --git a/webapp-next/components/login/FormForgotPassword.tsx b/webapp-next/components/login/FormForgotPassword.tsx index a69f8aa..fa0d601 100644 --- a/webapp-next/components/login/FormForgotPassword.tsx +++ b/webapp-next/components/login/FormForgotPassword.tsx @@ -18,6 +18,7 @@ import { import NextLink from "next/link"; import { useForm, type SubmitHandler } from "react-hook-form"; import useSWRMutation from "swr/mutation"; +import { WrapperForm } from "./WrapperForm"; type FormForgotPassword = { username: string; @@ -39,87 +40,78 @@ export const FormForgotPassword = () => { triggerForgotPassword({ username }); }; - return ( - - - { + return ( +

+ + + Identifiant + + + + User Icon + + + + + + {errors.username && errors.username.message} + + -
- ) : ( - - Un email de - réinitialisation de mot de passe a été envoyé à -
- l'adresse email associée à votre compte. -
- )} - - - - Retour à la connexion - - -
-
+ Réinitialiser le mot de passe + + + ); + }; + + const displaySubmitSuccess = () => { + return ( + + Un email de + réinitialisation de mot de passe a été envoyé à +
+ l'adresse email associée à votre compte. +
+ ); + }; + + return ( + + {isSubmitSuccessful ? displaySubmitSuccess() : displayForm()} + + + + Retour à la connexion + + + ); }; diff --git a/webapp-next/components/login/FormLogin.tsx b/webapp-next/components/login/FormLogin.tsx index ff42045..6e582e4 100644 --- a/webapp-next/components/login/FormLogin.tsx +++ b/webapp-next/components/login/FormLogin.tsx @@ -33,6 +33,7 @@ import { useEffect, useRef, useState } from "react"; import useSWRMutation from "swr/mutation"; import { ELASTIC_API_KEY_NAME, swrPOSTFetch } from "@/utils/tools"; import { ContentCGU } from "@/pages/legals/cgu"; +import { WrapperForm } from "./WrapperForm"; export const FormLogin = () => { const router = useRouter(); @@ -186,7 +187,7 @@ export const FormLogin = () => { }} > - + Code @@ -198,7 +199,7 @@ export const FormLogin = () => { id="code" autoFocus placeholder="Saisissez votre code" - fontSize={"12px"} + fontSize="xs" bg={"secondary.500"} value={code} onChange={handleCodeChange} @@ -251,7 +252,7 @@ export const FormLogin = () => { loadingText="Connexion en cours..." color={"white"} w={"full"} - fontSize={["14px", "16px", "18px"]} + fontSize={["md", "lg", "xl"]} fontWeight={600} > {isLoading ? : <>Je valide ->} @@ -269,7 +270,7 @@ export const FormLogin = () => { Identifiant @@ -283,7 +284,7 @@ export const FormLogin = () => { id="username" autoFocus placeholder="Saisissez votre adresse email" - fontSize={"12px"} + fontSize="xs" bg={"secondary.500"} value={username} onChange={handleUsernameChange} @@ -294,7 +295,7 @@ export const FormLogin = () => { Mot de passe @@ -307,7 +308,7 @@ export const FormLogin = () => { type={isOpen ? "text" : "password"} id="password" placeholder="Saisissez votre mot de passe" - fontSize={"12px"} + fontSize="xs" bg={"secondary.500"} value={password} onChange={handlePasswordChange} @@ -366,7 +367,7 @@ export const FormLogin = () => { loadingText="Connexion en cours..." color={"white"} w={"full"} - fontSize={["14px", "16px", "18px"]} + fontSize={["md", "lg", "xl"]} fontWeight={600} > {isLoading ? ( @@ -386,36 +387,19 @@ export const FormLogin = () => { return ( <> - + - - - Connexion 👋 - - - {showCodeForm - ? "Vous avez reçu un code par email, merci de le saisir ci-dessous." - : "Veuillez vous connecter pour accéder à votre compte."} - - {showCodeForm ? CodeForm : EmailPasswordForm} - - + {showCodeForm + ? "Vous avez reçu un code par email, merci de le saisir ci-dessous." + : "Veuillez vous connecter pour accéder à votre compte."} + + {showCodeForm ? CodeForm : EmailPasswordForm} + { }; return ( - - - - Mot de passe oublié - -
- - - Nouveau mot de passe - - - - User Icon - - - - - {errors.password && errors.password.message} - - - - - Confirmer le mot de passe - - - - User Icon - - - value === watch("password") || - "Les mots de passe ne correspondent pas", - })} + + + + + Mot de passe + + + + User Icon - - - {errors.confirmPassword && errors.confirmPassword.message} - - - {errorForm && ( - - - {errorForm} - - )} - - -
-
+ Confirmer le mot de passe +
+ + + User Icon + + + value === watch("password") || + "Les mots de passe ne correspondent pas", + })} + /> + + + {errors.confirmPassword && errors.confirmPassword.message} + +
+ {errorForm && ( + + + {errorForm} + + )} + + + ); }; diff --git a/webapp-next/components/login/WrapperForm.tsx b/webapp-next/components/login/WrapperForm.tsx new file mode 100644 index 0000000..c908fcd --- /dev/null +++ b/webapp-next/components/login/WrapperForm.tsx @@ -0,0 +1,33 @@ +import { Box, Heading } from "@chakra-ui/react"; +import React from "react"; + +export const WrapperForm = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => { + return ( + + + + {title} + + <>{children} + + + ); +};