Can request account creation from web app

This commit is contained in:
Pierre HUBERT 2023-06-13 10:06:04 +02:00
parent 37015807bb
commit ae84ae8822
5 changed files with 208 additions and 14 deletions

View File

@ -9,6 +9,7 @@ import { useAtom } from "jotai";
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage"; import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute"; import { PasswordForgottenRoute } from "./routes/auth/PasswordForgottenRoute";
import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute"; import { ResetPasswordRoute } from "./routes/auth/ResetPasswordRoute";
import { NewAccountRoute } from "./routes/auth/NewAccountRoute";
/** /**
* Core app * Core app
@ -24,6 +25,7 @@ function App() {
<Route path="*" element={<BaseLoginPage />}> <Route path="*" element={<BaseLoginPage />}>
<Route path="" element={<LoginRoute />} /> <Route path="" element={<LoginRoute />} />
<Route path="oidc_cb" element={<OIDCCbRoute />} /> <Route path="oidc_cb" element={<OIDCCbRoute />} />
<Route path="new-account" element={<NewAccountRoute />} />
<Route <Route
path="password_forgotten" path="password_forgotten"
element={<PasswordForgottenRoute />} element={<PasswordForgottenRoute />}

View File

@ -1,6 +1,14 @@
import { atom } from "jotai"; import { atom } from "jotai";
import { APIClient } from "./ApiClient"; import { APIClient } from "./ApiClient";
export enum CreateAccountResult {
TooManyRequests,
BadInputData,
MailAlreadyExists,
Success,
Error,
}
export interface CheckResetTokenResponse { export interface CheckResetTokenResponse {
name: string; name: string;
} }
@ -25,6 +33,38 @@ export class AuthApi {
return sessionStorage.getItem(TokenStateKey)!; return sessionStorage.getItem(TokenStateKey)!;
} }
/**
* Create a new account
*/
static async CreateAccount(
name: string,
mail: string
): Promise<CreateAccountResult> {
const res = await APIClient.exec({
uri: "/auth/create_account",
method: "POST",
allowFail: true,
jsonData: {
name: name,
email: mail,
},
});
switch (res.status) {
case 429:
return CreateAccountResult.TooManyRequests;
case 400:
return CreateAccountResult.BadInputData;
case 409:
return CreateAccountResult.MailAlreadyExists;
case 200:
case 201:
return CreateAccountResult.Success;
default:
return CreateAccountResult.Error;
}
}
/** /**
* Start OpenID login * Start OpenID login
* *

View File

@ -100,9 +100,8 @@ export function LoginRoute(): React.ReactElement {
</Grid> </Grid>
<Grid item> <Grid item>
<Typography variant="body2" color={"primary"}> <Typography variant="body2" color={"primary"}>
{" "}
<Link <Link
to="#" to="/new-account"
style={{ color: "inherit", textDecorationColor: "inherit" }} style={{ color: "inherit", textDecorationColor: "inherit" }}
> >
Créer un nouveau compte Créer un nouveau compte

View File

@ -0,0 +1,156 @@
import {
Alert,
Box,
Button,
CircularProgress,
TextField,
Typography,
} from "@mui/material";
import React from "react";
import { ServerApi } from "../../api/ServerApi";
import { Link } from "react-router-dom";
import { AuthSingleMessage } from "../../widgets/AuthSingleMessage";
import { AuthApi, CreateAccountResult } from "../../api/AuthApi";
export function NewAccountRoute(): React.ReactElement {
const [mail, setMail] = React.useState("");
const [name, setName] = React.useState("");
const [showErrors, setShowErrors] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState(false);
const nameLen = ServerApi.Config.constraints.user_name_len;
const mailLen = ServerApi.Config.constraints.mail_len;
const mailValid = mail.length >= mailLen.min && mail.length <= mailLen.max;
const nameValid = name.length >= nameLen.min && name.length <= nameLen.max;
const canSubmit = mailValid && nameValid;
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setShowErrors(true);
if (!canSubmit) return;
setLoading(true);
try {
const res = await AuthApi.CreateAccount(name, mail);
switch (res) {
case CreateAccountResult.Success:
setSuccess(true);
break;
case CreateAccountResult.TooManyRequests:
setError("Trop de tentatives. Veuillez réessayer ultérieurement.");
break;
case CreateAccountResult.MailAlreadyExists:
setError(
"Cette adresse mail est associée à un compte existant. Veuillez essayer de vous connecter ou de réinitialiser votre mot de passe."
);
break;
case CreateAccountResult.BadInputData:
setError("Les données saisies sont invalides !");
break;
case CreateAccountResult.Error:
setError("Une erreur a survenue lors de la création du compte !");
break;
}
} catch (e) {
console.error(e);
setError(
"Une erreur a survenue lors de la tentative de création de compte !"
);
}
setLoading(false);
};
if (loading)
return (
<>
<CircularProgress />
</>
);
if (success)
return (
<AuthSingleMessage
message={
"Votre demande a bien été enregistrée ! Pour terminer la création de votre compte, veuillez consulter votre messagerie et ouvrir le mail qui vous a été envoyé."
}
/>
);
return (
<>
{error && (
<Alert style={{ width: "100%" }} severity="error">
{error}
</Alert>
)}
<Typography component="h2" variant="body1">
Nouveau compte
</Typography>
<Box
component="form"
noValidate
onSubmit={handleSubmit}
sx={{ mt: 1 }}
style={{ width: "100%" }}
>
<TextField
margin="normal"
required
error={showErrors && !nameValid}
fullWidth
value={name}
onChange={(v) => setName(v.target.value)}
id="name"
label="Nom"
autoComplete="name"
autoFocus
inputProps={{
maxLength: nameLen.max,
}}
helperText={`Saisissez votre nom (entre ${nameLen.min} et ${nameLen.max} caractères)`}
/>
<TextField
margin="normal"
required
error={showErrors && !mailValid}
fullWidth
value={mail}
onChange={(v) => setMail(v.target.value)}
label="Adresse mail"
autoComplete="email"
autoFocus
inputProps={{ maxLength: mailLen.max }}
helperText={`Saisissez votre adresse mail (entre ${mailLen.min} et ${mailLen.max} caractères)`}
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Créer le compte
</Button>
</Box>
<Typography variant="body2" color={"primary"}>
<Link
to="/"
style={{ color: "inherit", textDecorationColor: "inherit" }}
>
Retour au formulaire de connexion
</Link>
</Typography>
</>
);
}

View File

@ -19,7 +19,6 @@ pub async fn create_account(remote_ip: RemoteIP, req: web::Json<CreateAccountBod
if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::CreateAccount).await? { if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::CreateAccount).await? {
return Ok(HttpResponse::TooManyRequests().finish()); return Ok(HttpResponse::TooManyRequests().finish());
} }
rate_limiter_service::record_action(remote_ip.0, RatedAction::CreateAccount).await?;
// Check if email is valid // Check if email is valid
if !mailchecker::is_valid(&req.email) { if !mailchecker::is_valid(&req.email) {
@ -33,6 +32,8 @@ pub async fn create_account(remote_ip: RemoteIP, req: web::Json<CreateAccountBod
return Ok(HttpResponse::BadRequest().json("Size constraints were not respected!")); return Ok(HttpResponse::BadRequest().json("Size constraints were not respected!"));
} }
rate_limiter_service::record_action(remote_ip.0, RatedAction::CreateAccount).await?;
// Perform cleanup // Perform cleanup
users_service::delete_not_validated_accounts().await?; users_service::delete_not_validated_accounts().await?;
@ -164,7 +165,7 @@ pub async fn reset_password(remote_ip: RemoteIP, req: web::Json<ResetPasswordBod
.password_len .password_len
.validate(&req.password) .validate(&req.password)
{ {
return Ok(HttpResponse::BadRequest().json("Taille du mot de passe invalide!")); return Ok(HttpResponse::BadRequest().json("Invalid password len!"));
} }
// Validate account, if required // Validate account, if required
@ -198,14 +199,14 @@ pub async fn password_login(remote_ip: RemoteIP, req: web::Json<PasswordLoginQue
log::error!("Auth failed: could not find account by mail! {}", e); log::error!("Auth failed: could not find account by mail! {}", e);
rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin) rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin)
.await?; .await?;
return Ok(HttpResponse::Unauthorized().json("Identifiants incorrects")); return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
} }
}; };
if !user.check_password(&req.password) { if !user.check_password(&req.password) {
log::error!("Auth failed: invalid password for mail {}", user.email); log::error!("Auth failed: invalid password for mail {}", user.email);
rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin).await?; rate_limiter_service::record_action(remote_ip.0, RatedAction::FailedPasswordLogin).await?;
return Ok(HttpResponse::Unauthorized().json("Identifiants incorrects")); return Ok(HttpResponse::Unauthorized().json("Invalid credentials"));
} }
finish_login(&user).await finish_login(&user).await
@ -220,7 +221,7 @@ struct LoginResponse {
async fn finish_login(user: &User) -> HttpResult { async fn finish_login(user: &User) -> HttpResult {
if !user.active { if !user.active {
log::error!("Auth failed: account for mail {} is disabled!", user.email); log::error!("Auth failed: account for mail {} is disabled!", user.email);
return Ok(HttpResponse::ExpectationFailed().json("Ce compte est désactivé !")); return Ok(HttpResponse::ExpectationFailed().json("This account is disabled!"));
} }
Ok(HttpResponse::Ok().json(LoginResponse { Ok(HttpResponse::Ok().json(LoginResponse {
@ -271,16 +272,13 @@ pub async fn finish_openid_login(
if user_info.email_verified != Some(true) { if user_info.email_verified != Some(true) {
log::error!("Email is not verified!"); log::error!("Email is not verified!");
return Ok( return Ok(HttpResponse::Unauthorized().json("Email unverified by IDP!"));
HttpResponse::Unauthorized().json("Email non vérifié par le fournisseur d'identité !")
);
} }
let mail = match user_info.email { let mail = match user_info.email {
Some(m) => m, Some(m) => m,
None => { None => {
return Ok(HttpResponse::Unauthorized() return Ok(HttpResponse::Unauthorized().json("Email not provided by the IDP!"));
.json("Email non spécifié par le fournisseur d'identité !"));
} }
}; };
@ -290,8 +288,7 @@ pub async fn finish_openid_login(
(Some(name), _, _) => name, (Some(name), _, _) => name,
(None, Some(g), Some(f)) => format!("{g} {f}"), (None, Some(g), Some(f)) => format!("{g} {f}"),
(_, _, _) => { (_, _, _) => {
return Ok(HttpResponse::Unauthorized() return Ok(HttpResponse::Unauthorized().json("Name unspecified by the IDP!"));
.json("Nom non spécifié par le fournisseur d'identité !"));
} }
}; };