Can replace user password from profile

This commit is contained in:
Pierre HUBERT 2023-06-14 15:23:23 +02:00
parent e3bec527f0
commit 71db3339d8
3 changed files with 266 additions and 91 deletions

View File

@ -11,6 +11,14 @@ export interface User {
has_password: boolean; has_password: boolean;
} }
export enum ReplacePasswordResponse {
Error,
Success,
InvalidOldPassword,
InvalidNewPassword,
TooManyRequests,
}
export class UserApi { export class UserApi {
/** /**
* Get current user information * Get current user information
@ -36,4 +44,39 @@ export class UserApi {
}, },
}); });
} }
/**
* Replace user password
*/
static async ReplacePassword(
oldPwd: string,
newPwd: string
): Promise<ReplacePasswordResponse> {
const res = await APIClient.exec({
uri: "/user/replace_password",
method: "POST",
jsonData: {
old_password: oldPwd,
new_password: newPwd,
},
allowFail: true,
});
if (res.status >= 200 && res.status < 300)
return ReplacePasswordResponse.Success;
switch (res.status) {
case 400:
return ReplacePasswordResponse.InvalidNewPassword;
case 401:
return ReplacePasswordResponse.InvalidOldPassword;
case 429:
return ReplacePasswordResponse.TooManyRequests;
default:
return ReplacePasswordResponse.Error;
}
}
} }

View File

@ -1,6 +1,6 @@
import React, { useRef } from "react"; import React, { useRef } from "react";
import { AsyncWidget } from "../widgets/AsyncWidget"; import { AsyncWidget } from "../widgets/AsyncWidget";
import { User, UserApi } from "../api/UserApi"; import { ReplacePasswordResponse, User, UserApi } from "../api/UserApi";
import { import {
Alert, Alert,
Box, Box,
@ -9,43 +9,26 @@ import {
CardActions, CardActions,
CardContent, CardContent,
Checkbox, Checkbox,
CircularProgress,
FormControlLabel, FormControlLabel,
TextField, TextField,
Typography, Typography,
} from "@mui/material"; } from "@mui/material";
import { TimeWidget, formatDate } from "../widgets/TimeWidget"; import { TimeWidget, formatDate } from "../widgets/TimeWidget";
import { ServerApi } from "../api/ServerApi"; import { ServerApi } from "../api/ServerApi";
import { PasswordInput } from "../widgets/PasswordInput";
import { normalize } from "path/win32";
export function ProfileRoute(): React.ReactElement { export function ProfileRoute(): React.ReactElement {
const [user, setUser] = React.useState<null | User>(null); const [user, setUser] = React.useState<null | User>(null);
const [newName, setNewName] = React.useState("");
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState<string | null>(null);
const load = async () => { const load = async () => {
const u = await UserApi.GetUserInfo(); const u = await UserApi.GetUserInfo();
setUser(u); setUser(u);
setNewName(u.name);
}; };
const counter = useRef(0); const counter = useRef(0);
const updateProfile = async () => {
try {
setSuccess(null);
setError(null);
await UserApi.UpdateProfile(newName);
counter.current += 1;
setSuccess("Informations du profil mises à jour avec succès !");
} catch (e) {
console.error(e);
setError("Echec de la mise à jour du profil !");
}
};
return ( return (
<AsyncWidget <AsyncWidget
loadKey={counter.current} loadKey={counter.current}
@ -55,76 +38,225 @@ export function ProfileRoute(): React.ReactElement {
<div style={{ maxWidth: "500px", margin: "auto" }}> <div style={{ maxWidth: "500px", margin: "auto" }}>
<Typography variant="h3">Profil</Typography> <Typography variant="h3">Profil</Typography>
{error && <Alert severity="error">{error}</Alert>} <ProfileSettingsCard
{success && <Alert severity="success">{success}</Alert>} user={user!}
onUpdate={() => (counter.current += 1)}
<Card style={{ marginTop: "10px" }}> />
<CardContent> {user?.has_password && <ChangePasswordCard />}
<Typography gutterBottom variant="h5" component="div">
Paramètres du compte
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { m: 1 },
}}
noValidate
autoComplete="off"
>
<TextField
disabled
fullWidth
label="Identifiant"
value={user?.id}
/>
<TextField
disabled
fullWidth
label="Création du compte"
value={formatDate(user!.time_create)}
/>
<TextField
disabled
fullWidth
label="Activation du compte"
value={formatDate(user!.time_activate)}
/>
<TextField
disabled
fullWidth
label="Adresse mail"
value={user?.email}
/>
<TextField
fullWidth
label="Nom d'utilisateur"
value={newName}
onChange={(e) => setNewName(e.target.value)}
inputProps={{
maxLength: ServerApi.Config.constraints.user_name_len.max,
}}
/>
<FormControlLabel
disabled
control={<Checkbox checked={user!.admin} />}
label="Compte administrateur"
/>
</Box>
</CardContent>
<CardActions>
<Button onClick={updateProfile} style={{ marginLeft: "auto" }}>
Enregistrer
</Button>
</CardActions>
</Card>
</div> </div>
)} )}
/> />
); );
} }
function ProfileSettingsCard(p: { user: User; onUpdate: () => {} }) {
const [newName, setNewName] = React.useState(p.user.name);
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState<string | null>(null);
const updateProfile = async () => {
try {
setSuccess(null);
setError(null);
await UserApi.UpdateProfile(newName);
p.onUpdate();
setSuccess("Informations du profil enregistrées avec succès !");
} catch (e) {
console.error(e);
setError("Echec de la mise à jour du profil !");
}
};
return (
<>
<Card style={{ marginTop: "10px" }}>
{error && <Alert severity="error">{error}</Alert>}
{success && <Alert severity="success">{success}</Alert>}
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Paramètres du compte
</Typography>
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
>
<TextField
disabled
fullWidth
label="Identifiant"
value={p.user.id}
/>
<TextField
disabled
fullWidth
label="Création du compte"
value={formatDate(p.user.time_create)}
/>
<TextField
disabled
fullWidth
label="Activation du compte"
value={formatDate(p.user.time_activate)}
/>
<TextField
disabled
fullWidth
label="Adresse mail"
value={p.user.email}
/>
<TextField
fullWidth
label="Nom d'utilisateur"
value={newName}
onChange={(e) => setNewName(e.target.value)}
inputProps={{
maxLength: ServerApi.Config.constraints.user_name_len.max,
}}
/>
<FormControlLabel
disabled
control={<Checkbox checked={p.user.admin} />}
label="Compte administrateur"
/>
</Box>
</CardContent>
<CardActions>
<Button onClick={updateProfile} style={{ marginLeft: "auto" }}>
Enregistrer
</Button>
</CardActions>
</Card>
</>
);
}
function ChangePasswordCard(): React.ReactElement {
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState<string | null>(null);
const [oldPassword, setOldpassword] = React.useState("");
const [newPassword, setNewpassword] = React.useState("");
const [confirmNewPassword, setConfirmNewpassword] = React.useState("");
const isValid =
ServerApi.CheckPassword(newPassword) === null &&
oldPassword.length > 0 &&
confirmNewPassword === newPassword;
const updatePassword = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isValid || loading) return;
setLoading(true);
setSuccess(null);
setError(null);
try {
const result = await UserApi.ReplacePassword(oldPassword, newPassword);
switch (result) {
case ReplacePasswordResponse.Error:
setError("Echec du changement de mot de passe !");
break;
case ReplacePasswordResponse.Success:
setSuccess("Mot de passe changé avec succès !");
break;
case ReplacePasswordResponse.InvalidOldPassword:
setError("Ancien mot de passe saisi invalide !");
break;
case ReplacePasswordResponse.InvalidNewPassword:
setError("Nouveau mot de passe saisi invalide !");
break;
case ReplacePasswordResponse.TooManyRequests:
setError(
"Trop de tentatives de changement de mot de passe, veuillez réessayer ultérieurement !"
);
break;
}
} catch (e) {
console.error(e);
setError("Echec de la mise à jour du mot de passe !");
}
setLoading(false);
};
return (
<>
<Card style={{ marginTop: "10px" }}>
{error && <Alert severity="error">{error}</Alert>}
{success && <Alert severity="success">{success}</Alert>}
<Box
component="form"
sx={{
"& .MuiTextField-root": { my: 1 },
}}
noValidate
autoComplete="off"
onSubmit={updatePassword}
>
<CardContent>
<Typography gutterBottom variant="h5" component="div">
Changement du mot de passe
</Typography>
<TextField
fullWidth
label="Mot de passe actuel"
value={oldPassword}
type="password"
onChange={(e) => setOldpassword(e.target.value)}
/>
<PasswordInput
value={newPassword}
onChange={(n) => setNewpassword(n)}
label={"Nouveau mot de passe"}
/>
<TextField
fullWidth
error={
confirmNewPassword !== "" && confirmNewPassword !== newPassword
}
helperText={
confirmNewPassword !== newPassword
? "Le nouveau mot de passe et sa confirmation doivent être identiques !"
: ""
}
label="Confirmation du nouveau mot de passe"
value={confirmNewPassword}
type="password"
onChange={(e) => setConfirmNewpassword(e.target.value)}
/>
</CardContent>
<CardActions>
<Button
disabled={!isValid && !loading}
type="submit"
style={{ marginLeft: "auto" }}
>
Enregistrer
</Button>
</CardActions>{" "}
</Box>
</Card>
</>
);
}

View File

@ -80,7 +80,7 @@ pub async fn replace_password(
.password_len .password_len
.validate(&q.new_password) .validate(&q.new_password)
{ {
return Ok(HttpResponse::BadRequest().json("Nouveau mot de passe invalide!")); return Ok(HttpResponse::BadRequest().json("Invalid new password!"));
} }
let mut user = users_service::get_by_id(token.user_id).await?; let mut user = users_service::get_by_id(token.user_id).await?;
@ -90,7 +90,7 @@ pub async fn replace_password(
RatedAction::RequestReplacePasswordSignedIn, RatedAction::RequestReplacePasswordSignedIn,
) )
.await?; .await?;
return Ok(HttpResponse::BadRequest().json("Ancien mot de passe invalide !")); return Ok(HttpResponse::Unauthorized().json("Invalid old password!"));
} }
users_service::change_password(&mut user, &q.new_password).await?; users_service::change_password(&mut user, &q.new_password).await?;