Set password first time working

This commit is contained in:
Maxime Lalo 2023-11-27 10:48:47 +01:00
parent 386ba6c32e
commit 485b63d5ed
6 changed files with 228 additions and 7 deletions

View File

@ -1,4 +1,5 @@
import BaseApiService from "@Front/Api/BaseApiService"; import BaseApiService from "@Front/Api/BaseApiService";
import { ICustomerTokens } from "../Id360/Customers/Customers";
export type IMailVerifyParams = { export type IMailVerifyParams = {
email: string; email: string;
@ -15,6 +16,13 @@ export type IVerifyTotpCodeParams = {
export type IVerifyTotpCodeReturn = { export type IVerifyTotpCodeReturn = {
validCode: boolean; validCode: boolean;
firstConnection: boolean;
};
export type ISetPasswordParams = {
password: string;
email: string;
totpCode: string;
}; };
export default class Auth extends BaseApiService { export default class Auth extends BaseApiService {
@ -45,4 +53,14 @@ export default class Auth extends BaseApiService {
return Promise.reject(err); return Promise.reject(err);
} }
} }
public async setPassword(body: ISetPasswordParams): Promise<ICustomerTokens> {
const url = new URL(this.baseURl.concat("/set-password"));
try {
return this.postRequest<ICustomerTokens>(url, body);
} catch (err) {
this.onError(err);
return Promise.reject(err);
}
}
} }

View File

@ -0,0 +1,29 @@
@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;
.password_indication {
margin-top: 8px;
margin-bottom: 24px;
}
.submit_button {
margin-top: 32px;
}
}
}

View File

@ -0,0 +1,43 @@
import React from "react";
import classes from "./classes.module.scss";
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";
import { ValidationError } from "class-validator";
type IProps = {
onSubmit: (e: React.FormEvent<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
};
export default function StepNewPassword(props: IProps) {
const { onSubmit, validationErrors } = props;
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Configurez votre mot de passe</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Mot de passe"
name="password"
validationError={validationErrors.find((error) => error.property === "password")}
password
/>
<Typography typo={ITypo.CAPTION_14} color={ITypoColor.GREY} className={classes["password_indication"]}>
Au moins 8 caractères dont 1 majuscule, 1 minuscule et 1 chiffre.
</Typography>
<TextField
placeholder="Confirmation du mot de passe"
name="confirm_password"
validationError={validationErrors.find((error) => error.property === "confirm_password")}
password
/>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Valider
</Button>
</Form>
</div>
);
}

View File

@ -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;
}
}
}

View File

@ -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<HTMLFormElement> | null, values: { [key: string]: string }) => void;
validationErrors: ValidationError[];
};
export default function StepPassword(props: IProps) {
const { onSubmit, validationErrors } = props;
return (
<div className={classes["root"]}>
<Typography typo={ITypo.H1}>
<div className={classes["title"]}>Entrez votre mot de passe</div>
</Typography>
<Form className={classes["form"]} onSubmit={onSubmit}>
<TextField
placeholder="Mot de passe"
name="password"
validationError={validationErrors.find((error) => error.property === "password")}
password
/>
<Link href="/forgot-password">
<Typography typo={ITypo.H1}>
<div className={classes["forgot-password"]}>Mot de passe oublié ?</div>
</Typography>
</Link>
<Button type="submit" variant={EButtonVariant.PRIMARY} className={classes["submit_button"]}>
Valider
</Button>
</Form>
</div>
);
}

View File

@ -10,10 +10,15 @@ import StepEmail from "./StepEmail";
import StepTotp from "./StepTotp"; import StepTotp from "./StepTotp";
import Auth from "@Front/Api/Auth/Customer/Auth"; import Auth from "@Front/Api/Auth/Customer/Auth";
import { ValidationError } from "class-validator"; 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 { export enum LoginStep {
EMAIL, EMAIL,
TOTP, TOTP,
PASSWORD,
NEW_PASSWORD, NEW_PASSWORD,
} }
export default function Login() { export default function Login() {
@ -27,6 +32,7 @@ export default function Login() {
const [email, setEmail] = useState<string>(""); const [email, setEmail] = useState<string>("");
const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>(""); const [partialPhoneNumber, setPartialPhoneNumber] = useState<string>("");
const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]); const [validationErrors, setValidationErrors] = useState<ValidationError[]>([]);
const openErrorModal = useCallback(() => { const openErrorModal = useCallback(() => {
setIsErrorModalOpen(true); setIsErrorModalOpen(true);
}, []); }, []);
@ -72,10 +78,14 @@ export default function Login() {
try { try {
if (!values["totpCode"]) return; if (!values["totpCode"]) return;
const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email }); const res = await Auth.getInstance().verifyTotpCode({ totpCode: values["totpCode"], email });
if (res.validCode) {
setTotpCode(values["totpCode"]); // If the code is valid setting it in state
setStep(LoginStep.NEW_PASSWORD); 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) { } catch (error: any) {
setValidationErrors([ setValidationErrors([
{ {
@ -90,6 +100,64 @@ export default function Login() {
}, },
[email, setStep], [email, setStep],
); );
const onNewPasswordSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement> | 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<HTMLFormElement> | 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 ( return (
<DefaultDoubleSidePage title={"Login"} image={LandingImage}> <DefaultDoubleSidePage title={"Login"} image={LandingImage}>
<div className={classes["root"]}> <div className={classes["root"]}>
@ -97,9 +165,8 @@ export default function Login() {
{step === LoginStep.TOTP && ( {step === LoginStep.TOTP && (
<StepTotp onSubmit={onSmsCodeSubmit} validationErrors={validationErrors} partialPhoneNumber={partialPhoneNumber} /> <StepTotp onSubmit={onSmsCodeSubmit} validationErrors={validationErrors} partialPhoneNumber={partialPhoneNumber} />
)} )}
{step === LoginStep.NEW_PASSWORD && ( {step === LoginStep.PASSWORD && <StepPassword onSubmit={onPasswordSubmit} validationErrors={validationErrors} />}
<StepTotp onSubmit={onSmsCodeSubmit} validationErrors={validationErrors} partialPhoneNumber={partialPhoneNumber} /> {step === LoginStep.NEW_PASSWORD && <StepNewPassword onSubmit={onNewPasswordSubmit} validationErrors={validationErrors} />}
)}
</div> </div>
<Confirm <Confirm
isOpen={isErrorModalOpen} isOpen={isErrorModalOpen}