Can request account creation from web app
This commit is contained in:
		@@ -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é !"));
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user