diff --git a/geneit_app/src/api/AuthApi.ts b/geneit_app/src/api/AuthApi.ts index 40e64e0..586cfca 100644 --- a/geneit_app/src/api/AuthApi.ts +++ b/geneit_app/src/api/AuthApi.ts @@ -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 { + await APIClient.exec({ + uri: "/auth/reset_password", + method: "POST", + jsonData: { token: token, password: newPassword }, + }); + } } diff --git a/geneit_app/src/api/ServerApi.ts b/geneit_app/src/api/ServerApi.ts index 34f62f9..a16fbec 100644 --- a/geneit_app/src/api/ServerApi.ts +++ b/geneit_app/src/api/ServerApi.ts @@ -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; + } } diff --git a/geneit_app/src/routes/auth/OIDCCbRoute.tsx b/geneit_app/src/routes/auth/OIDCCbRoute.tsx index 7313a52..0178df1 100644 --- a/geneit_app/src/routes/auth/OIDCCbRoute.tsx +++ b/geneit_app/src/routes/auth/OIDCCbRoute.tsx @@ -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 ( - <> -

Echec de la finalisation de l'authentification !

- - - - + ); return ( diff --git a/geneit_app/src/routes/auth/ResetPasswordRoute.tsx b/geneit_app/src/routes/auth/ResetPasswordRoute.tsx index 1d48918..c745db0 100644 --- a/geneit_app/src/routes/auth/ResetPasswordRoute.tsx +++ b/geneit_app/src/routes/auth/ResetPasswordRoute.tsx @@ -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(null); @@ -9,7 +19,7 @@ export function ResetPasswordRoute(): React.ReactElement { const { hash } = useLocation(); const token = hash.substring(1); - const info = React.useState(null); + const [info, setInfo] = React.useState(null); const checkedToken = useRef(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 ; + return ; } function ResetPasswordForm(p: { token: string; info: CheckResetTokenResponse; }): React.ReactElement { - return

TODO

; + const [error, setError] = React.useState(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) => { + 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 ( + <> + + + ); + + if (success) + return ( + + ); + + return ( + <> + {error && ( + + {error} + + )} + + + Bonjour {p.info.name}, veuillez définir votre nouveau mot de passe : + + + { + setPassword(n); + }} + /> + + setConfirmPassword(e.target.value)} + label="Confirmer le mot de passe" + type="password" + id="password" + autoComplete="current-password" + /> + + + + + ); } diff --git a/geneit_app/src/widgets/AuthSingleMessage.tsx b/geneit_app/src/widgets/AuthSingleMessage.tsx new file mode 100644 index 0000000..a9ff9bd --- /dev/null +++ b/geneit_app/src/widgets/AuthSingleMessage.tsx @@ -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.message}

+ + + + + ); +} diff --git a/geneit_app/src/widgets/PasswordInput.tsx b/geneit_app/src/widgets/PasswordInput.tsx new file mode 100644 index 0000000..7538219 --- /dev/null +++ b/geneit_app/src/widgets/PasswordInput.tsx @@ -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 + ) => { + event.preventDefault(); + }; + + return ( + + + {p.label} + + ) => { + p.onChange(event.target.value); + }} + endAdornment={ + + + {showPassword ? : } + + + } + /> + + + {error !== null ? error : ""} + + + ); +}