Can request account creation from web app
This commit is contained in:
parent
37015807bb
commit
ae84ae8822
@ -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 />}
|
||||||
|
@ -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
|
||||||
*
|
*
|
||||||
|
@ -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
|
||||||
|
156
geneit_app/src/routes/auth/NewAccountRoute.tsx
Normal file
156
geneit_app/src/routes/auth/NewAccountRoute.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -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é !"));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user