From 0cf67261a0069151cb7a2c685d7b59676b4629af Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 15:15:25 +0100 Subject: [PATCH 01/12] :sparkles: Customer login page --- .../Layouts/LoginCustomer/classes.module.scss | 34 +++++++++++++++---- .../Layouts/LoginCustomer/index.tsx | 21 +++++++++--- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/front/Components/Layouts/LoginCustomer/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/classes.module.scss index e1e032e8..2fd71d0d 100644 --- a/src/front/Components/Layouts/LoginCustomer/classes.module.scss +++ b/src/front/Components/Layouts/LoginCustomer/classes.module.scss @@ -2,28 +2,48 @@ .root { display: flex; - align-items: center; - justify-content: center; flex-direction: column; - height: 100%; max-width: 530px; margin: auto; + margin-top: 220px; .title { - margin: 32px 0; - text-align: center; + margin-bottom: 32px; + text-align: left; @media (max-width: $screen-s) { font-family: 48px; } } + .logo { + margin-top: 32px; + cursor: pointer; + } + + .what_is_france_connect { + color: var(--light-text-action-high-blue-france, #000091); + /* 2.Corps de texte/SM - Texte détail/Desktop & Mobile - Regular */ + font-family: Marianne; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + margin-top: 12px; + } + + .or { + margin-top: 32px; + } .forget-password { margin-top: 32px; margin-bottom: 8px; } - .logo { - cursor: pointer; + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } } } diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 40cb3edf..fb386de1 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -1,4 +1,3 @@ -import CoffreIcon from "@Assets/Icons/coffre.svg"; import franceConnectLogo from "./france-connect.svg"; import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; @@ -11,6 +10,8 @@ import classes from "./classes.module.scss"; import LandingImage from "./landing-connect.jpeg"; import Link from "next/link"; import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; export default function Login() { const router = useRouter(); @@ -45,17 +46,27 @@ export default function Login() { return (
- coffre -
Connexion espace client
+
Identifiez-vous
+ Pour accéder à votre espace de dépôt des documents, veuillez vous identifier. france-connect - +
Qu'est ce que FranceConnect ?
+ + Ou + +
+ + + + {/*
Vous n'arrivez pas à vous connecter ?
- + */}
Date: Fri, 24 Nov 2023 15:34:24 +0100 Subject: [PATCH 02/12] :sparkles: Verify sms request working --- src/front/Api/Auth/Customer/Auth.ts | 29 ++++++++++++++++ .../Layouts/LoginCustomer/index.tsx | 33 ++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 src/front/Api/Auth/Customer/Auth.ts diff --git a/src/front/Api/Auth/Customer/Auth.ts b/src/front/Api/Auth/Customer/Auth.ts new file mode 100644 index 00000000..7d0481f2 --- /dev/null +++ b/src/front/Api/Auth/Customer/Auth.ts @@ -0,0 +1,29 @@ +import BaseApiService from "@Front/Api/BaseApiService"; + +export type IMailVerifyParams = { + email: string; +}; + +export type IMailVerifyReturn = { + partialPhoneNumber: string; +}; + +export default class Auth extends BaseApiService { + private static instance: Auth; + protected readonly namespaceUrl = this.getBaseUrl().concat("/customer"); + private readonly baseURl = this.namespaceUrl.concat("/auth"); + + public static getInstance() { + return (this.instance ??= new this()); + } + + public async mailVerifySms(body: IMailVerifyParams): Promise { + const url = new URL(this.baseURl.concat("/mail/verify-sms")); + try { + return this.postRequest(url, body); + } catch (err) { + this.onError(err); + return Promise.reject(err); + } + } +} diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index fb386de1..384f59fd 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -8,15 +8,17 @@ import { useCallback, useEffect, useState } from "react"; import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; import classes from "./classes.module.scss"; import LandingImage from "./landing-connect.jpeg"; -import Link from "next/link"; import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; import Form from "@Front/Components/DesignSystem/Form"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Module from "@Front/Config/Module"; +import { ValidationError } from "class-validator"; +import Auth from "@Front/Api/Auth/Customer/Auth"; export default function Login() { const router = useRouter(); const error = router.query["error"]; - + const [validationError, setValidationError] = useState([]); const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); const redirectCustomerOnConnection = useCallback(() => { @@ -43,6 +45,25 @@ export default function Login() { if (error === "1") openErrorModal(); }, [error, openErrorModal]); + const onSubmitHandler = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + console.log(values); + if (!values["email"]) return; + const phoneNumber = await Auth.getInstance().mailVerifySms({ email: values["email"] }); + /* router.push( + Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path.replace("[uid]", "1"), + ); */ + } catch (validationErrors: Array | any) { + console.log(validationErrors); + if (validationErrors.length > 0) { + setValidationError(validationErrors as ValidationError[]); + } + return; + } + }, + [router], + ); return (
@@ -55,8 +76,12 @@ export default function Login() { Ou -
- + + error.property === "email")} + /> From 81731b72d82a2409c4d3a1c06124415e9c4380ef Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Fri, 24 Nov 2023 17:27:57 +0100 Subject: [PATCH 03/12] :construction: WIP login page --- src/front/Api/BaseApiService.ts | 7 +- .../StepEmail/classes.module.scss | 49 ++++++++++ .../Layouts/LoginCustomer/StepEmail/index.tsx | 91 +++++++++++++++++++ .../StepTotp/classes.module.scss | 24 +++++ .../Layouts/LoginCustomer/StepTotp/index.tsx | 67 ++++++++++++++ .../Layouts/LoginCustomer/classes.module.scss | 45 --------- .../Layouts/LoginCustomer/index.tsx | 81 ++++------------- 7 files changed, 253 insertions(+), 111 deletions(-) create mode 100644 src/front/Components/Layouts/LoginCustomer/StepEmail/classes.module.scss create mode 100644 src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx create mode 100644 src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss create mode 100644 src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx diff --git a/src/front/Api/BaseApiService.ts b/src/front/Api/BaseApiService.ts index 999ae0f0..198141b2 100644 --- a/src/front/Api/BaseApiService.ts +++ b/src/front/Api/BaseApiService.ts @@ -138,11 +138,16 @@ export default abstract class BaseApiService { } } else { // Handle error response + const responseCopy = response.clone(); try { const responseJson = await response.json(); return Promise.reject(responseJson); } catch (err) { - return Promise.reject(err); + const responseText = await responseCopy.text(); + return Promise.reject({ + http_status: response.status, + message: responseText, + }); } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepEmail/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepEmail/classes.module.scss new file mode 100644 index 00000000..2fd71d0d --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepEmail/classes.module.scss @@ -0,0 +1,49 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + margin-bottom: 32px; + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .logo { + margin-top: 32px; + cursor: pointer; + } + + .what_is_france_connect { + color: var(--light-text-action-high-blue-france, #000091); + /* 2.Corps de texte/SM - Texte détail/Desktop & Mobile - Regular */ + font-family: Marianne; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 171.429% */ + margin-top: 12px; + } + + .or { + margin-top: 32px; + } + .forget-password { + margin-top: 32px; + margin-bottom: 8px; + } + + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } + } +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx new file mode 100644 index 00000000..63607abe --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx @@ -0,0 +1,91 @@ +import React, { useCallback, useState } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; +import Image from "next/image"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import franceConnectLogo from "../france-connect.svg"; +import { useRouter } from "next/router"; +import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; +import { ValidationError } from "class-validator"; +import { LoginStep } from ".."; +import Auth from "@Front/Api/Auth/Customer/Auth"; +type IProps = { + setPartialPhoneNumber: (phoneNumber: string) => void; + setStep: (step: LoginStep) => void; +}; + +export default function StepEmail(props: IProps) { + const { setPartialPhoneNumber, setStep } = props; + const [validationError, setValidationError] = useState([]); + const router = useRouter(); + const redirectCustomerOnConnection = useCallback(() => { + async function getCustomer() { + try { + const loginRes = await Customers.getInstance().login(); + router.push(loginRes.enrollment.franceConnectUrl); + } catch (e) { + console.error(e); + } + } + getCustomer(); + }, [router]); + + const onSubmitHandler = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + if (!values["email"]) return; + const res = await Auth.getInstance().mailVerifySms({ email: values["email"] }); + setPartialPhoneNumber(res.partialPhoneNumber); + setStep(LoginStep.TOTP); + } catch (error: any) { + // If token already exists and is still valid redirect to the connect/register page + if (error.http_status === 425) { + setStep(LoginStep.TOTP); + return; + } + setValidationError([ + { + property: "email", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [setPartialPhoneNumber, setStep], + ); + + return ( +
+ +
Identifiez-vous
+
+ Pour accéder à votre espace de dépôt des documents, veuillez vous identifier. + france-connect +
Qu'est ce que FranceConnect ?
+ + Ou + + + error.property === "email")} + /> + + + {/* +
Vous n'arrivez pas à vous connecter ?
+
+ + + */} +
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss new file mode 100644 index 00000000..efbcd6ad --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss @@ -0,0 +1,24 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } + } +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx new file mode 100644 index 00000000..421ea78f --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx @@ -0,0 +1,67 @@ +import React, { useCallback, useState } from "react"; +import classes from "./classes.module.scss"; +import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; +import Image from "next/image"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import franceConnectLogo from "./france-connect.svg"; +import { useRouter } from "next/router"; +import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; +import { ValidationError } from "class-validator"; +import { LoginStep } from ".."; +import Auth from "@Front/Api/Auth/Customer/Auth"; +type IProps = { + setStep: (step: LoginStep) => void; + setTotpCode: (code: string) => void; + partialPhoneNumber: string; +}; + +export default function StepTotp(props: IProps) { + const { setStep, setTotpCode, partialPhoneNumber } = props; + const [validationError, setValidationError] = useState([]); + const router = useRouter(); + const onSubmitHandler = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + if (!values["email"]) return; + const res = await Auth.getInstance().mailVerifySms({ email: values["email"] }); + setStep(LoginStep.TOTP); + } catch (error: any) { + // If token already exists and is still valid redirect to the connect/register page + if (error.http_status === 425) { + setStep(LoginStep.TOTP); + return; + } + setValidationError([ + { + property: "code", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [setStep], + ); + + return ( +
+ +
Votre code a été envoyé par SMS au ** ** ** {partialPhoneNumber}
+
+
+ error.property === "code")} + /> + + +
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/classes.module.scss index 2fd71d0d..f68b3ab0 100644 --- a/src/front/Components/Layouts/LoginCustomer/classes.module.scss +++ b/src/front/Components/Layouts/LoginCustomer/classes.module.scss @@ -1,49 +1,4 @@ @import "@Themes/constants.scss"; .root { - display: flex; - flex-direction: column; - max-width: 530px; - margin: auto; - margin-top: 220px; - - .title { - margin-bottom: 32px; - text-align: left; - - @media (max-width: $screen-s) { - font-family: 48px; - } - } - - .logo { - margin-top: 32px; - cursor: pointer; - } - - .what_is_france_connect { - color: var(--light-text-action-high-blue-france, #000091); - /* 2.Corps de texte/SM - Texte détail/Desktop & Mobile - Regular */ - font-family: Marianne; - font-size: 14px; - font-style: normal; - font-weight: 400; - line-height: 24px; /* 171.429% */ - margin-top: 12px; - } - - .or { - margin-top: 32px; - } - .forget-password { - margin-top: 32px; - margin-bottom: 8px; - } - - .form { - margin-top: 32px; - .submit_button { - margin-top: 32px; - } - } } diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 384f59fd..16901f60 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -1,37 +1,28 @@ -import franceConnectLogo from "./france-connect.svg"; -import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage"; -import Image from "next/image"; import { useRouter } from "next/router"; import { useCallback, useEffect, useState } from "react"; -import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; + import classes from "./classes.module.scss"; import LandingImage from "./landing-connect.jpeg"; import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; -import Form from "@Front/Components/DesignSystem/Form"; -import TextField from "@Front/Components/DesignSystem/Form/TextField"; -import Module from "@Front/Config/Module"; -import { ValidationError } from "class-validator"; -import Auth from "@Front/Api/Auth/Customer/Auth"; +import StepEmail from "./StepEmail"; +import StepTotp from "./StepTotp"; +export enum LoginStep { + EMAIL, + TOTP, + NEW_PASSWORD, +} export default function Login() { const router = useRouter(); const error = router.query["error"]; - const [validationError, setValidationError] = useState([]); + const [isErrorModalOpen, setIsErrorModalOpen] = useState(false); - const redirectCustomerOnConnection = useCallback(() => { - async function getCustomer() { - try { - const loginRes = await Customers.getInstance().login(); - router.push(loginRes.enrollment.franceConnectUrl); - } catch (e) { - console.error(e); - } - } - getCustomer(); - }, [router]); + const [step, setStep] = useState(LoginStep.EMAIL); + const [totpCode, setTotpCode] = useState(""); + const [partialPhoneNumber, setPartialPhoneNumber] = useState(""); const openErrorModal = useCallback(() => { setIsErrorModalOpen(true); @@ -45,53 +36,13 @@ export default function Login() { if (error === "1") openErrorModal(); }, [error, openErrorModal]); - const onSubmitHandler = useCallback( - async (e: React.FormEvent | null, values: { [key: string]: string }) => { - try { - console.log(values); - if (!values["email"]) return; - const phoneNumber = await Auth.getInstance().mailVerifySms({ email: values["email"] }); - /* router.push( - Module.getInstance().get().modules.pages.DeedTypes.pages.DeedTypesInformations.props.path.replace("[uid]", "1"), - ); */ - } catch (validationErrors: Array | any) { - console.log(validationErrors); - if (validationErrors.length > 0) { - setValidationError(validationErrors as ValidationError[]); - } - return; - } - }, - [router], - ); return (
- -
Identifiez-vous
-
- Pour accéder à votre espace de dépôt des documents, veuillez vous identifier. - france-connect -
Qu'est ce que FranceConnect ?
- - Ou - -
- error.property === "email")} - /> - - - {/* -
Vous n'arrivez pas à vous connecter ?
-
- - - */} + {step === LoginStep.EMAIL && } + {step === LoginStep.TOTP && ( + + )}
Date: Mon, 27 Nov 2023 10:02:43 +0100 Subject: [PATCH 04/12] :sparkles: Two first steps working --- src/front/Api/Auth/Customer/Auth.ts | 19 ++++++ .../Layouts/LoginCustomer/StepEmail/index.tsx | 42 ++----------- .../Layouts/LoginCustomer/StepTotp/index.tsx | 47 +++----------- .../Layouts/LoginCustomer/index.tsx | 63 ++++++++++++++++++- 4 files changed, 92 insertions(+), 79 deletions(-) diff --git a/src/front/Api/Auth/Customer/Auth.ts b/src/front/Api/Auth/Customer/Auth.ts index 7d0481f2..d629f0eb 100644 --- a/src/front/Api/Auth/Customer/Auth.ts +++ b/src/front/Api/Auth/Customer/Auth.ts @@ -8,6 +8,15 @@ export type IMailVerifyReturn = { partialPhoneNumber: string; }; +export type IVerifyTotpCodeParams = { + totpCode: string; + email: string; +}; + +export type IVerifyTotpCodeReturn = { + validCode: boolean; +}; + export default class Auth extends BaseApiService { private static instance: Auth; protected readonly namespaceUrl = this.getBaseUrl().concat("/customer"); @@ -26,4 +35,14 @@ export default class Auth extends BaseApiService { return Promise.reject(err); } } + + public async verifyTotpCode(body: IVerifyTotpCodeParams): Promise { + const url = new URL(this.baseURl.concat("/verify-totp-code")); + try { + return this.postRequest(url, body); + } catch (err) { + this.onError(err); + return Promise.reject(err); + } + } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx index 63607abe..5a2595f6 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepEmail/index.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from "react"; +import React, { useCallback } from "react"; import classes from "./classes.module.scss"; import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; import Image from "next/image"; @@ -9,16 +9,13 @@ import franceConnectLogo from "../france-connect.svg"; import { useRouter } from "next/router"; import Customers from "@Front/Api/Auth/Id360/Customers/Customers"; import { ValidationError } from "class-validator"; -import { LoginStep } from ".."; -import Auth from "@Front/Api/Auth/Customer/Auth"; type IProps = { - setPartialPhoneNumber: (phoneNumber: string) => void; - setStep: (step: LoginStep) => void; + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; }; export default function StepEmail(props: IProps) { - const { setPartialPhoneNumber, setStep } = props; - const [validationError, setValidationError] = useState([]); + const { onSubmit, validationErrors } = props; const router = useRouter(); const redirectCustomerOnConnection = useCallback(() => { async function getCustomer() { @@ -32,33 +29,6 @@ export default function StepEmail(props: IProps) { getCustomer(); }, [router]); - const onSubmitHandler = useCallback( - async (e: React.FormEvent | null, values: { [key: string]: string }) => { - try { - if (!values["email"]) return; - const res = await Auth.getInstance().mailVerifySms({ email: values["email"] }); - setPartialPhoneNumber(res.partialPhoneNumber); - setStep(LoginStep.TOTP); - } catch (error: any) { - // If token already exists and is still valid redirect to the connect/register page - if (error.http_status === 425) { - setStep(LoginStep.TOTP); - return; - } - setValidationError([ - { - property: "email", - constraints: { - [error.http_status]: error.message, - }, - }, - ]); - return; - } - }, - [setPartialPhoneNumber, setStep], - ); - return (
@@ -70,11 +40,11 @@ export default function StepEmail(props: IProps) { Ou -
+ error.property === "email")} + validationError={validationErrors.find((error) => error.property === "email")} /> + +
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss new file mode 100644 index 00000000..efbcd6ad --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss @@ -0,0 +1,24 @@ +@import "@Themes/constants.scss"; + +.root { + display: flex; + flex-direction: column; + max-width: 530px; + margin: auto; + margin-top: 220px; + + .title { + text-align: left; + + @media (max-width: $screen-s) { + font-family: 48px; + } + } + + .form { + margin-top: 32px; + .submit_button { + margin-top: 32px; + } + } +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx new file mode 100644 index 00000000..9d379a81 --- /dev/null +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import classes from "./classes.module.scss"; +import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; +import Form from "@Front/Components/DesignSystem/Form"; +import TextField from "@Front/Components/DesignSystem/Form/TextField"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import { ValidationError } from "class-validator"; +import Link from "next/link"; +type IProps = { + onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; + validationErrors: ValidationError[]; +}; + +export default function StepPassword(props: IProps) { + const { onSubmit, validationErrors } = props; + + return ( +
+ +
Entrez votre mot de passe
+
+
+ error.property === "password")} + password + /> + + +
Mot de passe oublié ?
+
+ + + +
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 7a4267c1..4418fa4d 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -10,10 +10,15 @@ import StepEmail from "./StepEmail"; import StepTotp from "./StepTotp"; import Auth from "@Front/Api/Auth/Customer/Auth"; import { ValidationError } from "class-validator"; +import StepPassword from "./StepPassword"; +import StepNewPassword from "./StepNewPassword"; +import CustomerStore from "@Front/Stores/CustomerStore"; +import Module from "@Front/Config/Module"; export enum LoginStep { EMAIL, TOTP, + PASSWORD, NEW_PASSWORD, } export default function Login() { @@ -27,6 +32,7 @@ export default function Login() { const [email, setEmail] = useState(""); const [partialPhoneNumber, setPartialPhoneNumber] = useState(""); const [validationErrors, setValidationErrors] = useState([]); + const openErrorModal = useCallback(() => { setIsErrorModalOpen(true); }, []); @@ -72,10 +78,14 @@ export default function Login() { try { if (!values["totpCode"]) return; const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email }); - if (res.validCode) { - setTotpCode(values["totpCode"]); - setStep(LoginStep.NEW_PASSWORD); - } + + // If the code is valid setting it in state + if (res.validCode) setTotpCode(values["totpCode"]); + + // If it's first connection, show the form for first connection + if (res.firstConnection) setStep(LoginStep.NEW_PASSWORD); + // Else just login normally + else setStep(LoginStep.PASSWORD); } catch (error: any) { setValidationErrors([ { @@ -90,6 +100,64 @@ export default function Login() { }, [email, setStep], ); + + const onNewPasswordSubmit = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + console.log(values); + if (!values["password"] || !values["confirm_password"]) return; + if (values["password"] !== values["confirm_password"]) { + setValidationErrors([ + { + property: "confirm_password", + constraints: { + "400": "Les mots de passe ne correspondent pas.", + }, + }, + ]); + return; + } + const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] }); + CustomerStore.instance.connect(token.accessToken, token.refreshToken); + router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); + // If set password worked, setting the token and redirecting + } catch (error: any) { + setValidationErrors([ + { + property: "password", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [email, totpCode, setValidationErrors], + ); + + const onPasswordSubmit = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + if (!values["password"]) return; + const res = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] }); + + // If set password worked, setting the token and redirecting + } catch (error: any) { + setValidationErrors([ + { + property: "password", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [email, totpCode], + ); + return (
@@ -97,9 +165,8 @@ export default function Login() { {step === LoginStep.TOTP && ( )} - {step === LoginStep.NEW_PASSWORD && ( - - )} + {step === LoginStep.PASSWORD && } + {step === LoginStep.NEW_PASSWORD && }
Date: Mon, 27 Nov 2023 10:54:07 +0100 Subject: [PATCH 06/12] :sparkles: Login working --- src/front/Api/Auth/Customer/Auth.ts | 16 ++++++++++++++++ .../Components/Layouts/LoginCustomer/index.tsx | 12 ++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/front/Api/Auth/Customer/Auth.ts b/src/front/Api/Auth/Customer/Auth.ts index 60c0788d..c04686c1 100644 --- a/src/front/Api/Auth/Customer/Auth.ts +++ b/src/front/Api/Auth/Customer/Auth.ts @@ -25,6 +25,12 @@ export type ISetPasswordParams = { totpCode: string; }; +export type ILoginParams = { + password: string; + email: string; + totpCode: string; +}; + export default class Auth extends BaseApiService { private static instance: Auth; protected readonly namespaceUrl = this.getBaseUrl().concat("/customer"); @@ -63,4 +69,14 @@ export default class Auth extends BaseApiService { return Promise.reject(err); } } + + public async login(body: ILoginParams): Promise { + const url = new URL(this.baseURl.concat("/login")); + try { + return this.postRequest(url, body); + } catch (err) { + this.onError(err); + return Promise.reject(err); + } + } } diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 4418fa4d..788c1700 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -49,9 +49,9 @@ export default function Login() { async (e: React.FormEvent | null, values: { [key: string]: string }) => { try { if (!values["email"]) return; + setEmail(values["email"]); const res = await Auth.getInstance().mailVerifySms({ email: values["email"] }); setPartialPhoneNumber(res.partialPhoneNumber); - setEmail(values["email"]); setStep(LoginStep.TOTP); } catch (error: any) { // If token already exists and is still valid redirect to the connect/register page @@ -133,16 +133,16 @@ export default function Login() { return; } }, - [email, totpCode, setValidationErrors], + [totpCode, email, router], ); const onPasswordSubmit = useCallback( async (e: React.FormEvent | null, values: { [key: string]: string }) => { try { if (!values["password"]) return; - const res = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] }); - - // If set password worked, setting the token and redirecting + const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] }); + CustomerStore.instance.connect(token.accessToken, token.refreshToken); + router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); } catch (error: any) { setValidationErrors([ { @@ -155,7 +155,7 @@ export default function Login() { return; } }, - [email, totpCode], + [email, router, totpCode], ); return ( From 4d884b865b926d2d7f16be10b734a0c7988b1a73 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Mon, 27 Nov 2023 10:57:11 +0100 Subject: [PATCH 07/12] :sparkles: Login design for forgot password --- .../Layouts/LoginCustomer/StepPassword/classes.module.scss | 5 +++++ .../Components/Layouts/LoginCustomer/StepPassword/index.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss index efbcd6ad..3ef4f0ef 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss @@ -20,5 +20,10 @@ .submit_button { margin-top: 32px; } + + .forgot-password { + margin-top: 8px; + text-decoration: underline; + } } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx index 9d379a81..b4632b76 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx @@ -27,8 +27,8 @@ export default function StepPassword(props: IProps) { password /> - -
Mot de passe oublié ?
+ + Mot de passe oublié ? + +
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx index b4632b76..45888814 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx @@ -5,14 +5,29 @@ import Form from "@Front/Components/DesignSystem/Form"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; import { ValidationError } from "class-validator"; -import Link from "next/link"; +import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; type IProps = { onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; validationErrors: ValidationError[]; + onPasswordForgotClicked: () => void; }; export default function StepPassword(props: IProps) { - const { onSubmit, validationErrors } = props; + const { onSubmit, validationErrors, onPasswordForgotClicked } = props; + const [isModalOpened, setIsModalOpened] = React.useState(false); + + const closeModal = () => { + setIsModalOpened(false); + }; + + const openModal = () => { + setIsModalOpened(true); + }; + + const onModalAccept = () => { + onPasswordForgotClicked(); + setIsModalOpened(false); + }; return (
@@ -26,15 +41,29 @@ export default function StepPassword(props: IProps) { validationError={validationErrors.find((error) => error.property === "password")} password /> - +
Mot de passe oublié ? - +
+ +
+ + Un code à usage unique va vous être envoyé par sms pour réinitialiser votre mot de passe. + +
+
); } diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 3fe19a7f..0030af15 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -15,12 +15,14 @@ import StepNewPassword from "./StepNewPassword"; import CustomerStore from "@Front/Stores/CustomerStore"; import Module from "@Front/Config/Module"; import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; +import PasswordForgotten from "./PasswordForgotten"; export enum LoginStep { EMAIL, TOTP, PASSWORD, NEW_PASSWORD, + PASSWORD_FORGOTTEN, } export default function Login() { const router = useRouter(); @@ -85,6 +87,8 @@ export default function Login() { // If it's first connection, show the form for first connection if (res.reason === TotpCodesReasons.FIRST_LOGIN) setStep(LoginStep.NEW_PASSWORD); + // If it's password forgotten, show the form for password forgotten + else if (res.reason === TotpCodesReasons.RESET_PASSWORD) setStep(LoginStep.PASSWORD_FORGOTTEN); // Else just login normally else setStep(LoginStep.PASSWORD); } catch (error: any) { @@ -105,7 +109,6 @@ export default function Login() { const onNewPasswordSubmit = useCallback( async (e: React.FormEvent | null, values: { [key: string]: string }) => { try { - console.log(values); if (!values["password"] || !values["confirm_password"]) return; if (values["password"] !== values["confirm_password"]) { setValidationErrors([ @@ -159,6 +162,21 @@ export default function Login() { [email, router, totpCode], ); + const onPasswordForgotClicked = useCallback(async () => { + try { + const res = await Auth.getInstance().askNewPassword({ email }); + setPartialPhoneNumber(res.partialPhoneNumber); + setStep(LoginStep.TOTP); + } catch (error: any) { + // If token already exists and is still valid redirect to the connect/register page + if (error.http_status === 425) { + setStep(LoginStep.TOTP); + return; + } + return; + } + }, [email]); + return (
@@ -166,8 +184,17 @@ export default function Login() { {step === LoginStep.TOTP && ( )} - {step === LoginStep.PASSWORD && } + {step === LoginStep.PASSWORD && ( + + )} {step === LoginStep.NEW_PASSWORD && } + {step === LoginStep.PASSWORD_FORGOTTEN && ( + + )}
Date: Wed, 29 Nov 2023 16:47:38 +0100 Subject: [PATCH 10/12] :sparkles: Send another code working --- src/front/Api/Auth/Customer/Auth.ts | 10 ++++ .../StepTotp/classes.module.scss | 20 ++++++++ .../Layouts/LoginCustomer/StepTotp/index.tsx | 47 +++++++++++++++++-- .../Layouts/LoginCustomer/index.tsx | 15 +++++- 4 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/front/Api/Auth/Customer/Auth.ts b/src/front/Api/Auth/Customer/Auth.ts index 237df5c3..906d939d 100644 --- a/src/front/Api/Auth/Customer/Auth.ts +++ b/src/front/Api/Auth/Customer/Auth.ts @@ -94,4 +94,14 @@ export default class Auth extends BaseApiService { return Promise.reject(err); } } + + public async sendAnotherCode(body: IMailVerifyParams): Promise { + const url = new URL(this.baseURl.concat("/send-another-code")); + try { + return this.postRequest(url, body); + } catch (err) { + this.onError(err); + return Promise.reject(err); + } + } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss index efbcd6ad..fae541af 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss +++ b/src/front/Components/Layouts/LoginCustomer/StepTotp/classes.module.scss @@ -21,4 +21,24 @@ margin-top: 32px; } } + + .ask-another-code { + margin-top: 48px; + display: flex; + flex-direction: column; + gap: 16px; + align-items: flex-start; + + .new-code-button { + &[data-disabled="true"] { + opacity: 0.5; + cursor: not-allowed; + } + } + .new-code-timer { + display: flex; + gap: 6px; + align-items: center; + } + } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx index c71044ee..443c2369 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx @@ -1,6 +1,6 @@ -import React from "react"; +import React, { useEffect } from "react"; import classes from "./classes.module.scss"; -import Typography, { ITypo } from "@Front/Components/DesignSystem/Typography"; +import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography"; import Form from "@Front/Components/DesignSystem/Form"; import TextField from "@Front/Components/DesignSystem/Form/TextField"; import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; @@ -9,15 +9,37 @@ type IProps = { onSubmit: (e: React.FormEvent | null, values: { [key: string]: string }) => void; validationErrors: ValidationError[]; partialPhoneNumber: string; + onSendAnotherCode: () => void; }; export default function StepTotp(props: IProps) { - const { onSubmit, validationErrors, partialPhoneNumber } = props; + const { onSubmit, validationErrors, partialPhoneNumber, onSendAnotherCode } = props; + const [disableNewCodeButton, setDisableNewCodeButton] = React.useState(true); + const [secondsBeforeNewCode, setSecondsBeforeNewCode] = React.useState(30); + + useEffect(() => { + const interval = setInterval(() => { + if (secondsBeforeNewCode > 0) { + setSecondsBeforeNewCode(secondsBeforeNewCode - 1); + } else { + setDisableNewCodeButton(false); + } + }, 1000); + return () => clearInterval(interval); + }, [secondsBeforeNewCode]); + + const sendAnotherCode = () => { + onSendAnotherCode(); + setDisableNewCodeButton(true); + setSecondsBeforeNewCode(30); + }; return (
-
Votre code a été envoyé par SMS au ** ** ** {partialPhoneNumber}
+
+ Votre code a été envoyé par SMS au ** ** ** {partialPhoneNumber.replace(/(.{2})/g, "$1 ")} +
+
+ Vous n'avez rien reçu ? + + + Redemandez un code dans + + 00:{secondsBeforeNewCode < 10 ? `0${secondsBeforeNewCode}` : secondsBeforeNewCode} + + +
); } diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index 0030af15..c0a48bcc 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -177,12 +177,25 @@ export default function Login() { } }, [email]); + const onSendAnotherCode = useCallback(async () => { + try { + await Auth.getInstance().sendAnotherCode({ email }); + } catch (error: any) { + return; + } + }, [email]); + return (
{step === LoginStep.EMAIL && } {step === LoginStep.TOTP && ( - + )} {step === LoginStep.PASSWORD && ( Date: Wed, 29 Nov 2023 16:52:38 +0100 Subject: [PATCH 11/12] :sparkles: Sending validation errors to totp --- .../Components/Layouts/LoginCustomer/StepTotp/index.tsx | 2 +- src/front/Components/Layouts/LoginCustomer/index.tsx | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx index 443c2369..635a52de 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepTotp/index.tsx @@ -56,8 +56,8 @@ export default function StepTotp(props: IProps) { diff --git a/src/front/Components/Layouts/LoginCustomer/index.tsx b/src/front/Components/Layouts/LoginCustomer/index.tsx index c0a48bcc..c4f1e117 100644 --- a/src/front/Components/Layouts/LoginCustomer/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/index.tsx @@ -181,6 +181,14 @@ export default function Login() { try { await Auth.getInstance().sendAnotherCode({ email }); } catch (error: any) { + setValidationErrors([ + { + property: "totpCode", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); return; } }, [email]); From 1fbd6ce79e57f9f343f5468d64306aa615889f61 Mon Sep 17 00:00:00 2001 From: Maxime Lalo Date: Wed, 29 Nov 2023 16:54:20 +0100 Subject: [PATCH 12/12] :bug: Small adjustments --- .../Layouts/LoginCustomer/StepPassword/classes.module.scss | 1 + .../Components/Layouts/LoginCustomer/StepPassword/index.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss index 3ef4f0ef..0244c442 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/classes.module.scss @@ -24,6 +24,7 @@ .forgot-password { margin-top: 8px; text-decoration: underline; + cursor: pointer; } } } diff --git a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx index 45888814..978dd97b 100644 --- a/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx +++ b/src/front/Components/Layouts/LoginCustomer/StepPassword/index.tsx @@ -57,7 +57,8 @@ export default function StepPassword(props: IProps) { onAccept={onModalAccept} closeBtn header={"Mot de passe oublié ?"} - confirmText={"Valider"}> + confirmText={"Valider"} + cancelText={"Annuler"}>
Un code à usage unique va vous être envoyé par sms pour réinitialiser votre mot de passe.