Can reset password
This commit is contained in:
		@@ -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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user