refont back express

This commit is contained in:
jleroy
2025-03-11 10:29:18 +01:00
parent f944b99172
commit 4e41294eee
18 changed files with 507 additions and 9 deletions

2
.gitignore vendored
View File

@@ -40,3 +40,5 @@ testem.log
# System files
.DS_Store
Thumbs.db
/backend/package-lock.json
/backend/node_modules

7
backend/.env Normal file
View File

@@ -0,0 +1,7 @@
ALLOWED_ORIGIN=http://localhost:4300
PORT=3333
JWT_SECRET=d1bcf750e6246f1d75a2b32e3ff9ac7a8507b1d27462fc3223b966a86e3ef28762efcfc90a50e20b1888b1c8d7ce107260784b6d3ac9cb3662f499a7b272b1d926e3e0232de3581f25cde6e6da3dd0f62ee00fbe8bbfc3425c5abc0ad3b53f9b9c75875fe57629c9f1fe01ffb280d5605a54ab90ca15ae4cb6c43e298448e95bffd55582a16d18867d3c5db1cb316ba5fc9dfbdde1d8ef523a2d35f425aacd42286058801e79dc0b7c3fd3f9430ef3696e0fbcef5f28ba12ba3e38ee7e9b8f79d9ae51fb81f13528e9008d917d5be6145f3ab9a621dc89aef1f4df09bcdcce9f109f4f792623061ad1cfe541097fdf3695cfb72673ece58db49894d25f486e99
DB_HOST=localhost
DB_USER=eni
DB_PASSWORD=gkAAUlq2e)0*ROLO
DB_DATABASE=eni

16
backend/config/cors.js Normal file
View File

@@ -0,0 +1,16 @@
import dotenv from 'dotenv';
import * as path from "node:path";
const envFile = `.env`;
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
const allowedOrigin = process.env.ALLOWED_ORIGIN || 'http://localhost:4200';
const corsOptions = {
origin: allowedOrigin,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
};
export default corsOptions;

13
backend/config/db.js Normal file
View File

@@ -0,0 +1,13 @@
import mariadb from 'mariadb';
import dotenv from 'dotenv';
dotenv.config();
const pool = mariadb.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
});
export default pool;

View File

@@ -0,0 +1,6 @@
export default function getConnect(req, res) {
res.json({
message: 'Connexion : OK',
status: true
});
};

View File

@@ -0,0 +1,143 @@
import pool from "../config/db.js";
import jwt from "jsonwebtoken";
import bcrypt from 'bcryptjs';
// Création d'un utilisateur
export const register = async (req, res) => {
let conn;
try {
conn = await pool.getConnection();
const { email, firstname, lastname, password_hash } = req.body;
if (!email || !firstname || !lastname || !password_hash) {
return res.status(400).json({
message: 'Tout les champs sont requis !',
status: false
});
}
const existingUsers = await conn.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (existingUsers.length > 0) {
return res.status(409).json({
message: 'L\'utilisateur existe déjà !',
status: false
});
}
// Hash du mot de passe
const hashedPassword = await bcrypt.hash(password_hash, 10);
const result = await conn.query(
'INSERT INTO users (email, firstname, lastname, password_hash) VALUES (?, ?, ?, ?, ?)',
[email, firstname, lastname, hashedPassword]
);
res.status(201).json({
message: 'L\'utilisateur à bien été enregistré !',
status: true
});
} catch (err) {
console.error('Erreur interne :', err);
return res.status(500).json({
message: 'Erreur interne du serveur.',
status: false
});
} finally {
if (conn) await conn.release();
}
};
// Connexion
export const login = async (req, res) => {
let conn;
try {
conn = await pool.getConnection();
const { email, password } = req.body;
// Chercher l'utilisateur
const users = await conn.query(
'SELECT * FROM users WHERE email = ?',
[email]
);
if (users.length === 0) {
return res.status(401).json({
message: 'Identifiant incorrect',
status: false
});
}
const user = users[0];
// Verify password
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
return res.status(401).json({
message: 'Identifiant incorrect',
status: false
});
}
// Generate JWT token
const token = jwt.sign(
{
id: user.id,
},
process.env.JWT_SECRET || 'fallback_secret',
{ expiresIn: '12h' }
);
await conn.query(
'UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[user.id]
);
await setLogs(email + " s'est connecté !")
res.cookie('jwt', token, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 12 * 60 * 60 * 1000
});
res.json({
message: 'Connexion réussie',
status: true
});
} catch (err) {
console.error('Erreur interne :', err);
return res.status(500).json({
message: 'Erreur interne du serveur.',
status: false
});
} finally {
if (conn) await conn.release();
}
};
// Déconnecter l'utilisateur
export const logout = (req, res) => {
res.clearCookie('jwt', {
httpOnly: true,
secure: true,
sameSite: 'Strict',
});
res.status(200).json({
message: 'Successfully logged out',
status: true
});
};
// Vérification de la connexion sur la page pour guard angular
export const checkAuth = (req, res) => {
const token = req.cookies.jwt;
if (!token) {
return res.status(401).json({
message: 'Non identifié',
status: false
});
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
res.status(200).json({
message: 'identifié',
status: true
});
} catch (err) {
console.error('Erreur interne :', err);
return res.status(500).json({
message: 'Erreur interne du serveur.',
status: false
});
}
};

View File

@@ -0,0 +1,24 @@
export const apiKeyMiddleware = async (req, res, next) => {
const authHeader = req.header('Authorization');
if (!authHeader) {
return res.status(403).json({
message: 'Forbidden: No API Key provided',
status: false
});
}
try {
if (authHeader !== "9IgFg8cnUS4XJE7Q91A0XjrWnjbnBhdk98jcI6fV1n6NAEYz31SHicge8Vkq0bCGvfKsjylb19ouri6FFUeNC1PgPvwrNCC3G5jcz4PLInlFanzf47hCsBJw4IXuhNHC"){
return res.status(403).json({
message: 'Forbidden: Invalid API Key',
status: false
});
}
next();
} catch (err) {
console.error('Erreur interne :', err);
return res.status(500).json({
message: 'Erreur interne du serveur.',
status: false
});
}
};

View File

@@ -0,0 +1,25 @@
import jwt from 'jsonwebtoken';
export const verifyToken = async (req, res, next) => {
// Récupérer le token depuis le cookie 'jxwt'
const token = req.cookies['jwt'];
if (!token) {
return res.status(403).json({
message: 'Token is required',
status: false,
});
}
// Vérifier le token
jwt.verify(token, process.env.JWT_SECRET || 'fallback_secret', (err, decoded) => {
if (err) {
return res.status(401).json({
message: 'Invalid or expired token',
status: false
});
}
req.user = decoded;
next();
});
};

24
backend/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "api_eni_angular",
"version": "1.0.0",
"type": "module",
"description": "API ENI Angular",
"main": "index.js",
"scripts": {
"start": "node server.js"
},
"author": "Johan Leroy",
"license": "ISC",
"dependencies": {
"bcryptjs": "^3.0.2",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"mariadb": "^3.4.0",
"multer": "^1.4.5-lts.1",
"url": "^0.11.4",
"uuid": "^11.1.0"
}
}

View File

@@ -0,0 +1,53 @@
import express from "express";
const router = express.Router();
import apiController from "../controllers/apiController.js";
import {
register,
login,
checkAuth,
logout,
} from "../controllers/authController.js";
import {verifyToken} from "../middleware/tokenJWTMiddleware.js";
import multer from "multer";
import { v4 as uuidv4 } from 'uuid';
import path from "node:path";
router.get('', apiController);
// Upload
const generateFileName = (originalName) => {
const ext = path.extname(originalName);
const now = new Date();
const timestamp = now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
const uuid = uuidv4();
return `${timestamp}_${uuid}${ext}`;
};
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/'); // Stockage des fichiers dans le dossier uploads/
},
filename: function (req, file, cb) {
cb(null, generateFileName(file.originalname));
}
});
const upload = multer({ storage: storage });
router.post('/upload', verifyToken, upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).json({ message: 'Aucun fichier fourni' });
}
const filePath = `/uploads/${req.file.filename}`;
res.json({ filePath });
});
// Authentification et utilisateur
router.post('/auth/register', register);
router.post('/auth/login', login);
router.post('/auth/logout', logout);
router.get('/auth/check-auth', verifyToken, checkAuth);
export default router;

82
backend/server.js Normal file
View File

@@ -0,0 +1,82 @@
import express from 'express';
import cors from 'cors';
import apiRoutes from './routes/apiRoutes.js';
import corsOptions from './config/cors.js'
import {apiKeyMiddleware} from './middleware/apiKeyMiddleware.js';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
import pool from "./config/db.js";
import * as path from "node:path";
import { fileURLToPath } from 'url';
const envFile = `.env`;
dotenv.config({ path: path.resolve(process.cwd(), envFile), override: true });
const app = express();
// Utiliser CORS
app.use(cors(corsOptions));
// Utilisation des cookies
app.use(cookieParser());
// Middleware pour parser les corps de requêtes JSON
app.use(express.json());
// Utilisation des fichiers/images
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Middleware pour vérifier la clé API (Authorization)
app.use('/', apiKeyMiddleware);
// Utiliser les routes API
app.use('/', apiRoutes);
// Middleware pour les routes non trouvées
app.use((req, res) => {
res.status(404).json({
message: 'Forbidden: Invalid Request',
status: false
});
});
// Test de conenxion à la base de donnée
const testDatabaseConnection = async () => {
let conn;
try {
conn = await pool.getConnection();
console.log('✅ Connexion à la base de données réussie ! 🎉');
} catch (err) {
console.error('❌ Erreur de connexion à la base de données :', err.message);
throw new Error('⛔ Impossible de se connecter à la base de données.');
} finally {
if (conn) await conn.end();
}
};
// Lancer le serveur
const startServer = async () => {
while (true) {
try {
console.log(`🚀 Lancement de l'API en cours... 🔧`);
await testDatabaseConnection();
app.listen(process.env.PORT, () => {
console.log(`📢 NODE_ENV = ${process.env.NODE_ENV} 🌍`);
console.log(`📢 Chargement du fichier : ${envFile} 📄`);
console.log(`🚀 API démarrée sur le port ${process.env.PORT} 🎯`);
console.log(`🌍 Allowed Origin : ${process.env.ALLOWED_ORIGIN} 🔗`);
console.log(`✅ L'API est lancée et prête à l'emploi ! 🎉`);
});
break;
} catch (err) {
console.error('❌ Le serveur na pas pu démarrer :', err.message);
console.log('⏳ Nouvelle tentative de démarrage dans 5 secondes... 🔄');
await new Promise((resolve) => setTimeout(resolve, 5000));
}
}
};
startServer();

View File

@@ -3,7 +3,7 @@
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"start": "ng serve --port=4300",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"

View File

@@ -22,12 +22,6 @@
<p class="block mr-auto font-sans text-base antialiased font-normal leading-relaxed text-blue-gray-900">
Ma bibliothèque
</p>
<span class="ml-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2.5"
stroke="currentColor" aria-hidden="true" class="w-4 h-4 mx-auto transition-transform rotate-180">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"></path>
</svg>
</span>
</button>
</div>
<div class="overflow-hidden">

View File

@@ -1,2 +1,6 @@
<app-sidbar />
<router-outlet />
<div class="flex flex-row">
<app-sidbar></app-sidbar>
<div class="flex-1">
<router-outlet></router-outlet>
</div>
</div>

View File

@@ -0,0 +1,86 @@
<section class="text-gray-600 body-font">
<div class="container px-5 py-24 mx-auto">
<div class="flex flex-wrap -m-4">
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/420x260">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
<p class="mt-1">$16.00</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/421x261">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
<p class="mt-1">$21.15</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/422x262">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
<p class="mt-1">$12.00</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/423x263">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
<p class="mt-1">$18.40</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/424x264">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">The Catalyzer</h2>
<p class="mt-1">$16.00</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/425x265">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">Shooting Stars</h2>
<p class="mt-1">$21.15</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/427x267">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">Neptune</h2>
<p class="mt-1">$12.00</p>
</div>
</div>
<div class="lg:w-1/4 md:w-1/2 p-4 w-full">
<a class="block relative h-48 rounded overflow-hidden">
<img alt="ecommerce" class="object-cover object-center w-full h-full block" src="https://dummyimage.com/428x268">
</a>
<div class="mt-4">
<h3 class="text-gray-500 text-xs tracking-widest title-font mb-1">CATEGORY</h3>
<h2 class="text-gray-900 title-font text-lg font-medium">The 400 Blows</h2>
<p class="mt-1">$18.40</p>
</div>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,19 @@
import { Component } from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {NgForOf, NgIf} from '@angular/common';
@Component({
selector: 'app-search',
imports: [
NgIf,
NgForOf,
ReactiveFormsModule
],
templateUrl: './search.component.html',
styleUrl: './search.component.css'
})
export class SearchComponent {
}