Can reset password
This commit is contained in:
parent
1bd18133b3
commit
e5827656fa
@ -1,5 +1,6 @@
|
||||
import { atom } from "jotai";
|
||||
import { APIClient } from "./ApiClient";
|
||||
import { url } from "inspector";
|
||||
|
||||
export interface CheckResetTokenResponse {
|
||||
name: string;
|
||||
@ -92,4 +93,18 @@ export class AuthApi {
|
||||
})
|
||||
).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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -44,4 +44,18 @@ export class ServerApi {
|
||||
if (config === null) throw new Error("Missing configuration!");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { AuthApi } from "../../api/AuthApi";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { AuthSingleMessage } from "../../widgets/AuthSingleMessage";
|
||||
|
||||
/**
|
||||
* OpenID login callback route
|
||||
@ -37,16 +38,11 @@ export function OIDCCbRoute(): React.ReactElement {
|
||||
};
|
||||
|
||||
load();
|
||||
}, [code, state]);
|
||||
});
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<>
|
||||
<p>Echec de la finalisation de l'authentification !</p>
|
||||
<Link to={"/"}>
|
||||
<Button>Retour à l'accueil</Button>
|
||||
</Link>
|
||||
</>
|
||||
<AuthSingleMessage message="Echec de la finalisation de l'authentification !" />
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -1,7 +1,17 @@
|
||||
import { Alert, CircularProgress } from "@mui/material";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
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 {
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
@ -9,7 +19,7 @@ export function ResetPasswordRoute(): React.ReactElement {
|
||||
const { hash } = useLocation();
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -19,7 +29,8 @@ export function ResetPasswordRoute(): React.ReactElement {
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
await AuthApi.CheckResetPasswordToken(token);
|
||||
const info = await AuthApi.CheckResetPasswordToken(token);
|
||||
setInfo(info);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
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: {
|
||||
token: string;
|
||||
info: CheckResetTokenResponse;
|
||||
}): 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
13
geneit_app/src/widgets/AuthSingleMessage.tsx
Normal file
13
geneit_app/src/widgets/AuthSingleMessage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
66
geneit_app/src/widgets/PasswordInput.tsx
Normal file
66
geneit_app/src/widgets/PasswordInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user