feat: initial import — ClickerZ formation project (Express + React/Vite)
This commit is contained in:
15
Backend/.env.sample
Executable file
15
Backend/.env.sample
Executable file
@@ -0,0 +1,15 @@
|
||||
# .env.sample - Sample Environment Variables
|
||||
|
||||
# Application Configuration
|
||||
APP_PORT=3310
|
||||
APP_SECRET=YOUR_APP_SECRET_KEY
|
||||
|
||||
# Database Configuration
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=YOUR_DATABASE_USERNAME
|
||||
DB_PASSWORD=YOUR_DATABASE_PASSWORD
|
||||
DB_NAME=YOUR_DATABASE_NAME
|
||||
|
||||
# Frontend URL (for CORS configuration)
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
3
Backend/.gitignore
vendored
Executable file
3
Backend/.gitignore
vendored
Executable file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
coverage
|
||||
.env
|
||||
21
Backend/LICENSE
Executable file
21
Backend/LICENSE
Executable file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Tetardtek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
27
Backend/README.md
Executable file
27
Backend/README.md
Executable file
@@ -0,0 +1,27 @@
|
||||
# B-TetaRdPG
|
||||
## Description
|
||||
Here is the BACKEND part of TetaRdPG project.
|
||||
It's an basic RPG game.
|
||||
You can make fight againest monster ... and more !
|
||||
|
||||
## Installation & Start
|
||||
(Don't forget to use F-TetaRdPG project with)
|
||||
1. git clone https://github.com/Tetardtek/B-TetaRdPG.git
|
||||
2. cd B-TetaRdPG
|
||||
3. Create your own .env file (use env.sample for example)
|
||||
4. npm i
|
||||
5. You have to use npm run db:migrate && npm run db:seed (to create you own DB project)
|
||||
6. npm run dev
|
||||
|
||||
## License
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## Technology
|
||||
- [Node.js](https://nodejs.org/) - A JavaScript runtime built on Chrome's V8 JavaScript engine.
|
||||
- [Express](https://expressjs.com/) - A fast, unopinionated, minimalist web framework for Node.js.
|
||||
- [MySQL](https://www.mysql.com/) - An open-source relational database management system.
|
||||
- [Jsonwebtoken](https://jwt.io/introduction) - JSON Web Token implementation for Node.js.
|
||||
|
||||
|
||||
## Contact
|
||||
[](https://discord.com/users/235413280103858176)
|
||||
36
Backend/database/client.js
Executable file
36
Backend/database/client.js
Executable file
@@ -0,0 +1,36 @@
|
||||
// Get variables from .env file for database connection
|
||||
const { DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME } = process.env;
|
||||
|
||||
// Create a connection pool to the database
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
const client = mysql.createPool({
|
||||
host: DB_HOST,
|
||||
port: DB_PORT,
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
database: DB_NAME,
|
||||
});
|
||||
|
||||
// Try to get a connection to the database
|
||||
client
|
||||
.getConnection()
|
||||
.then((connection) => {
|
||||
console.info(`Using database ${DB_NAME}`);
|
||||
|
||||
connection.release();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
"Warning:",
|
||||
"Failed to establish a database connection.",
|
||||
"Please check your database credentials in the .env file if you need a database access."
|
||||
);
|
||||
console.error("Error message:", error.message);
|
||||
});
|
||||
|
||||
// Store database name into client for further uses
|
||||
client.databaseName = DB_NAME;
|
||||
|
||||
// Ready to export
|
||||
module.exports = client;
|
||||
9
Backend/database/schema.sql
Executable file
9
Backend/database/schema.sql
Executable file
@@ -0,0 +1,9 @@
|
||||
DROP TABLE IF EXISTS users;
|
||||
|
||||
CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nickname VARCHAR(30) NOT NULL,
|
||||
mail VARCHAR(90) NOT NULL,
|
||||
password VARCHAR(200) NOT NULL,
|
||||
tetardcoin INT default 0
|
||||
);
|
||||
17
Backend/index.js
Executable file
17
Backend/index.js
Executable file
@@ -0,0 +1,17 @@
|
||||
// Load environment variables from .env file
|
||||
require("dotenv").config();
|
||||
|
||||
// Import the Express application from src/app.js
|
||||
const app = require("./src/app");
|
||||
|
||||
// Get the port from the environment variables
|
||||
const port = process.env.APP_PORT;
|
||||
|
||||
// Start the server and listen on the specified port
|
||||
app
|
||||
.listen(port, () => {
|
||||
console.info(`Server is listening on port ${port}`);
|
||||
})
|
||||
.on("error", (err) => {
|
||||
console.error("Error:", err.message);
|
||||
});
|
||||
52
Backend/migrate.js
Executable file
52
Backend/migrate.js
Executable file
@@ -0,0 +1,52 @@
|
||||
// Load environment variables from .env file
|
||||
require("dotenv").config();
|
||||
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
// Build the path to the schema SQL file
|
||||
const schema = path.join(__dirname, "database", "schema.sql");
|
||||
|
||||
// Get database connection details from .env file
|
||||
const { DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME } = process.env;
|
||||
|
||||
// Update the database schema
|
||||
const mysql = require("mysql2/promise");
|
||||
|
||||
const migrate = async () => {
|
||||
try {
|
||||
// Read the SQL statements from the schema file
|
||||
const sql = fs.readFileSync(schema, "utf8");
|
||||
|
||||
// Create a specific connection to the database
|
||||
const database = await mysql.createConnection({
|
||||
host: DB_HOST,
|
||||
port: DB_PORT,
|
||||
user: DB_USER,
|
||||
password: DB_PASSWORD,
|
||||
multipleStatements: true, // Allow multiple SQL statements
|
||||
});
|
||||
|
||||
// Drop the existing database if it exists
|
||||
await database.query(`drop database if exists ${DB_NAME}`);
|
||||
|
||||
// Create a new database with the specified name
|
||||
await database.query(`create database ${DB_NAME}`);
|
||||
|
||||
// Switch to the newly created database
|
||||
await database.query(`use ${DB_NAME}`);
|
||||
|
||||
// Execute the SQL statements to update the database schema
|
||||
await database.query(sql);
|
||||
|
||||
// Close the database connection
|
||||
database.end();
|
||||
|
||||
console.info(`${DB_NAME} updated from ${schema} 🆙`);
|
||||
} catch (err) {
|
||||
console.error("Error updating the database:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
// Run the migration function
|
||||
migrate();
|
||||
1839
Backend/package-lock.json
generated
Executable file
1839
Backend/package-lock.json
generated
Executable file
File diff suppressed because it is too large
Load Diff
22
Backend/package.json
Executable file
22
Backend/package.json
Executable file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "nodemon index.js",
|
||||
"db:migrate": "node migrate.js",
|
||||
"db:seed": "node seed.js",
|
||||
"build": "node migrate.js",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.1",
|
||||
"express": "^4.18.2",
|
||||
"joi": "^17.13.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.9.1",
|
||||
"nodemailer": "^6.9.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3"
|
||||
}
|
||||
}
|
||||
BIN
Backend/public/assets/images/favicon.png
Executable file
BIN
Backend/public/assets/images/favicon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
33
Backend/seed.js
Executable file
33
Backend/seed.js
Executable file
@@ -0,0 +1,33 @@
|
||||
// eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}]
|
||||
|
||||
// Load environment variables from .env file
|
||||
require("dotenv").config();
|
||||
|
||||
// Import database client
|
||||
const database = require("./database/client");
|
||||
|
||||
const insertUsers = async () => {
|
||||
return database.query(`
|
||||
INSERT INTO users (nickname, mail, password, tetardcoin) VALUES
|
||||
('Tetardtek', 'kvnn64@gmail.com', '$2b$10$4VWdZ7SANvRr7qn3k6LAEu6eGApGQUvPOqcCCmgzVLKNlSpBL0rGa', 1000)
|
||||
`);
|
||||
};
|
||||
|
||||
const seed = async () => {
|
||||
try {
|
||||
await database.query("START TRANSACTION");
|
||||
|
||||
await insertUsers();
|
||||
|
||||
await database.query("COMMIT");
|
||||
|
||||
database.end();
|
||||
|
||||
console.info(`${database.databaseName} filled from ${__filename} 🌱`);
|
||||
} catch (err) {
|
||||
await database.query("ROLLBACK");
|
||||
console.error("Error filling the database:", err.message);
|
||||
}
|
||||
};
|
||||
|
||||
seed();
|
||||
152
Backend/src/app.js
Executable file
152
Backend/src/app.js
Executable file
@@ -0,0 +1,152 @@
|
||||
// Load the express module to create a web application
|
||||
|
||||
const express = require("express");
|
||||
|
||||
const app = express();
|
||||
|
||||
// Configure it
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// CORS Handling: Why is the current code commented out and do I need to define specific allowed origins for my project?
|
||||
|
||||
// CORS (Cross-Origin Resource Sharing) is a security mechanism in web browsers that blocks requests from a different domain than the server.
|
||||
// You may find the following magic line in forums:
|
||||
|
||||
// app.use(cors());
|
||||
|
||||
// You should NOT do that: such code uses the `cors` module to allow all origins, which can pose security issues.
|
||||
// For this pedagogical template, the CORS code is commented out to show the need for defining specific allowed origins.
|
||||
|
||||
// To enable CORS and define allowed origins:
|
||||
// 1. Install the `cors` module in the backend directory
|
||||
// 2. Uncomment the line `const cors = require("cors");`
|
||||
// 3. Uncomment the section `app.use(cors({ origin: [...] }))`
|
||||
// 4. Be sure to only have URLs in the array with domains from which you want to allow requests.
|
||||
// For example: ["http://mysite.com", "http://another-domain.com"]
|
||||
|
||||
const cors = require("cors");
|
||||
|
||||
app.use(
|
||||
cors({
|
||||
origin: [
|
||||
process.env.FRONTEND_URL, // keep this one, after checking the value in `backend/.env`
|
||||
"http://mysite.com",
|
||||
"http://another-domain.com",
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Request Parsing: Understanding the purpose of this part
|
||||
|
||||
// Request parsing is necessary to extract data sent by the client in an HTTP request.
|
||||
// For example to access the body of a POST request.
|
||||
// The current code contains different parsing options as comments to demonstrate different ways of extracting data.
|
||||
|
||||
// 1. `express.json()`: Parses requests with JSON data.
|
||||
// 2. `express.urlencoded()`: Parses requests with URL-encoded data.
|
||||
// 3. `express.text()`: Parses requests with raw text data.
|
||||
// 4. `express.raw()`: Parses requests with raw binary data.
|
||||
|
||||
// Uncomment one or more of these options depending on the format of the data sent by your client:
|
||||
|
||||
app.use(express.json());
|
||||
// app.use(express.urlencoded());
|
||||
// app.use(express.text());
|
||||
// app.use(express.raw());
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Cookies: Why and how to use the `cookie-parser` module?
|
||||
|
||||
// Cookies are small pieces of data stored in the client's browser. They are often used to store user-specific information or session data.
|
||||
|
||||
// The `cookie-parser` module allows us to parse and manage cookies in our Express application. It parses the `Cookie` header in incoming requests and populates `req.cookies` with an object containing the cookies.
|
||||
|
||||
// To use `cookie-parser`, make sure it is installed in `backend/package.json` (you may need to install it separately):
|
||||
// npm install cookie-parser
|
||||
|
||||
// Then, require the module and use it as middleware in your Express application:
|
||||
|
||||
// const cookieParser = require("cookie-parser");
|
||||
// app.use(cookieParser());
|
||||
|
||||
// Once `cookie-parser` is set up, you can read and set cookies in your routes.
|
||||
// For example, to set a cookie named "username" with the value "john":
|
||||
// res.cookie("username", "john");
|
||||
|
||||
// To read the value of a cookie named "username":
|
||||
// const username = req.cookies.username;
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Import the API routes from the router module
|
||||
const path = require("path");
|
||||
const router = require("./router");
|
||||
|
||||
// Mount the API routes under the "/api" endpoint
|
||||
app.use("/api", router);
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Production-ready setup: What is it for, and when should I enable it?
|
||||
|
||||
// The code includes commented sections to set up a production environment where the frontend and backend are served from the same server.
|
||||
|
||||
// What it's for:
|
||||
// - Serving frontend static files from the backend, which is useful when building a single-page application with React, Angular, etc.
|
||||
// - Redirecting unhandled requests (e.g., all requests not matching a defined API route) to the frontend's index.html. This allows the frontend to handle client-side routing.
|
||||
|
||||
// When to enable it:
|
||||
// It depends on your project and its structure. If you are developing a single-page application, you'll enable these sections when you are ready to deploy your project to production.
|
||||
|
||||
// To enable production configuration:
|
||||
// 1. Uncomment the lines related to serving static files and redirecting unhandled requests.
|
||||
// 2. Ensure that the `reactBuildPath` points to the correct directory where your frontend's build artifacts are located.
|
||||
|
||||
// const reactBuildPath = `${__dirname}/../../frontend/dist`;
|
||||
|
||||
// // Serve react resources
|
||||
|
||||
// app.use(express.static(reactBuildPath));
|
||||
|
||||
// // Redirect unhandled requests to the react index file
|
||||
|
||||
// app.get("*", (req, res) => {
|
||||
// res.sendFile(`${reactBuildPath}/index.html`);
|
||||
// });
|
||||
|
||||
app.use("*", (req, res) => {
|
||||
if (req.originalUrl.includes("assets")) {
|
||||
res.sendFile(
|
||||
path.resolve(__dirname, `../../frontend/dist/${req.originalUrl}`)
|
||||
);
|
||||
} else {
|
||||
res.sendFile(path.resolve(__dirname, `../../frontend/dist/index.html`));
|
||||
}
|
||||
});
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Middleware for Error Logging (Uncomment to enable)
|
||||
// Important: Error-handling middleware should be defined last, after other app.use() and routes calls.
|
||||
|
||||
/*
|
||||
// Define a middleware function to log errors
|
||||
const logErrors = (err, req, res, next) => {
|
||||
// Log the error to the console for debugging purposes
|
||||
console.error(err);
|
||||
console.error("on req:", req.method, req.path);
|
||||
|
||||
// Pass the error to the next middleware in the stack
|
||||
next(err);
|
||||
};
|
||||
|
||||
// Mount the logErrors middleware globally
|
||||
app.use(logErrors);
|
||||
*/
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
module.exports = app;
|
||||
421
Backend/src/controllers/userControllers.js
Executable file
421
Backend/src/controllers/userControllers.js
Executable file
@@ -0,0 +1,421 @@
|
||||
require("dotenv").config();
|
||||
|
||||
const bcrypt = require("bcrypt");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const Joi = require("joi");
|
||||
const nodemailer = require("nodemailer");
|
||||
const tables = require("../tables");
|
||||
|
||||
const secretKey = process.env.APP_SECRET;
|
||||
const saltRounds = 10;
|
||||
|
||||
const passwordSchema = Joi.string()
|
||||
.min(8)
|
||||
.regex(/[A-Z]/)
|
||||
.message("Le mot de passe doit contenir au moins une majuscule.")
|
||||
.regex(/\d/)
|
||||
.message("Le mot de passe doit contenir au moins un chiffre.")
|
||||
.regex(/[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/)
|
||||
.message("Le mot de passe doit contenir au moins un caractère spécial.");
|
||||
|
||||
const nicknameSchema = Joi.string()
|
||||
.alphanum()
|
||||
.min(3)
|
||||
.message("Le pseudo doit contenir au moins 3 caractères alphanumériques.");
|
||||
|
||||
const emailSchema = Joi.string().email().message("Format d'e-mail invalide.");
|
||||
|
||||
const validatePasswordComplexity = async (password) => {
|
||||
await passwordSchema.validateAsync(password);
|
||||
};
|
||||
|
||||
const validateEmail = async (email) => {
|
||||
await emailSchema.validateAsync(email);
|
||||
};
|
||||
|
||||
const validateUniqueNickname = async (nickname) => {
|
||||
await nicknameSchema.validateAsync(nickname);
|
||||
const existingUserByNickname = await tables.users.getByNickname(
|
||||
nickname
|
||||
);
|
||||
|
||||
if (existingUserByNickname) {
|
||||
throw new Error("Ce pseudo est déjà pris.");
|
||||
}
|
||||
};
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: "gmail",
|
||||
auth: {
|
||||
user: process.env.EMAIL_USER,
|
||||
pass: process.env.EMAIL_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
const generateResetToken = (user) => {
|
||||
const resetToken = jwt.sign({ user: user.id }, resetTokenSecret, {
|
||||
expiresIn: "1h",
|
||||
});
|
||||
|
||||
const base64Token = Buffer.from(resetToken).toString("base64");
|
||||
|
||||
return base64Token;
|
||||
};
|
||||
|
||||
const sendPasswordResetEmail = async (user, resetToken) => {
|
||||
const resetLink = `${process.env.FRONTEND_URL}/reset-password/${resetToken}`;
|
||||
const mailOptions = {
|
||||
from: "INSERT_YOUR_EMAIL_ADDRESS_HERE",
|
||||
to: user.mail,
|
||||
subject: "Réinitialisation de mot de passe",
|
||||
html: `
|
||||
<p>Bonjour ${user.firstname},</p>
|
||||
<p>Vous avez demandé une réinitialisation de mot de passe pour votre compte.</p>
|
||||
<p>Cliquez sur le lien ci-dessous pour réinitialiser votre mot de passe :</p>
|
||||
<a href="${resetLink}">Réinitialiser le mot de passe</a>
|
||||
<p>Si vous n'avez pas demandé cette réinitialisation, veuillez ignorer ce message.</p>
|
||||
<p>Merci,</p>
|
||||
`,
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
};
|
||||
|
||||
const forgottenPassword = async (req, res) => {
|
||||
const { mail } = req.body;
|
||||
|
||||
try {
|
||||
const user = await tables.users.getByMail(mail);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
||||
}
|
||||
|
||||
const resetToken = generateResetToken(user);
|
||||
|
||||
await sendPasswordResetEmail(user, resetToken);
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Envoi d'un e-mail de réinitialisation du mot de passe",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"Erreur dans l'envoi de l'e-mail de réinitialisation du mot de passe:",
|
||||
error
|
||||
);
|
||||
return res.status(500).json({
|
||||
message:
|
||||
"Erreur dans l'envoi de l'e-mail de réinitialisation du mot de passe",
|
||||
});
|
||||
}
|
||||
};
|
||||
const resetPassword = async (req, res) => {
|
||||
const { password } = req.body;
|
||||
|
||||
const resetToken = decodeURIComponent(req.params.token);
|
||||
|
||||
try {
|
||||
const decodedToken = jwt.verify(resetToken, resetTokenSecret);
|
||||
|
||||
const user = await tables.users.read(decodedToken.user);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return res.status(400).json({ message: "Nouveau mot de passe manquant" });
|
||||
}
|
||||
|
||||
const { error } = Joi.string()
|
||||
.min(8)
|
||||
.regex(/[A-Z]/)
|
||||
.message("Le mot de passe doit contenir au moins une majuscule.")
|
||||
.regex(/\d/)
|
||||
.message("Le mot de passe doit contenir au moins un chiffre.")
|
||||
.regex(/[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/)
|
||||
.message("Le mot de passe doit contenir au moins un caractère spécial.")
|
||||
.validate(password);
|
||||
|
||||
if (error) {
|
||||
return res.status(400).json({ message: error.details[0].message });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
await tables.users.edit(user.id, { password: hashedPassword });
|
||||
|
||||
return res
|
||||
.status(200)
|
||||
.json({ message: "Réinitialisation du mot de passe réussie" });
|
||||
} catch (error) {
|
||||
console.error("Erreur de réinitialisation du mot de passe:", error);
|
||||
return res.status(500).json({
|
||||
message: "Erreur de réinitialisation du mot de passe",
|
||||
error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (req, res) => {
|
||||
const { mail, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = await tables.users.getByMail(mail);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: "Adresse email introuvable" });
|
||||
}
|
||||
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!passwordMatch) {
|
||||
return res.status(401).json({ message: "Mot de passe incorrect" });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ user: user.id }, secretKey);
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Connexion réussie",
|
||||
user: { ...user, credits: user.credits },
|
||||
token,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la connexion :", error);
|
||||
return res.status(500).json({ message: "Erreur interne du serveur" });
|
||||
}
|
||||
};
|
||||
|
||||
const browse = async (req, res, next) => {
|
||||
try {
|
||||
const users = await tables.users.readAll();
|
||||
res.json(users);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
const read = async (req, res, next) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { field } = req.query;
|
||||
|
||||
const user = await tables.users.read(id);
|
||||
|
||||
if (field && user && user[field]) {
|
||||
res.json({ [field]: user[field] });
|
||||
} else if (user) {
|
||||
res.json(user);
|
||||
} else {
|
||||
res.sendStatus(404);
|
||||
}
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
const edit = async (req, res) => {
|
||||
const userId = req.params.id;
|
||||
|
||||
try {
|
||||
if (!req.body) {
|
||||
return res.status(400).json({ message: "Corps de la requête vide" });
|
||||
}
|
||||
|
||||
const {
|
||||
nickname,
|
||||
mail,
|
||||
newPassword,
|
||||
tetardcoin,
|
||||
currentPassword,
|
||||
} = req.body;
|
||||
|
||||
const user = await tables.users.read(userId);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: "Utilisateur non trouvé" });
|
||||
}
|
||||
|
||||
const isCurrentPasswordCorrect = await bcrypt.compare(
|
||||
currentPassword,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
return res.status(401).json({ message: "Mot de passe actuel incorrect" });
|
||||
}
|
||||
|
||||
const errors = {};
|
||||
|
||||
if (nickname !== undefined) {
|
||||
if (nickname.trim() === "") {
|
||||
errors.nickname = "Le pseudo ne peut pas être vide";
|
||||
} else if (nickname.trim() !== user.nickname.trim()) {
|
||||
try {
|
||||
await validateUniqueNickname(nickname);
|
||||
} catch (error) {
|
||||
errors.nickname = error.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mail !== undefined && mail.trim() === "") {
|
||||
errors.mail = "L'e-mail ne peut pas être vide";
|
||||
}
|
||||
|
||||
if (newPassword !== undefined && newPassword.trim() !== "") {
|
||||
try {
|
||||
await validatePasswordComplexity(newPassword.trim());
|
||||
} catch (error) {
|
||||
errors.newPassword = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return res.status(400).json({ errors });
|
||||
}
|
||||
|
||||
const updatedFields = {};
|
||||
|
||||
if (nickname !== undefined) {
|
||||
updatedFields.nickname = nickname;
|
||||
}
|
||||
|
||||
if (mail !== undefined) {
|
||||
updatedFields.mail = mail;
|
||||
}
|
||||
|
||||
if (newPassword !== undefined && newPassword.trim() !== "") {
|
||||
updatedFields.password = await bcrypt.hash(
|
||||
newPassword.trim(),
|
||||
saltRounds
|
||||
);
|
||||
}
|
||||
|
||||
const affectedRows = await tables.users.edit(userId, updatedFields);
|
||||
|
||||
if (affectedRows === 0) {
|
||||
return res.status(500).json({ message: "Échec de la mise à jour" });
|
||||
}
|
||||
|
||||
const editedUser = await tables.users.read(userId);
|
||||
|
||||
return res.json({ message: "Mis à jour", user: editedUser });
|
||||
} catch (error) {
|
||||
return res.status(500).json({
|
||||
message: "Erreur lors de la mise à jour de l'utilisateur",
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const add = async (req, res, next) => {
|
||||
try {
|
||||
const { nickname, mail, password, confirmPassword } =
|
||||
req.body;
|
||||
|
||||
const existingUserByMail = await tables.users.getByMail(mail);
|
||||
if (existingUserByMail) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "Cette adresse e-mail est déjà enregistrée." });
|
||||
}
|
||||
|
||||
const existingUserByNickname = await tables.users.getByNickname(
|
||||
nickname
|
||||
);
|
||||
if (existingUserByNickname) {
|
||||
return res.status(400).json({ message: "Ce pseudo est déjà pris." });
|
||||
}
|
||||
|
||||
await validateEmail(mail);
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
return res
|
||||
.status(400)
|
||||
.json({ message: "Les mots de passe ne correspondent pas." });
|
||||
}
|
||||
|
||||
try {
|
||||
await passwordSchema.validateAsync(password);
|
||||
} catch (validationError) {
|
||||
return res.status(400).json({ message: validationError.message });
|
||||
}
|
||||
|
||||
const hashedPassword = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
const user = {
|
||||
firstname,
|
||||
lastname,
|
||||
nickname,
|
||||
mail,
|
||||
password: hashedPassword,
|
||||
tetardcoin: 1000,
|
||||
};
|
||||
|
||||
const insertId = await tables.users.create(user);
|
||||
|
||||
const token = jwt.sign({ user: user.id }, secretKey);
|
||||
|
||||
res.status(201).json({ insertId, token });
|
||||
return insertId;
|
||||
} catch (err) {
|
||||
console.error("Erreur lors de l'enregistrement de l'utilisateur :", err);
|
||||
return next(err);
|
||||
}
|
||||
};
|
||||
|
||||
const destroy = async (req, res) => {
|
||||
try {
|
||||
const userId = req.params.id;
|
||||
const { currentPassword } = req.body;
|
||||
|
||||
if (!currentPassword) {
|
||||
return res.status(400).json({
|
||||
errors: {
|
||||
currentPassword:
|
||||
"Le mot de passe actuel est requis pour la suppression du compte.",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await tables.users.getById(userId);
|
||||
|
||||
const isCurrentPasswordCorrect = await bcrypt.compare(
|
||||
currentPassword,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isCurrentPasswordCorrect) {
|
||||
return res.status(401).json({
|
||||
errors: {
|
||||
currentPassword: "Le mot de passe actuel est incorrect.",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await tables.users.delete(userId);
|
||||
|
||||
res.sendStatus(204);
|
||||
} catch (err) {
|
||||
console.error("Erreur lors de la suppression du compte :", err);
|
||||
|
||||
res.status(500).json({
|
||||
errors: {
|
||||
general: "Une erreur s'est produite lors de la suppression du compte.",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
|
||||
module.exports = {
|
||||
browse,
|
||||
read,
|
||||
edit,
|
||||
add,
|
||||
destroy,
|
||||
login,
|
||||
forgottenPassword,
|
||||
resetPassword,
|
||||
};
|
||||
24
Backend/src/middlewares/verifyToken.js
Executable file
24
Backend/src/middlewares/verifyToken.js
Executable file
@@ -0,0 +1,24 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
|
||||
const secretKey = process.env.APP_SECRET;
|
||||
|
||||
const verifyToken = (req, res, next) => {
|
||||
const token = req.header("x-auth-token");
|
||||
|
||||
if (!token) {
|
||||
return res
|
||||
.status(401)
|
||||
.json({ message: "Access denied. No token provided." });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, secretKey);
|
||||
req.user = decoded.user;
|
||||
next();
|
||||
return null;
|
||||
} catch (error) {
|
||||
return res.status(401).json({ message: "Invalid token." });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = verifyToken;
|
||||
16
Backend/src/models/AbstractManager.js
Executable file
16
Backend/src/models/AbstractManager.js
Executable file
@@ -0,0 +1,16 @@
|
||||
// Import database client
|
||||
const database = require("../../database/client");
|
||||
|
||||
// Provide database access through AbstractManager class
|
||||
class AbstractManager {
|
||||
constructor({ table }) {
|
||||
// Store the table name
|
||||
this.table = table;
|
||||
|
||||
// Provide access to the database client
|
||||
this.database = database;
|
||||
}
|
||||
}
|
||||
|
||||
// Ready to export
|
||||
module.exports = AbstractManager;
|
||||
101
Backend/src/models/UserManager.js
Executable file
101
Backend/src/models/UserManager.js
Executable file
@@ -0,0 +1,101 @@
|
||||
const AbstractManager = require("./AbstractManager");
|
||||
|
||||
class UserManager extends AbstractManager {
|
||||
constructor() {
|
||||
super({ table: "users" });
|
||||
}
|
||||
|
||||
// The C of CRUD - Create operation
|
||||
async create(user) {
|
||||
const { nickname, mail, tetardcoin, password } = user;
|
||||
|
||||
const [result] = await this.database.query(
|
||||
`INSERT INTO ${this.table} (nickname, mail, tetardcoin, password) VALUES (?, ?, ?, ?)`,
|
||||
[nickname, mail, tetardcoin, password]
|
||||
);
|
||||
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// The Rs of CRUD - Read operations
|
||||
async read(id, field) {
|
||||
if (field) {
|
||||
const [rows] = await this.database.query(
|
||||
`SELECT ?? FROM ${this.table} WHERE id = ?`,
|
||||
[field, id]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rows[0][field];
|
||||
}
|
||||
|
||||
const [rows] = await this.database.query(
|
||||
`SELECT * FROM ${this.table} WHERE id = ?`,
|
||||
[id]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async getByMail(mail) {
|
||||
const [rows] = await this.database.query(
|
||||
`SELECT * FROM ${this.table} WHERE mail = ?`,
|
||||
[mail]
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async readAll() {
|
||||
const [rows] = await this.database.query(`SELECT * FROM ${this.table}`);
|
||||
return rows;
|
||||
}
|
||||
|
||||
// The U of CRUD - Update operation
|
||||
async edit(id, updatedFields) {
|
||||
const allowedFields = [
|
||||
"mail",
|
||||
"nickname",
|
||||
"tetardcoin",
|
||||
"password",
|
||||
];
|
||||
|
||||
const fieldsToUpdate = Object.keys(updatedFields).filter((field) =>
|
||||
allowedFields.includes(field)
|
||||
);
|
||||
|
||||
const updateValues = fieldsToUpdate.map((field) => updatedFields[field]);
|
||||
|
||||
if (fieldsToUpdate.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const updateQuery = `UPDATE ${this.table} SET ${fieldsToUpdate
|
||||
.map((field) => `${field} = ?`)
|
||||
.join(", ")} WHERE id = ?`;
|
||||
|
||||
updateValues.push(id);
|
||||
|
||||
const [result] = await this.database.query(updateQuery, updateValues);
|
||||
|
||||
return result.affectedRows;
|
||||
}
|
||||
|
||||
// The D of CRUD - Delete operation
|
||||
async delete(id) {
|
||||
await this.database.query(`DELETE FROM ${this.table} WHERE id = ?`, [id]);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = UserManager;
|
||||
26
Backend/src/router.js
Executable file
26
Backend/src/router.js
Executable file
@@ -0,0 +1,26 @@
|
||||
const express = require("express");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/* ************************************************************************* */
|
||||
// Define Your API Routes Here
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Import Controllers
|
||||
const userControllers = require("./controllers/userControllers");
|
||||
const verifyToken = require("./middlewares/verifyToken");
|
||||
|
||||
|
||||
// User management
|
||||
router.get("/users", verifyToken, userControllers.browse);
|
||||
router.get("/users/:id", userControllers.read);
|
||||
router.get("/users/:id/field", userControllers.read);
|
||||
router.put("/users/:id", userControllers.edit);
|
||||
router.post("/users", userControllers.add);
|
||||
router.delete("/users/:id", userControllers.destroy);
|
||||
router.post("/login", userControllers.login);
|
||||
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
module.exports = router;
|
||||
0
Backend/src/services/.gitkeep
Executable file
0
Backend/src/services/.gitkeep
Executable file
38
Backend/src/tables.js
Executable file
38
Backend/src/tables.js
Executable file
@@ -0,0 +1,38 @@
|
||||
/* ************************************************************************* */
|
||||
// Register Data Managers for Tables
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Import the manager modules responsible for handling data operations on the tables
|
||||
const UserManager = require("./models/UserManager");
|
||||
|
||||
const managers = [
|
||||
UserManager,
|
||||
// Add other managers here
|
||||
];
|
||||
|
||||
// Create an empty object to hold data managers for different tables
|
||||
const tables = {};
|
||||
|
||||
// Register each manager as data access point for its table
|
||||
managers.forEach((ManagerClass) => {
|
||||
const manager = new ManagerClass();
|
||||
|
||||
tables[manager.table] = manager;
|
||||
});
|
||||
|
||||
/* ************************************************************************* */
|
||||
|
||||
// Use a Proxy to customize error messages when trying to access a non-existing table
|
||||
|
||||
// Export the Proxy instance with custom error handling
|
||||
module.exports = new Proxy(tables, {
|
||||
get(obj, prop) {
|
||||
// Check if the property (table) exists in the tables object
|
||||
if (prop in obj) return obj[prop];
|
||||
|
||||
// If the property (table) does not exist, throw a ReferenceError with a custom error message
|
||||
throw new ReferenceError(
|
||||
`tables.${prop} is not defined. Did you register it in ${__filename}?`
|
||||
);
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user