Can reset password
This commit is contained in:
parent
1bd18133b3
commit
e5827656fa
@ -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 },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
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