Les routes et les contrôleurs backend pour l'inscription, la connexion et la déconnexion des utilisateurs
This commit is contained in:
parent
37ffb163d7
commit
f4ab9b3d15
52 changed files with 10487 additions and 0 deletions
55
backend/src/config/database.ts
Normal file
55
backend/src/config/database.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Pool, PoolConfig } from 'pg';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// Configuration de base de la connexion
|
||||
const poolConfig: PoolConfig = {
|
||||
user: process.env.DB_USER,
|
||||
host: process.env.DB_HOST || '192.168.0.104',
|
||||
database: process.env.DB_NAME || 'wimusic_db',
|
||||
password: process.env.DB_PASSWORD,
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
// Configuration du timeout et des tentatives de reconnexion
|
||||
connectionTimeoutMillis: 5000,
|
||||
idleTimeoutMillis: 30000,
|
||||
max: 20, // nombre maximum de clients dans le pool
|
||||
};
|
||||
|
||||
// Si SSL est requis (recommandé pour la production)
|
||||
if (process.env.DB_SSL === 'true') {
|
||||
poolConfig.ssl = {
|
||||
rejectUnauthorized: false // À modifier selon votre configuration SSL
|
||||
};
|
||||
}
|
||||
|
||||
// Création du pool de connexions
|
||||
const pool = new Pool(poolConfig);
|
||||
|
||||
// Gestion des événements de connexion
|
||||
pool.on('connect', () => {
|
||||
console.log('📦 Connexion établie avec la base de données PostgreSQL');
|
||||
console.log(`📍 Hôte: ${poolConfig.host}`);
|
||||
console.log(`📚 Base de données: ${poolConfig.database}`);
|
||||
});
|
||||
|
||||
// Gestion des erreurs
|
||||
pool.on('error', (err) => {
|
||||
console.error('❌ Erreur de connexion à la base de données:', err);
|
||||
console.error('⚠️ Vérifiez vos paramètres de connexion et que le serveur est accessible');
|
||||
});
|
||||
|
||||
// Fonction pour tester la connexion
|
||||
export const testConnection = async (): Promise<boolean> => {
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
console.log('✅ Test de connexion réussi');
|
||||
client.release();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('❌ Test de connexion échoué:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default pool;
|
20
backend/src/config/db.ts
Normal file
20
backend/src/config/db.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Pool } from 'pg';
|
||||
|
||||
// Configuration du pool de connexion PostgreSQL
|
||||
export const pool = new Pool({
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
database: process.env.DB_NAME || 'wemusic',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
});
|
||||
|
||||
// Test de connexion
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
console.error('🔥 Erreur de connexion à la base de données:', err.stack);
|
||||
return;
|
||||
}
|
||||
console.log('✅ Connexion à la base de données établie avec succès');
|
||||
release();
|
||||
});
|
198
backend/src/controllers/auth.controller.ts
Normal file
198
backend/src/controllers/auth.controller.ts
Normal file
|
@ -0,0 +1,198 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { validationResult } from 'express-validator';
|
||||
import bcrypt from 'bcrypt';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Pool } from 'pg';
|
||||
import pool from '../config/database';
|
||||
import { AppError } from '../middleware/error.middleware';
|
||||
import { UserRegistrationData, UserResponse } from '../types/user.types';
|
||||
import 'express-session';
|
||||
|
||||
declare module 'express-session' {
|
||||
interface SessionData {
|
||||
userId: string;
|
||||
userEmail: string;
|
||||
userType: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Interface pour les données de connexion
|
||||
interface LoginData {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Interface pour l'utilisateur en base de données
|
||||
interface DBUser {
|
||||
id: number;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
artist_name: string;
|
||||
user_type: string;
|
||||
bio_short?: string;
|
||||
bio_long?: string;
|
||||
profile_picture_url?: string;
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
// Validation des données
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({ errors: errors.array() });
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, password, artist_name, user_type = 'artist', bio_short, bio_long }: UserRegistrationData = req.body;
|
||||
|
||||
// Vérification si l'email existe déjà
|
||||
const existingUser = await pool.query(
|
||||
'SELECT id FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
if (existingUser.rows.length > 0) {
|
||||
res.status(409).json({
|
||||
error: 'Un utilisateur avec cette adresse email existe déjà'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Hachage du mot de passe
|
||||
const saltRounds = 12;
|
||||
const password_hash = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
// Insertion du nouvel utilisateur
|
||||
const result = await pool.query(
|
||||
`INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
artist_name,
|
||||
user_type,
|
||||
bio_short,
|
||||
bio_long
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, email, artist_name, user_type, bio_short, bio_long, profile_picture_url, created_at`,
|
||||
[email, password_hash, artist_name, user_type, bio_short, bio_long]
|
||||
);
|
||||
|
||||
// Construction de la réponse
|
||||
const user: UserResponse = result.rows[0];
|
||||
|
||||
// Envoi de la réponse
|
||||
res.status(201).json({
|
||||
message: 'Inscription réussie',
|
||||
user
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'inscription:', error);
|
||||
res.status(500).json({
|
||||
error: 'Une erreur est survenue lors de l\'inscription'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Contrôleur pour la connexion d'un utilisateur
|
||||
* @route POST /api/auth/login
|
||||
*/
|
||||
export const login = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
// Validation des données
|
||||
const errors = validationResult(req);
|
||||
if (!errors.isEmpty()) {
|
||||
throw new AppError(400, 'Données de connexion invalides', errors.array());
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Recherche de l'utilisateur dans la base de données
|
||||
const result = await pool.query<DBUser>(
|
||||
'SELECT * FROM users WHERE email = $1',
|
||||
[email]
|
||||
);
|
||||
|
||||
const user = result.rows[0];
|
||||
|
||||
// Vérification si l'utilisateur existe
|
||||
if (!user) {
|
||||
throw new AppError(401, 'Email ou mot de passe incorrect');
|
||||
}
|
||||
|
||||
// Vérification du mot de passe
|
||||
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!isPasswordValid) {
|
||||
throw new AppError(401, 'Email ou mot de passe incorrect');
|
||||
}
|
||||
|
||||
// Création de la session
|
||||
req.session.userId = user.id.toString();
|
||||
req.session.userEmail = user.email;
|
||||
req.session.userType = user.user_type;
|
||||
|
||||
// Création du token JWT pour une double sécurité
|
||||
const token = jwt.sign(
|
||||
{
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
userType: user.user_type
|
||||
},
|
||||
process.env.JWT_SECRET || 'votre_secret_par_defaut',
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
// Suppression du mot de passe avant d'envoyer la réponse
|
||||
const { password_hash, ...userWithoutPassword } = user;
|
||||
|
||||
// Envoi de la réponse
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Connexion réussie',
|
||||
token,
|
||||
user: userWithoutPassword
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Contrôleur pour la déconnexion d'un utilisateur
|
||||
* @route POST /api/auth/logout
|
||||
*/
|
||||
export const logout = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
if (req.session) {
|
||||
// Destruction de la session
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
throw new AppError(500, 'Erreur lors de la déconnexion');
|
||||
}
|
||||
|
||||
// Suppression du cookie de session
|
||||
res.clearCookie('wemusic.sid', {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: process.env.NODE_ENV === 'production' ? ('strict' as const) : ('lax' as const)
|
||||
});
|
||||
|
||||
// Envoi de la réponse
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Déconnexion réussie'
|
||||
});
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Déjà déconnecté'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
68
backend/src/db/examples/user_queries.sql
Normal file
68
backend/src/db/examples/user_queries.sql
Normal file
|
@ -0,0 +1,68 @@
|
|||
-- ⚠️ ATTENTION : Ces exemples sont uniquement à des fins de démonstration
|
||||
-- En production, utilisez TOUJOURS une bibliothèque comme bcrypt pour le hachage des mots de passe
|
||||
|
||||
-- 1. Insertion d'un nouvel utilisateur
|
||||
-- ⚠️ N'utilisez JAMAIS MD5 pour le hachage des mots de passe en production !
|
||||
INSERT INTO users (
|
||||
email,
|
||||
password_hash,
|
||||
artist_name,
|
||||
user_type,
|
||||
bio_short,
|
||||
bio_long
|
||||
) VALUES (
|
||||
'artiste@example.com',
|
||||
-- Ceci est un exemple NON SÉCURISÉ. Utilisez bcrypt en production !
|
||||
md5('motdepasse123'),
|
||||
'Artiste Exemple',
|
||||
'artist',
|
||||
'Musicien indie rock basé à Paris',
|
||||
'Artiste indie rock avec plus de 10 ans d''expérience...'
|
||||
) RETURNING id, email, artist_name, created_at;
|
||||
|
||||
-- 2. Sélection d'un utilisateur par email
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
artist_name,
|
||||
user_type,
|
||||
bio_short,
|
||||
profile_picture_url,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE email = 'artiste@example.com';
|
||||
|
||||
-- 3. Mise à jour de la photo de profil
|
||||
UPDATE users
|
||||
SET profile_picture_url = 'https://example.com/photos/profile123.jpg'
|
||||
WHERE email = 'artiste@example.com'
|
||||
RETURNING id, email, artist_name, profile_picture_url;
|
||||
|
||||
-- 4. Recherche d'utilisateurs par type
|
||||
SELECT
|
||||
id,
|
||||
artist_name,
|
||||
email,
|
||||
user_type,
|
||||
created_at
|
||||
FROM users
|
||||
WHERE user_type = 'artist'
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- 5. Suppression d'un utilisateur
|
||||
-- ⚠️ En production, préférez une suppression logique avec un champ 'deleted_at'
|
||||
DELETE FROM users
|
||||
WHERE email = 'artiste@example.com'
|
||||
RETURNING id, email, artist_name;
|
||||
|
||||
-- Exemple de requête pour vérifier l'authentification
|
||||
-- ⚠️ Encore une fois, n'utilisez JAMAIS MD5 en production !
|
||||
SELECT
|
||||
id,
|
||||
email,
|
||||
artist_name,
|
||||
user_type
|
||||
FROM users
|
||||
WHERE
|
||||
email = 'artiste@example.com'
|
||||
AND password_hash = md5('motdepasse123');
|
61
backend/src/db/migrations/001_create_users_table.sql
Normal file
61
backend/src/db/migrations/001_create_users_table.sql
Normal file
|
@ -0,0 +1,61 @@
|
|||
-- Création du type ENUM pour user_type
|
||||
CREATE TYPE user_type AS ENUM ('artist', 'venue_owner', 'partner', 'admin');
|
||||
|
||||
-- Création de l'extension uuid-ossp si elle n'existe pas
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Création de la table users
|
||||
CREATE TABLE users (
|
||||
-- Identifiant unique de l'utilisateur
|
||||
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
|
||||
|
||||
-- Informations d'authentification
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
|
||||
-- Informations de profil
|
||||
artist_name TEXT NOT NULL,
|
||||
user_type user_type NOT NULL DEFAULT 'artist',
|
||||
|
||||
-- Champs de biographie
|
||||
bio_short TEXT,
|
||||
bio_long TEXT,
|
||||
|
||||
-- URL de la photo de profil
|
||||
profile_picture_url TEXT,
|
||||
|
||||
-- Horodatage
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Création d'un index sur l'email pour optimiser les recherches
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
-- Fonction pour mettre à jour automatiquement updated_at
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Trigger pour mettre à jour automatiquement updated_at
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Commentaires sur la table et les colonnes
|
||||
COMMENT ON TABLE users IS 'Table stockant les informations des utilisateurs de WeMusic';
|
||||
COMMENT ON COLUMN users.id IS 'Identifiant unique de l''utilisateur';
|
||||
COMMENT ON COLUMN users.email IS 'Adresse email unique de l''utilisateur, utilisée pour l''authentification';
|
||||
COMMENT ON COLUMN users.password_hash IS 'Hash du mot de passe de l''utilisateur (à générer avec bcrypt)';
|
||||
COMMENT ON COLUMN users.artist_name IS 'Nom d''artiste ou nom d''utilisateur';
|
||||
COMMENT ON COLUMN users.user_type IS 'Type d''utilisateur (artist, venue_owner, partner, admin)';
|
||||
COMMENT ON COLUMN users.bio_short IS 'Courte biographie de l''utilisateur';
|
||||
COMMENT ON COLUMN users.bio_long IS 'Biographie détaillée de l''utilisateur';
|
||||
COMMENT ON COLUMN users.profile_picture_url IS 'URL de la photo de profil';
|
||||
COMMENT ON COLUMN users.created_at IS 'Date et heure de création du compte';
|
||||
COMMENT ON COLUMN users.updated_at IS 'Date et heure de la dernière modification du compte';
|
39
backend/src/examples/api-test.sh
Executable file
39
backend/src/examples/api-test.sh
Executable file
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Couleurs pour le terminal
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
API_URL="http://192.168.0.90:3001"
|
||||
|
||||
echo "🔍 Test des routes d'API..."
|
||||
|
||||
# Test de la route racine de l'API
|
||||
echo -e "\n${GREEN}1. Test de la route /api :${NC}"
|
||||
curl -X GET "${API_URL}/api"
|
||||
|
||||
# Test de la route d'inscription
|
||||
echo -e "\n\n${GREEN}2. Test de la route /api/auth/register :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "Password123!",
|
||||
"artist_name": "Test Artist",
|
||||
"user_type": "artist",
|
||||
"bio_short": "Test bio",
|
||||
"bio_long": "Test biography long version"
|
||||
}'
|
||||
|
||||
# Test de la route de connexion
|
||||
echo -e "\n\n${GREEN}3. Test de la route /api/auth/login :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "test@example.com",
|
||||
"password": "Password123!"
|
||||
}'
|
||||
|
||||
echo -e "\n\n${GREEN}Tests terminés !${NC}"
|
48
backend/src/examples/login-test.sh
Executable file
48
backend/src/examples/login-test.sh
Executable file
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Couleurs pour le terminal
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration de l'API
|
||||
API_URL="http://192.168.0.90:3001"
|
||||
|
||||
echo "🔍 Test de connexion d'un utilisateur..."
|
||||
|
||||
# Test avec des données valides
|
||||
echo -e "\n${GREEN}1. Test avec des données valides :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "MonMotDePasse123!"
|
||||
}'
|
||||
|
||||
# Test avec un email invalide
|
||||
echo -e "\n\n${GREEN}2. Test avec un email invalide :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "email_invalide",
|
||||
"password": "MonMotDePasse123!"
|
||||
}'
|
||||
|
||||
# Test avec un mot de passe incorrect
|
||||
echo -e "\n\n${GREEN}3. Test avec un mot de passe incorrect :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "MauvaisMotDePasse123!"
|
||||
}'
|
||||
|
||||
# Test avec des champs manquants
|
||||
echo -e "\n\n${GREEN}4. Test avec des champs manquants :${NC}"
|
||||
curl -X POST "${API_URL}/api/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com"
|
||||
}'
|
||||
|
||||
echo -e "\n\n${GREEN}Tests terminés !${NC}"
|
54
backend/src/examples/register-test.sh
Executable file
54
backend/src/examples/register-test.sh
Executable file
|
@ -0,0 +1,54 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Couleurs pour le terminal
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo "🔍 Test d'inscription d'un nouvel utilisateur..."
|
||||
|
||||
# Test avec des données valides
|
||||
echo -e "\n${GREEN}1. Test avec des données valides :${NC}"
|
||||
curl -X POST http://localhost:3001/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "MonMotDePasse123!",
|
||||
"artist_name": "John Doe",
|
||||
"user_type": "artist",
|
||||
"bio_short": "Musicien indie rock",
|
||||
"bio_long": "Musicien indie rock avec plus de 10 ans d'\''expérience..."
|
||||
}'
|
||||
|
||||
# Test avec un email invalide
|
||||
echo -e "\n\n${GREEN}2. Test avec un email invalide :${NC}"
|
||||
curl -X POST http://localhost:3001/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "email_invalide",
|
||||
"password": "MonMotDePasse123!",
|
||||
"artist_name": "John Doe"
|
||||
}'
|
||||
|
||||
# Test avec un mot de passe trop court
|
||||
echo -e "\n\n${GREEN}3. Test avec un mot de passe trop court :${NC}"
|
||||
curl -X POST http://localhost:3001/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "court",
|
||||
"artist_name": "John Doe"
|
||||
}'
|
||||
|
||||
# Test avec un type d'utilisateur invalide
|
||||
echo -e "\n\n${GREEN}4. Test avec un type d'utilisateur invalide :${NC}"
|
||||
curl -X POST http://localhost:3001/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "MonMotDePasse123!",
|
||||
"artist_name": "John Doe",
|
||||
"user_type": "invalid_type"
|
||||
}'
|
||||
|
||||
echo -e "\n\n${GREEN}Tests terminés !${NC}"
|
13
backend/src/examples/register.curl.sh
Normal file
13
backend/src/examples/register.curl.sh
Normal file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Test d'inscription d'un nouvel utilisateur
|
||||
curl -X POST http://localhost:3001/api/auth/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "artiste@example.com",
|
||||
"password": "MonMotDePasse123!",
|
||||
"artist_name": "John Doe",
|
||||
"user_type": "artist",
|
||||
"bio_short": "Musicien indie rock",
|
||||
"bio_long": "Musicien indie rock avec plus de 10 ans d'\''expérience..."
|
||||
}'
|
24
backend/src/middleware/auth.middleware.ts
Normal file
24
backend/src/middleware/auth.middleware.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { AppError } from './error.middleware';
|
||||
|
||||
/**
|
||||
* Middleware pour vérifier si l'utilisateur est authentifié
|
||||
*/
|
||||
export const isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.session.userId) {
|
||||
throw new AppError(401, 'Non authentifié');
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Middleware pour vérifier le type d'utilisateur
|
||||
*/
|
||||
export const checkUserType = (allowedTypes: string[]) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.session.userType || !allowedTypes.includes(req.session.userType)) {
|
||||
throw new AppError(403, 'Accès non autorisé');
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
36
backend/src/middleware/error.middleware.ts
Normal file
36
backend/src/middleware/error.middleware.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public statusCode: number,
|
||||
public message: string,
|
||||
public errors?: any[]
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'AppError';
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error | AppError,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Response => {
|
||||
console.error('🔥 Erreur:', err);
|
||||
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
success: false,
|
||||
message: err.message,
|
||||
errors: err.errors
|
||||
});
|
||||
}
|
||||
|
||||
// Erreur par défaut
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: 'Une erreur interne est survenue'
|
||||
});
|
||||
};
|
41
backend/src/middleware/security.middleware.ts
Normal file
41
backend/src/middleware/security.middleware.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
|
||||
// Configuration du rate limiter
|
||||
export const limiter = rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // limite chaque IP à 100 requêtes par fenêtre
|
||||
message: 'Trop de requêtes depuis cette IP, veuillez réessayer plus tard'
|
||||
});
|
||||
|
||||
// Configuration spécifique pour les routes d'authentification
|
||||
export const authLimiter = rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 heure
|
||||
max: 5, // limite chaque IP à 5 tentatives par heure
|
||||
message: 'Trop de tentatives de connexion, veuillez réessayer plus tard'
|
||||
});
|
||||
|
||||
// Middleware de sécurité
|
||||
export const securityMiddleware = [
|
||||
helmet(), // Sécurité des en-têtes HTTP
|
||||
helmet.hidePoweredBy(), // Cache le header X-Powered-By
|
||||
helmet.noSniff(), // Empêche le MIME-type sniffing
|
||||
helmet.xssFilter(), // Protection XSS basique
|
||||
];
|
||||
|
||||
// Middleware de validation du contenu JSON
|
||||
export const validateJson = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
): Response | void => {
|
||||
if (err instanceof SyntaxError && 'body' in err) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'JSON invalide dans la requête'
|
||||
});
|
||||
}
|
||||
next();
|
||||
};
|
20
backend/src/routes/auth.routes.ts
Normal file
20
backend/src/routes/auth.routes.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Router } from 'express';
|
||||
import { register, login } from '../controllers/auth.controller';
|
||||
import { registerValidator, loginValidator } from '../validators/auth.validator';
|
||||
|
||||
const router = Router();
|
||||
|
||||
/**
|
||||
* @route POST /api/auth/register
|
||||
* @desc Inscription d'un nouvel utilisateur
|
||||
* @access Public
|
||||
*/
|
||||
router.post('/register', registerValidator, register);
|
||||
|
||||
/**
|
||||
* @route POST /api/auth/login
|
||||
* @desc Connexion d'un utilisateur existant
|
||||
*/
|
||||
router.post('/login', loginValidator, login);
|
||||
|
||||
export default router;
|
12
backend/src/routes/test.routes.ts
Normal file
12
backend/src/routes/test.routes.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Router } from 'express';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/test', (req, res) => {
|
||||
res.json({
|
||||
message: 'Connexion backend réussie !',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
18
backend/src/scripts/test-db-connection.ts
Normal file
18
backend/src/scripts/test-db-connection.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { testConnection } from '../config/database';
|
||||
|
||||
console.log('🔍 Test de connexion à la base de données...');
|
||||
|
||||
testConnection()
|
||||
.then(success => {
|
||||
if (success) {
|
||||
console.log('✨ La connexion à la base de données est fonctionnelle');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error('❌ Échec de la connexion à la base de données');
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('💥 Erreur lors du test de connexion:', error);
|
||||
process.exit(1);
|
||||
});
|
124
backend/src/server.ts
Normal file
124
backend/src/server.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
import express, { Response as ExpressResponse } from 'express';
|
||||
import cors from 'cors';
|
||||
import morgan from 'morgan';
|
||||
import dotenv from 'dotenv';
|
||||
import session from 'express-session';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import crypto from 'crypto';
|
||||
import authRoutes from './routes/auth.routes';
|
||||
import { errorHandler, AppError } from './middleware/error.middleware';
|
||||
import {
|
||||
limiter,
|
||||
authLimiter,
|
||||
securityMiddleware,
|
||||
validateJson
|
||||
} from './middleware/security.middleware';
|
||||
|
||||
// Configuration des variables d'environnement
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3001;
|
||||
const nodeEnv = process.env.NODE_ENV || 'development';
|
||||
const isProduction = nodeEnv === 'production';
|
||||
|
||||
// Génération d'un secret de session aléatoire si non défini
|
||||
if (!process.env.SESSION_SECRET) {
|
||||
process.env.SESSION_SECRET = crypto.randomBytes(32).toString('hex');
|
||||
console.warn('⚠️ Attention : SESSION_SECRET non défini dans les variables d\'environnement.');
|
||||
console.warn('🔑 Un secret temporaire a été généré pour cette session.');
|
||||
}
|
||||
|
||||
// Middlewares de sécurité
|
||||
app.use(securityMiddleware);
|
||||
app.use(limiter);
|
||||
|
||||
// Configuration CORS avec support des cookies
|
||||
app.use(cors({
|
||||
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization']
|
||||
}));
|
||||
|
||||
// Configuration des cookies et sessions
|
||||
app.use(cookieParser());
|
||||
|
||||
// Configuration de la session
|
||||
const sessionConfig = {
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
name: 'wemusic.sid', // Pour masquer l'utilisation d'express-session
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
rolling: true, // Renouvelle le cookie à chaque requête
|
||||
cookie: {
|
||||
secure: isProduction,
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 60 * 1000, // 1 heure
|
||||
sameSite: isProduction ? ('strict' as const) : ('lax' as const),
|
||||
path: '/',
|
||||
domain: undefined // Le domaine sera automatiquement défini normalement
|
||||
}
|
||||
};
|
||||
|
||||
// Avertissement en développement pour le cookie non sécurisé
|
||||
if (!isProduction) {
|
||||
console.warn('⚠️ Mode développement : Cookie secure désactivé');
|
||||
console.warn('🔒 En production, activez HTTPS et secure: true');
|
||||
}
|
||||
|
||||
app.use(session(sessionConfig));
|
||||
|
||||
// Middleware de base
|
||||
app.use(morgan(nodeEnv === 'development' ? 'dev' : 'combined'));
|
||||
app.use(express.json({
|
||||
verify: (req, res: ExpressResponse, buf, encoding) => {
|
||||
try {
|
||||
JSON.parse(buf.toString());
|
||||
} catch (e) {
|
||||
res.status(400).json({ message: 'JSON invalide dans la requête' });
|
||||
throw new Error('JSON invalide');
|
||||
}
|
||||
}
|
||||
}));
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Middleware pour renouveler la session
|
||||
app.use((req, res, next) => {
|
||||
if (req.session && req.session.userId) {
|
||||
// Renouvelle la session si l'utilisateur est authentifié
|
||||
req.session.touch();
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Routes d'authentification avec rate limiting
|
||||
app.use('/api/auth', authLimiter, authRoutes);
|
||||
|
||||
// Route de base de l'API
|
||||
app.get('/api', (req, res) => {
|
||||
res.json({
|
||||
message: 'Bienvenue sur l\'API WeMusic!',
|
||||
environment: nodeEnv,
|
||||
version: '1.0.0'
|
||||
});
|
||||
});
|
||||
|
||||
// Middleware de gestion des erreurs
|
||||
app.use((err: Error | AppError, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
errorHandler(err, req, res, next);
|
||||
});
|
||||
|
||||
// Gestion des routes non trouvées
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ message: 'Route non trouvée' });
|
||||
});
|
||||
|
||||
// Démarrage du serveur
|
||||
app.listen(port, () => {
|
||||
console.log(`🚀 Serveur ${nodeEnv} démarré sur le port ${port}`);
|
||||
console.log(`👉 CORS activé pour: ${process.env.CORS_ORIGIN || 'http://localhost:3000'}`);
|
||||
if (!isProduction) {
|
||||
console.log('⚙️ Mode développement actif');
|
||||
}
|
||||
});
|
34
backend/src/types/user.types.ts
Normal file
34
backend/src/types/user.types.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
export type UserType = 'artist' | 'venue_owner' | 'partner' | 'admin';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password_hash: string;
|
||||
artist_name: string;
|
||||
user_type: UserType;
|
||||
bio_short?: string;
|
||||
bio_long?: string;
|
||||
profile_picture_url?: string;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
export interface UserRegistrationData {
|
||||
email: string;
|
||||
password: string;
|
||||
artist_name: string;
|
||||
user_type?: UserType;
|
||||
bio_short?: string;
|
||||
bio_long?: string;
|
||||
}
|
||||
|
||||
export interface UserResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
artist_name: string;
|
||||
user_type: UserType;
|
||||
bio_short?: string;
|
||||
bio_long?: string;
|
||||
profile_picture_url?: string;
|
||||
created_at: Date;
|
||||
}
|
20
backend/src/validators/auth.validator.ts
Normal file
20
backend/src/validators/auth.validator.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { body } from 'express-validator';
|
||||
|
||||
export const registerValidator = [
|
||||
// ... existing code ...
|
||||
];
|
||||
|
||||
export const loginValidator = [
|
||||
body('email')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('L\'email est requis')
|
||||
.isEmail()
|
||||
.withMessage('L\'email n\'est pas valide'),
|
||||
|
||||
body('password')
|
||||
.notEmpty()
|
||||
.withMessage('Le mot de passe est requis')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Le mot de passe doit contenir au moins 8 caractères')
|
||||
];
|
51
backend/src/validators/auth.validators.ts
Normal file
51
backend/src/validators/auth.validators.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { body } from 'express-validator';
|
||||
import { UserType } from '../types/user.types';
|
||||
|
||||
export const registerValidator = [
|
||||
// Email validation
|
||||
body('email')
|
||||
.trim()
|
||||
.isEmail()
|
||||
.withMessage('L\'adresse email n\'est pas valide')
|
||||
.normalizeEmail(),
|
||||
|
||||
// Password validation
|
||||
body('password')
|
||||
.isLength({ min: 8 })
|
||||
.withMessage('Le mot de passe doit contenir au moins 8 caractères')
|
||||
.matches(/[A-Z]/)
|
||||
.withMessage('Le mot de passe doit contenir au moins une majuscule')
|
||||
.matches(/[a-z]/)
|
||||
.withMessage('Le mot de passe doit contenir au moins une minuscule')
|
||||
.matches(/[0-9]/)
|
||||
.withMessage('Le mot de passe doit contenir au moins un chiffre')
|
||||
.matches(/[!@#$%^&*]/)
|
||||
.withMessage('Le mot de passe doit contenir au moins un caractère spécial (!@#$%^&*)'),
|
||||
|
||||
// Artist name validation
|
||||
body('artist_name')
|
||||
.trim()
|
||||
.notEmpty()
|
||||
.withMessage('Le nom d\'artiste est requis')
|
||||
.isLength({ min: 2, max: 100 })
|
||||
.withMessage('Le nom d\'artiste doit contenir entre 2 et 100 caractères'),
|
||||
|
||||
// User type validation
|
||||
body('user_type')
|
||||
.optional()
|
||||
.isIn(['artist', 'venue_owner', 'partner', 'admin'] as UserType[])
|
||||
.withMessage('Le type d\'utilisateur n\'est pas valide'),
|
||||
|
||||
// Bio validations
|
||||
body('bio_short')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 200 })
|
||||
.withMessage('La bio courte ne doit pas dépasser 200 caractères'),
|
||||
|
||||
body('bio_long')
|
||||
.optional()
|
||||
.trim()
|
||||
.isLength({ max: 2000 })
|
||||
.withMessage('La bio longue ne doit pas dépasser 2000 caractères')
|
||||
];
|
Loading…
Add table
Add a link
Reference in a new issue