Can reset password

This commit is contained in:
Pierre HUBERT 2023-06-12 19:10:31 +02:00
parent 1bd18133b3
commit e5827656fa
6 changed files with 221 additions and 13 deletions

View File

@ -1,5 +1,6 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
import { url } from "inspector";
export interface CheckResetTokenResponse { export interface CheckResetTokenResponse {
name: string; name: string;
@ -92,4 +93,18 @@ export class AuthApi {
}) })
).data; ).data;
} }
/**
* Reset password
*/
static async ResetPassword(
token: string,
newPassword: string
): Promise<void> {
await APIClient.exec({
uri: "/auth/reset_password",
method: "POST",
jsonData: { token: token, password: newPassword },
});
}
} }

View File

@ -44,4 +44,18 @@ export class ServerApi {
if (config === null) throw new Error("Missing configuration!"); if (config === null) throw new Error("Missing configuration!");
return config; return config;
} }
/**
* Check password against policy
*
* @returns The detected error (if any) or null if the password
* is valid
*/
static CheckPassword(pwd: string): string | null {
const constraint = this.Config.constraints.password_len;
if (pwd.length < constraint.min || pwd.length > constraint.max)
return `Le mot de passe doit comporter entre ${constraint.min} et ${constraint.max} caractères`;
return null;
}
} }

View File

@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { AuthApi } from "../../api/AuthApi"; import { AuthApi } from "../../api/AuthApi";
import { useSetAtom } from "jotai"; import { useSetAtom } from "jotai";
import { AuthSingleMessage } from "../../widgets/AuthSingleMessage";
/** /**
* OpenID login callback route * OpenID login callback route
@ -37,16 +38,11 @@ export function OIDCCbRoute(): React.ReactElement {
}; };
load(); load();
}, [code, state]); });
if (error) if (error)
return ( return (
<> <AuthSingleMessage message="Echec de la finalisation de l'authentification !" />
<p>Echec de la finalisation de l'authentification !</p>
<Link to={"/"}>
<Button>Retour à l'accueil</Button>
</Link>
</>
); );
return ( return (

View File

@ -1,7 +1,17 @@
import { Alert, CircularProgress } from "@mui/material"; import {
import React, { useEffect, useRef } from "react"; Alert,
Box,
Button,
CircularProgress,
TextField,
Typography,
} from "@mui/material";
import React, { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom"; import { useLocation } from "react-router-dom";
import { AuthApi, CheckResetTokenResponse } from "../../api/AuthApi"; import { AuthApi, CheckResetTokenResponse } from "../../api/AuthApi";
import { PasswordInput } from "../../widgets/PasswordInput";
import { AuthSingleMessage } from "../../widgets/AuthSingleMessage";
import { ServerApi } from "../../api/ServerApi";
export function ResetPasswordRoute(): React.ReactElement { export function ResetPasswordRoute(): React.ReactElement {
const [error, setError] = React.useState<string | null>(null); const [error, setError] = React.useState<string | null>(null);
@ -9,7 +19,7 @@ export function ResetPasswordRoute(): React.ReactElement {
const { hash } = useLocation(); const { hash } = useLocation();
const token = hash.substring(1); const token = hash.substring(1);
const info = React.useState<null | CheckResetTokenResponse>(null); const [info, setInfo] = React.useState<null | CheckResetTokenResponse>(null);
const checkedToken = useRef<null | string>(null); const checkedToken = useRef<null | string>(null);
useEffect(() => { useEffect(() => {
@ -19,7 +29,8 @@ export function ResetPasswordRoute(): React.ReactElement {
try { try {
setError(null); setError(null);
await AuthApi.CheckResetPasswordToken(token); const info = await AuthApi.CheckResetPasswordToken(token);
setInfo(info);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError( setError(
@ -43,12 +54,105 @@ export function ResetPasswordRoute(): React.ReactElement {
</> </>
); );
return <ResetPasswordForm token={token} info={info as any} />; return <ResetPasswordForm token={token} info={info} />;
} }
function ResetPasswordForm(p: { function ResetPasswordForm(p: {
token: string; token: string;
info: CheckResetTokenResponse; info: CheckResetTokenResponse;
}): React.ReactElement { }): React.ReactElement {
return <p>TODO</p>; const [error, setError] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const [success, setSuccess] = React.useState(false);
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const canSubmit =
ServerApi.CheckPassword(password) === null && password === confirmPassword;
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!canSubmit) return;
setLoading(true);
try {
await AuthApi.ResetPassword(p.token, password);
setSuccess(true);
} catch (e) {
console.error(e);
setError("Echec de la réinitialisation du mot de passe !");
}
setLoading(false);
};
if (loading)
return (
<>
<CircularProgress />
</>
);
if (success)
return (
<AuthSingleMessage message="Mot de passe réinitialisé avec succès. Vous pouvez dès à présent l'utiliser pour accéder à votre compte." />
);
return (
<>
{error && (
<Alert style={{ width: "100%" }} severity="error">
{error}
</Alert>
)}
<Typography
component="h2"
variant="body1"
style={{ marginBottom: "15px" }}
>
Bonjour {p.info.name}, veuillez définir votre nouveau mot de passe :
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
<PasswordInput
label="Nouveau mot de passe"
value={password}
onChange={(n) => {
setPassword(n);
}}
/>
<TextField
margin="normal"
required
fullWidth
error={password !== confirmPassword}
helperText={
password !== confirmPassword
? "Les mots de passe saisis ne correspondent pas !"
: null
}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
label="Confirmer le mot de passe"
type="password"
id="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
disabled={!canSubmit}
>
Valider
</Button>
</Box>
</>
);
} }

View File

@ -0,0 +1,13 @@
import { Button } from "@mui/material";
import { Link } from "react-router-dom";
export function AuthSingleMessage(p: { message: string }): React.ReactElement {
return (
<>
<p style={{ textAlign: "center" }}>{p.message}</p>
<Link to={"/"}>
<Button>Retour à l'accueil</Button>
</Link>
</>
);
}

View File

@ -0,0 +1,66 @@
import {
FormControl,
FormHelperText,
IconButton,
Input,
InputAdornment,
InputLabel,
OutlinedInput,
TextField,
} from "@mui/material";
import { ServerApi } from "../api/ServerApi";
import React from "react";
import { Visibility, VisibilityOff } from "@mui/icons-material";
export function PasswordInput(p: {
value: string;
onChange: (newPassword: string) => void;
label: string;
}) {
const [showPassword, setShowPassword] = React.useState(false);
const error = p.value.length > 0 ? ServerApi.CheckPassword(p.value) : null;
const handleClickShowPassword = () => setShowPassword((show) => !show);
const handleMouseDownPassword = (
event: React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
};
return (
<FormControl fullWidth variant="outlined">
<InputLabel htmlFor="pwdinput" error={error !== null}>
{p.label}
</InputLabel>
<OutlinedInput
id="pwdinput"
required
error={error !== null}
value={p.value}
label={p.label}
type={showPassword ? "text" : "password"}
fullWidth
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
p.onChange(event.target.value);
}}
endAdornment={
<InputAdornment position="end">
<IconButton
aria-label="toggle password visibility"
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
edge="end"
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
</InputAdornment>
}
/>
<FormHelperText error id="pwd-helper-text">
{error !== null ? error : ""}
</FormHelperText>
</FormControl>
);
}