Les routes et les contrôleurs backend pour l'inscription, la connexion et la déconnexion des utilisateurs

This commit is contained in:
el 2025-05-18 00:46:52 +02:00
parent 37ffb163d7
commit f4ab9b3d15
52 changed files with 10487 additions and 0 deletions

3
backend/.env.example Normal file
View file

@ -0,0 +1,3 @@
PORT=3001
NODE_ENV=development
CORS_ORIGIN=http://localhost:3000

25
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# dependencies
/node_modules
# production
/dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# IDE
.idea/
.vscode/

2182
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

46
backend/package.json Normal file
View file

@ -0,0 +1,46 @@
{
"name": "backend",
"version": "1.0.0",
"description": "Backend API pour WeMusic",
"main": "dist/server.js",
"scripts": {
"start": "node dist/server.js",
"dev": "nodemon src/server.ts",
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1",
"test:db": "ts-node src/scripts/test-db-connection.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"@types/cookie-parser": "^1.4.8",
"@types/express-session": "^1.18.1",
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"express-rate-limit": "^7.5.0",
"express-session": "^1.18.1",
"express-validator": "^7.2.1",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"morgan": "^1.10.0",
"pg": "^8.16.0"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.18",
"@types/express": "^5.0.2",
"@types/helmet": "^0.0.48",
"@types/jsonwebtoken": "^9.0.9",
"@types/morgan": "^1.9.9",
"@types/node": "^22.15.18",
"@types/pg": "^8.15.2",
"nodemon": "^3.1.10",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}

View 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
View 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();
});

View 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);
}
};

View 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');

View 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';

View 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}"

View 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}"

View 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}"

View 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..."
}'

View 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();
};
};

View 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'
});
};

View 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();
};

View 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;

View 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;

View 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
View 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');
}
});

View 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;
}

View 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')
];

View 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')
];

21
backend/tsconfig.json Normal file
View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"lib": ["es2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}