diff --git a/package-lock.json b/package-lock.json index d5f111c7..7988639e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-toastify": "^9.1.3", "sass": "^1.59.2", "sharp": "^0.32.1", + "ts-pattern": "^4.3.0", "typescript": "4.9.5", "uuidv4": "^6.2.13" } @@ -4910,6 +4911,11 @@ "node": ">=8.0" } }, + "node_modules/ts-pattern": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-4.3.0.tgz", + "integrity": "sha512-pefrkcd4lmIVR0LA49Imjf9DYLK8vtWhqBPA3Ya1ir8xCW0O2yjL9dsCVvI7pCodLC5q7smNpEtDR2yVulQxOg==" + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", diff --git a/package.json b/package.json index 3d64ae97..5ae85dcc 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev", + "dev": "PORT=5005 next dev", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/src/front/Components/Layouts/Login/classes.module.scss b/src/front/Components/Layouts/Login/classes.module.scss index 61a34f65..794913a1 100644 --- a/src/front/Components/Layouts/Login/classes.module.scss +++ b/src/front/Components/Layouts/Login/classes.module.scss @@ -1,24 +1,25 @@ @import "@Themes/constants.scss"; .root { - padding: 40px 40px 40px 64px; - .notary-container { - display: flex; - flex-direction: column; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + height: 100%; + max-width: 530px; + margin: auto; - .title { - margin-bottom: 24px; - } + .title { + margin: 32px 0; + text-align: center; - .forget-password { - margin-top: 24px; - margin-bottom: 8px; - } - - .separator { - margin: 48px 0; - height: 1px; - background-color: var(--grey-medium); + @media (max-width: $screen-s) { + font-family: 48px; } } + + .forget-password { + margin-top: 32px; + margin-bottom: 8px; + } } diff --git a/src/front/Components/Layouts/Login/index.tsx b/src/front/Components/Layouts/Login/index.tsx index cbfa4ffe..d5a2fc02 100644 --- a/src/front/Components/Layouts/Login/index.tsx +++ b/src/front/Components/Layouts/Login/index.tsx @@ -1,214 +1,24 @@ +import CoffreIcon from "@Assets/Icons/coffre.svg"; +import idNoteLogo from "@Assets/Icons/id-note-logo.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 classes from "./classes.module.scss"; import LandingImage from "./landing-connect.jpeg"; -import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; -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"; -import { TotpCodesReasons } from "le-coffre-resources/dist/Customer/TotpCodes"; -import PasswordForgotten from "./PasswordForgotten"; import { FrontendVariables } from "@Front/Config/VariablesFront"; -import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; import Link from "next/link"; -import idNoteLogo from "@Assets/Icons/id-note-logo.svg"; +import Confirm from "@Front/Components/DesignSystem/Modal/Confirm"; -export enum LoginStep { - EMAIL, - TOTP, - PASSWORD, - NEW_PASSWORD, - PASSWORD_FORGOTTEN, -} export default function Login() { const router = useRouter(); const error = router.query["error"]; const [isErrorModalOpen, setIsErrorModalOpen] = useState(0); - const [step, setStep] = useState(LoginStep.EMAIL); - const [totpCodeUid, setTotpCodeUid] = useState(""); - const [totpCode, setTotpCode] = useState(""); - const [email, setEmail] = useState(""); - const [partialPhoneNumber, setPartialPhoneNumber] = useState(""); - const [validationErrors, setValidationErrors] = useState([]); - - const openErrorModal = useCallback(() => { - setIsErrorModalOpen(1); - }, []); - - const closeErrorModal = useCallback(() => { - setIsErrorModalOpen(0); - }, []); - - const onEmailFormSubmit = useCallback(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); - setTotpCodeUid(res.totpCodeUid); - setStep(LoginStep.TOTP); - setValidationErrors([]); - } catch (error: any) { - setValidationErrors([ - { - property: "email", - constraints: { - [error.http_status]: error.message, - }, - }, - ]); - return; - } - }, []); - - const onSmsCodeSubmit = useCallback( - async (e: React.FormEvent | null, values: { [key: string]: string }) => { - try { - if (!values["totpCode"]) return; - const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email }); - - // If the code is valid setting it in state - if (res.validCode) setTotpCode(values["totpCode"]); - - setValidationErrors([]); - // 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) { - setValidationErrors([ - { - property: "totpCode", - constraints: { - [error.http_status]: error.message, - }, - }, - ]); - return; - } - }, - [email, setStep], - ); - - const onNewPasswordSubmit = useCallback( - async (e: React.FormEvent | null, values: { [key: string]: string }) => { - try { - 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 passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/); - if (!passwordRegex.test(values["password"])) { - setValidationErrors([ - { - property: "password", - constraints: { - "400": "Le mot de passe doit contenir au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.", - }, - }, - ]); - return; - } - const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] }); - CustomerStore.instance.connect(token.accessToken, token.refreshToken); - setValidationErrors([]); - 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; - } - }, - [totpCode, email, router], - ); - - const onPasswordSubmit = useCallback( - async (e: React.FormEvent | null, values: { [key: string]: string }) => { - try { - if (!values["password"]) return; - const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] }); - CustomerStore.instance.connect(token.accessToken, token.refreshToken); - setValidationErrors([]); - router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); - } catch (error: any) { - setValidationErrors([ - { - property: "password", - constraints: { - [error.http_status]: error.message, - }, - }, - ]); - return; - } - }, - [email, router, totpCode], - ); - - const onPasswordForgotClicked = useCallback(async () => { - try { - const res = await Auth.getInstance().askNewPassword({ email }); - setPartialPhoneNumber(res.partialPhoneNumber); - setValidationErrors([]); - 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]); - - const onSendAnotherCode = useCallback(async () => { - try { - const res = await Auth.getInstance().sendAnotherCode({ email, totpCodeUid }); - - setValidationErrors([]); - setPartialPhoneNumber(res.partialPhoneNumber); - setTotpCodeUid(res.totpCodeUid); - } catch (error: any) { - setValidationErrors([ - { - property: "totpCode", - constraints: { - [error.http_status]: error.message, - }, - }, - ]); - return; - } - }, [email, totpCodeUid]); - const redirectUserOnConnection = useCallback(() => { const variables = FrontendVariables.getInstance(); router.push( @@ -218,6 +28,14 @@ export default function Login() { ); }, [router]); + const openErrorModal = useCallback((index: number) => { + setIsErrorModalOpen(index); + }, []); + + const closeErrorModal = useCallback(() => { + setIsErrorModalOpen(0); + }, []); + const closeNoEmailModal = useCallback(() => { setIsErrorModalOpen(0); router.push("https://connexion.idnot.fr/"); @@ -229,49 +47,25 @@ export default function Login() { }; useEffect(() => { - if (error === "1") openErrorModal(); + openErrorModal(parseInt(error as string)); }, [error, openErrorModal]); return (
- {step === LoginStep.EMAIL && ( -
- - Connectez vous en tant que notaire - - - - Vous n'arrivez pas à vous connecter ? - - - - -
-
- )} - {step === LoginStep.EMAIL && } - {step === LoginStep.TOTP && ( - - )} - {step === LoginStep.PASSWORD && ( - - )} - {step === LoginStep.NEW_PASSWORD && } - {step === LoginStep.PASSWORD_FORGOTTEN && ( - - )} + coffre + +
Connexion espace professionnel
+
+ + +
Vous n'arrivez pas à vous connecter ?
+
+ + +
- Connectez-vous en tant que 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 */} + Pour accéder à votre espace de dépôt des documents, veuillez vous identifier.
(LoginStep.EMAIL); + const [totpCodeUid, setTotpCodeUid] = useState(""); + const [totpCode, setTotpCode] = useState(""); + const [email, setEmail] = useState(""); + const [partialPhoneNumber, setPartialPhoneNumber] = useState(""); + const [validationErrors, setValidationErrors] = useState([]); + + const openErrorModal = useCallback(() => { + setIsErrorModalOpen(true); + }, []); + + const closeErrorModal = useCallback(() => { + setIsErrorModalOpen(false); + }, []); + + useEffect(() => { + if (error === "1") openErrorModal(); + }, [error, openErrorModal]); + + const onEmailFormSubmit = useCallback(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); + setTotpCodeUid(res.totpCodeUid); + setStep(LoginStep.TOTP); + setValidationErrors([]); + } catch (error: any) { + setValidationErrors([ + { + property: "email", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, []); + + const onSmsCodeSubmit = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + if (!values["totpCode"]) return; + const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email }); + + // If the code is valid setting it in state + if (res.validCode) setTotpCode(values["totpCode"]); + + setValidationErrors([]); + // 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) { + setValidationErrors([ + { + property: "totpCode", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [email, setStep], + ); + + const onNewPasswordSubmit = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + 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 passwordRegex = new RegExp(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/); + if (!passwordRegex.test(values["password"])) { + setValidationErrors([ + { + property: "password", + constraints: { + "400": "Le mot de passe doit contenir au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.", + }, + }, + ]); + return; + } + const token = await Auth.getInstance().setPassword({ totpCode, email, password: values["password"] }); + CustomerStore.instance.connect(token.accessToken, token.refreshToken); + setValidationErrors([]); + 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; + } + }, + [totpCode, email, router], + ); + + const onPasswordSubmit = useCallback( + async (e: React.FormEvent | null, values: { [key: string]: string }) => { + try { + if (!values["password"]) return; + const token = await Auth.getInstance().login({ totpCode, email, password: values["password"] }); + CustomerStore.instance.connect(token.accessToken, token.refreshToken); + setValidationErrors([]); + router.push(Module.getInstance().get().modules.pages.Folder.pages.Select.props.path); + } catch (error: any) { + setValidationErrors([ + { + property: "password", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, + [email, router, totpCode], + ); + + const onPasswordForgotClicked = useCallback(async () => { + try { + const res = await Auth.getInstance().askNewPassword({ email }); + setPartialPhoneNumber(res.partialPhoneNumber); + setValidationErrors([]); + 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]); + + const onSendAnotherCode = useCallback(async () => { + try { + const res = await Auth.getInstance().sendAnotherCode({ email, totpCodeUid }); + + setValidationErrors([]); + setPartialPhoneNumber(res.partialPhoneNumber); + setTotpCodeUid(res.totpCodeUid); + } catch (error: any) { + setValidationErrors([ + { + property: "totpCode", + constraints: { + [error.http_status]: error.message, + }, + }, + ]); + return; + } + }, [email, totpCodeUid]); + + return ( + +
+ {step === LoginStep.EMAIL && } + {step === LoginStep.TOTP && ( + + )} + {step === LoginStep.PASSWORD && ( + + )} + {step === LoginStep.NEW_PASSWORD && } + {step === LoginStep.PASSWORD_FORGOTTEN && ( + + )} +
+ +
+ + Une erreur est survenue lors de la connexion. Veuillez réessayer. + +
+
+
+ ); +} diff --git a/src/front/Components/Layouts/LoginCustomer/landing-connect.jpeg b/src/front/Components/Layouts/LoginCustomer/landing-connect.jpeg new file mode 100644 index 00000000..789e0ef3 Binary files /dev/null and b/src/front/Components/Layouts/LoginCustomer/landing-connect.jpeg differ diff --git a/src/front/Components/Layouts/LoginHome/classes.module.scss b/src/front/Components/Layouts/LoginHome/classes.module.scss new file mode 100644 index 00000000..6d2dc12b --- /dev/null +++ b/src/front/Components/Layouts/LoginHome/classes.module.scss @@ -0,0 +1,23 @@ +.root { + padding: 64px; + + .content { + display: flex; + gap: 48px; + flex-direction: column; + margin-top: 48px; + + .section { + display: flex; + flex-direction: column; + gap: 24px; + } + .separator { + height: 1px; + background-color: var(--grey-medium); + } + } + .bottom { + margin-top: 48px; + } +} diff --git a/src/front/Components/Layouts/LoginHome/index.tsx b/src/front/Components/Layouts/LoginHome/index.tsx new file mode 100644 index 00000000..e39702d2 --- /dev/null +++ b/src/front/Components/Layouts/LoginHome/index.tsx @@ -0,0 +1,47 @@ +import Typography, { ITypo, ITypoColor } from "@Front/Components/DesignSystem/Typography"; +import DefaultDoubleSidePage from "@Front/Components/LayoutTemplates/DefaultDoubleSidePage"; + +import classes from "./classes.module.scss"; +import LandingImage from "../Login/landing-connect.jpeg"; +import Button, { EButtonVariant } from "@Front/Components/DesignSystem/Button"; +import Link from "next/link"; +import Module from "@Front/Config/Module"; + +export default function LoginHome() { + return ( + +
+ +
Connectez-vous à votre plateforme Lecoffre.io
+
+
+
+ + Je suis un notaire + + + + +
+
+
+ + Je suis un client + + + + +
+
+
+ +
Vous n'arrivez pas à vous connecter ?
+
+ + + +
+
+ + ); +} diff --git a/src/front/Components/Layouts/Subscription/Manage/SubscriptionManageCollaborators/index.tsx b/src/front/Components/Layouts/Subscription/Manage/SubscriptionManageCollaborators/index.tsx index 435157bf..c0f06dbb 100644 --- a/src/front/Components/Layouts/Subscription/Manage/SubscriptionManageCollaborators/index.tsx +++ b/src/front/Components/Layouts/Subscription/Manage/SubscriptionManageCollaborators/index.tsx @@ -71,7 +71,7 @@ export default function SubscriptionManageCollaborators() { useEffect(() => { loadSubscription(); loadCollaborators(); - }, [loadCollaborators, loadSubscription]); + }, [loadSubscription]); return ( diff --git a/src/middleware.ts b/src/middleware.ts index 39b4491e..1af4a2af 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -6,14 +6,14 @@ import type { NextRequest } from "next/server"; export async function middleware(request: NextRequest) { // Get the JWT from the cookies const cookies = request.cookies.get("leCoffreAccessToken"); - if (!cookies) return NextResponse.redirect(new URL("/login", request.url)); + if (!cookies) return NextResponse.redirect(new URL("/", request.url)); // Decode it const userDecodedToken = jwt_decode(cookies.value) as IUserJwtPayload; const customerDecodedToken = jwt_decode(cookies.value) as ICustomerJwtPayload; // If no JWT provided, redirect to login page - if (!userDecodedToken && !customerDecodedToken) return NextResponse.redirect(new URL("/login", request.url)); + if (!userDecodedToken && !customerDecodedToken) return NextResponse.redirect(new URL("/", request.url)); // If JWT expired, redirect to login callback page to refresh tokens const now = Math.floor(Date.now() / 1000); @@ -39,6 +39,5 @@ export const config = { "/offices/:path*", "/roles/:path*", "/users/:path*", - "/", ], }; diff --git a/src/pages/customer-login.tsx b/src/pages/customer-login.tsx index 40f96abe..785decd1 100644 --- a/src/pages/customer-login.tsx +++ b/src/pages/customer-login.tsx @@ -1,5 +1,5 @@ -import Login from "@Front/Components/Layouts/Login"; +import LoginCustomer from "@Front/Components/Layouts/LoginCustomer"; export default function Route() { - return ; + return ; } diff --git a/src/pages/customers/login.tsx b/src/pages/customers/login.tsx index fe395216..785decd1 100644 --- a/src/pages/customers/login.tsx +++ b/src/pages/customers/login.tsx @@ -1,4 +1,4 @@ -import LoginCustomer from "@Front/Components/Layouts/Login"; +import LoginCustomer from "@Front/Components/Layouts/LoginCustomer"; export default function Route() { return ; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 3def80ce..790c8a7c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,5 @@ -import Login from "./login"; +import LoginHome from "@Front/Components/Layouts/LoginHome"; export default function Route() { - return ; + return ; }