Ajout formulaire d'inscription relié avec bdd users
This commit is contained in:
parent
dd54481152
commit
bdf363726b
0
Back-end/mvnw
vendored
Normal file → Executable file
0
Back-end/mvnw
vendored
Normal file → Executable file
@ -82,6 +82,12 @@
|
|||||||
<version>${junit-jupiter.version}</version>
|
<version>${junit-jupiter.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency> <!--Dependy pour hacher le mdp-->
|
||||||
|
<groupId>at.favre.lib</groupId>
|
||||||
|
<artifactId>bcrypt</artifactId>
|
||||||
|
<version>0.9.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -11,7 +11,7 @@ public class DatabaseService {
|
|||||||
public DatabaseService(Vertx vertx) {
|
public DatabaseService(Vertx vertx) {
|
||||||
pool = JDBCPool.pool(vertx,
|
pool = JDBCPool.pool(vertx,
|
||||||
new JDBCConnectOptions()
|
new JDBCConnectOptions()
|
||||||
.setJdbcUrl("jdbc:postgresql://localhost:5432/postgres?useUnicode=true&amp;characterEncoding=UTF-8") // URL de la base de données
|
.setJdbcUrl("jdbc:postgresql://localhost:5432/users?useUnicode=true&characterEncoding=UTF-8") //Url de la bdd
|
||||||
.setUser("postgres") // Nom d'utilisateur PostgreSQL
|
.setUser("postgres") // Nom d'utilisateur PostgreSQL
|
||||||
.setPassword("admin"), // Mot de passe PostgreSQL
|
.setPassword("admin"), // Mot de passe PostgreSQL
|
||||||
new PoolOptions()
|
new PoolOptions()
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.example.starter;
|
package com.example.starter;
|
||||||
|
|
||||||
|
import io.vertx.ext.web.handler.BodyHandler;
|
||||||
import io.vertx.ext.web.handler.CorsHandler;
|
import io.vertx.ext.web.handler.CorsHandler;
|
||||||
import io.vertx.sqlclient.Row;
|
import io.vertx.sqlclient.Row;
|
||||||
import io.vertx.sqlclient.RowSet;
|
import io.vertx.sqlclient.RowSet;
|
||||||
@ -7,30 +8,40 @@ import io.vertx.sqlclient.Tuple;
|
|||||||
import io.vertx.core.http.HttpMethod;
|
import io.vertx.core.http.HttpMethod;
|
||||||
import io.vertx.core.json.JsonArray;
|
import io.vertx.core.json.JsonArray;
|
||||||
import io.vertx.core.json.JsonObject;
|
import io.vertx.core.json.JsonObject;
|
||||||
|
|
||||||
import io.vertx.core.AbstractVerticle;
|
import io.vertx.core.AbstractVerticle;
|
||||||
import io.vertx.core.Promise;
|
import io.vertx.core.Promise;
|
||||||
import io.vertx.ext.web.Router;
|
import io.vertx.ext.web.Router;
|
||||||
import io.vertx.ext.web.RoutingContext;
|
import io.vertx.ext.web.RoutingContext;
|
||||||
|
import at.favre.lib.crypto.bcrypt.BCrypt;
|
||||||
|
|
||||||
public class MainVerticle extends AbstractVerticle {
|
public class MainVerticle extends AbstractVerticle {
|
||||||
private DatabaseService databaseService;
|
private DatabaseService databaseService;
|
||||||
|
private Router router; // Déclaration du router en variable de classe
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(Promise<Void> startPromise) throws Exception {
|
public void start(Promise<Void> startPromise) throws Exception {
|
||||||
databaseService = new DatabaseService(vertx);
|
databaseService = new DatabaseService(vertx);
|
||||||
// Create a Router
|
|
||||||
Router router = Router.router(vertx);
|
// Initialisation du router
|
||||||
|
router = Router.router(vertx);
|
||||||
|
|
||||||
|
// Activation du BodyHandler pour pouvoir lire les requêtes JSON (important pour POST)
|
||||||
|
router.route().handler(BodyHandler.create());
|
||||||
|
|
||||||
|
// Gestion des CORS
|
||||||
router.route().handler(CorsHandler.create()
|
router.route().handler(CorsHandler.create()
|
||||||
.addOrigin("*") // Allow all origins
|
.addOrigin("*")
|
||||||
.allowedMethod(HttpMethod.GET) // Allow GET requests
|
.allowedMethod(HttpMethod.GET)
|
||||||
.allowedMethod(HttpMethod.POST) // Allow POST requests
|
.allowedMethod(HttpMethod.POST)
|
||||||
.allowedHeader("Content-Type") // Allow Content-Type header
|
.allowedHeader("Content-Type")
|
||||||
.allowedHeader("Authorization"));
|
.allowedHeader("Authorization"));
|
||||||
|
|
||||||
|
// Déclaration des routes
|
||||||
router.get("/objets").handler(this::getObjects);
|
router.get("/objets").handler(this::getObjects);
|
||||||
router.get("/objet").handler(this::getParticularObject);
|
router.get("/objet").handler(this::getParticularObject);
|
||||||
// Create the HTTP server
|
router.post("/signup").handler(this::handleSignup); // Route pour l'inscription
|
||||||
|
|
||||||
|
// Création du serveur HTTP
|
||||||
vertx.createHttpServer()
|
vertx.createHttpServer()
|
||||||
.requestHandler(router)
|
.requestHandler(router)
|
||||||
.listen(8888)
|
.listen(8888)
|
||||||
@ -44,6 +55,7 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupération des objets
|
||||||
private void getObjects(RoutingContext context) {
|
private void getObjects(RoutingContext context) {
|
||||||
databaseService.pool
|
databaseService.pool
|
||||||
.query("SELECT * FROM weather_objects;")
|
.query("SELECT * FROM weather_objects;")
|
||||||
@ -60,6 +72,8 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
.end(getInfosObjects(rows).encode());
|
.end(getInfosObjects(rows).encode());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupération d'un objet spécifique
|
||||||
private void getParticularObject(RoutingContext context) {
|
private void getParticularObject(RoutingContext context) {
|
||||||
String id = context.request().getParam("id");
|
String id = context.request().getParam("id");
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
@ -68,6 +82,7 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
.end(new JsonObject().put("error", "Paramètre 'id' manquant").encode());
|
.end(new JsonObject().put("error", "Paramètre 'id' manquant").encode());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseService.pool
|
databaseService.pool
|
||||||
.preparedQuery("SELECT * FROM weather_objects WHERE id=?")
|
.preparedQuery("SELECT * FROM weather_objects WHERE id=?")
|
||||||
.execute(Tuple.of(Integer.parseInt(id)))
|
.execute(Tuple.of(Integer.parseInt(id)))
|
||||||
@ -85,12 +100,12 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.response()
|
context.response()
|
||||||
.putHeader("content-type","application/json: charset=UTF-8")
|
.putHeader("content-type", "application/json; charset=UTF-8")
|
||||||
.end(getInfosObjects(rows).encode());
|
.end(getInfosObjects(rows).encode());
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convertit les résultats SQL en JSON
|
||||||
private JsonArray getInfosObjects(RowSet<Row> rows) {
|
private JsonArray getInfosObjects(RowSet<Row> rows) {
|
||||||
JsonArray objects = new JsonArray();
|
JsonArray objects = new JsonArray();
|
||||||
for (Row row : rows) {
|
for (Row row : rows) {
|
||||||
@ -104,4 +119,51 @@ public class MainVerticle extends AbstractVerticle {
|
|||||||
}
|
}
|
||||||
return objects;
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleSignup(RoutingContext context) {
|
||||||
|
JsonObject body = context.body().asJsonObject();
|
||||||
|
|
||||||
|
if (body == null) {
|
||||||
|
context.response()
|
||||||
|
.setStatusCode(400)
|
||||||
|
.end(new JsonObject().put("error", "Requête invalide").encode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log pour vérifier le corps de la requête
|
||||||
|
System.out.println("Received body: " + body.encodePrettily());
|
||||||
|
|
||||||
|
String name = body.getString("name");
|
||||||
|
String surname = body.getString("surname");
|
||||||
|
String email = body.getString("email");
|
||||||
|
String gender = body.getString("gender");
|
||||||
|
String password = body.getString("password");
|
||||||
|
|
||||||
|
if (name == null || surname == null || email == null || gender == null || password == null) {
|
||||||
|
context.response()
|
||||||
|
.setStatusCode(400)
|
||||||
|
.end(new JsonObject().put("error", "Tous les champs sont requis").encode());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hashage du mot de passe avec BCrypt
|
||||||
|
String hashedPassword = BCrypt.withDefaults().hashToString(12, password.toCharArray());
|
||||||
|
|
||||||
|
databaseService.pool
|
||||||
|
.preparedQuery("INSERT INTO users (name, surname, email, gender, password) VALUES (?, ?, ?, ?, ?)")
|
||||||
|
.execute(Tuple.of(name, surname, email, gender, hashedPassword))
|
||||||
|
.onSuccess(result -> {
|
||||||
|
context.response()
|
||||||
|
.setStatusCode(201)
|
||||||
|
.end(new JsonObject().put("message", "Utilisateur inscrit avec succès").encode());
|
||||||
|
})
|
||||||
|
.onFailure(err -> {
|
||||||
|
// Log pour afficher l'erreur d'insertion
|
||||||
|
System.err.println("Erreur d'inscription : " + err.getMessage());
|
||||||
|
context.response()
|
||||||
|
.setStatusCode(500)
|
||||||
|
.end(new JsonObject().put("error", "Erreur d'inscription").encode());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||||
import Home from "./pages/Home.jsx";
|
import Home from "./pages/Home.jsx";
|
||||||
import About from "./pages/About.jsx";
|
import About from "./pages/About.jsx";
|
||||||
import Gestion from "./pages/Gestion/Gestion.jsx";
|
import Gestion from "./pages/Gestion/Gestion.jsx";
|
||||||
import Header from "./components/Header.jsx";
|
import Header from "./components/Header.jsx";
|
||||||
import ObjectManagement from "./pages/Gestion/ObjectManagement.jsx";
|
import ObjectManagement from "./pages/Gestion/ObjectManagement.jsx";
|
||||||
import Objet from "./pages/Gestion/Objet.jsx";
|
import Objet from "./pages/Gestion/Objet.jsx";
|
||||||
|
import Signup from './pages/Signup.jsx';
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -17,6 +18,7 @@ function App() {
|
|||||||
<Route path="/gestion" element={<Gestion />} />
|
<Route path="/gestion" element={<Gestion />} />
|
||||||
<Route path="/gestionObjets" element={<ObjectManagement />} />
|
<Route path="/gestionObjets" element={<ObjectManagement />} />
|
||||||
<Route path="/objet" element={<Objet />} />
|
<Route path="/objet" element={<Objet />} />
|
||||||
|
<Route path="/signup" element={<Signup />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { LogIn, UserPlus } from "lucide-react";
|
import { LogIn, UserPlus } from "lucide-react";
|
||||||
|
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
||||||
|
|
||||||
function Header() {
|
function Header() {
|
||||||
return (
|
return (
|
||||||
@ -25,10 +26,10 @@ function Header() {
|
|||||||
<LogIn size={20} />
|
<LogIn size={20} />
|
||||||
<span>Connexion</span>
|
<span>Connexion</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700">
|
<Link to="/signup" className="flex items-center gap-2 bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700">
|
||||||
<UserPlus size={20} />
|
<UserPlus size={20} />
|
||||||
<span>Inscription</span>
|
<span>Inscription</span>
|
||||||
</button>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
function Inscription() {
|
|
||||||
return (
|
|
||||||
<div className="w-96 bg-white rounded-lg shadow-sm p-4 mx-auto text-center mt-8">
|
|
||||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">Inscription</h2>
|
|
||||||
<form className="flex flex-col gap-2">
|
|
||||||
<label className="block">
|
|
||||||
Nom:
|
|
||||||
<input
|
|
||||||
className="rounded-sm border-indigo-200"
|
|
||||||
ype="text"
|
|
||||||
name="name"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label className="block">
|
|
||||||
Prénom:
|
|
||||||
<input type="text" name="surname" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Email:
|
|
||||||
<input type="email" name="email" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Sexe:
|
|
||||||
<label for="homme">Homme</label>
|
|
||||||
<input type="radio" name="sexe" />
|
|
||||||
<label for="femme">Femme</label>
|
|
||||||
<input type="radio" name="sexe" />
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Inscription;
|
|
||||||
199
Front-end/src/pages/Signup.jsx
Normal file
199
Front-end/src/pages/Signup.jsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Mail, User, Lock, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
|
function Signup() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
surname: '',
|
||||||
|
email: '',
|
||||||
|
gender: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
alert("Les mots de passe ne correspondent pas !");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("http://localhost:8888/signup", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || "Erreur lors de l'inscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Inscription réussie !");
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 py-12 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="w-96 bg-white rounded-lg shadow-md p-6 mx-auto">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-800 mb-6 text-center">Inscription</h2>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nom:
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Prénom:
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<User className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="surname"
|
||||||
|
value={formData.surname}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Sexe:
|
||||||
|
</label>
|
||||||
|
<div className="flex gap-6 items-center">
|
||||||
|
<label className="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="gender"
|
||||||
|
value="homme"
|
||||||
|
checked={formData.gender === 'homme'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-radio h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="ml-2">Homme</span>
|
||||||
|
</label>
|
||||||
|
<label className="inline-flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="gender"
|
||||||
|
value="femme"
|
||||||
|
checked={formData.gender === 'femme'}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="form-radio h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300"
|
||||||
|
/>
|
||||||
|
<span className="ml-2">Femme</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email:
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Mail className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Mot de passe:
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Confirmer le mot de passe:
|
||||||
|
</label>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
<Lock className="h-5 w-5 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="confirmPassword"
|
||||||
|
value={formData.confirmPassword}
|
||||||
|
onChange={handleChange}
|
||||||
|
className="pl-10 block w-full rounded-lg border-gray-300 border p-2.5 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
|
||||||
|
required
|
||||||
|
minLength="8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div className="pt-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
|
>
|
||||||
|
S'inscrire
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Signup;
|
||||||
Loading…
Reference in New Issue
Block a user