Version: 2.0.0
Node.js: >= 18.0.0
Framework: Express 5.2.1
Base de données: Firebase Firestore
Port: 2106
- Architecture Générale
- Stack Technologique
- Structure des Fichiers
- Configuration & Variables d'Environnement
- Authentification JWT
- API Routes - Toutes les Endpoints
- Schémas de Validation
- Middleware
- Base de Données Firestore
- Seed Scripts
- Sécurité
- Logging
- Gestion des Erreurs
- Scripts NPM
backend/
├── src/
│ ├── config/ # Configuration (Firebase, Swagger)
│ ├── middleware/ # Authentification, validation, error handler
│ ├── routes/ # Toutes les routes API (8 modules)
│ ├── utils/ # Utilitaires (logger, tokenBlacklist)
│ └── index.js # Point d'entrée principal
├── seed-*.js # Scripts de population de données
├── test-api.js # Tests des endpoints
├── package.json
└── .env # Variables d'environnement
Client Request
↓
CORS Middleware (allowedOrigins check)
↓
Rate Limiter (100 req/15min)
↓
Helmet (Security Headers)
↓
Body Parser (JSON)
↓
Routes (/api/*)
↓
authenticateToken (JWT verification)
↓
validate(schema) (Joi validation)
↓
Controller Logic
↓
Firestore Database
↓
Response (JSON)
↓
Error Handler (if error)
↓
Logger (Winston)
{
"express": "^5.2.1", // Framework web
"firebase-admin": "^13.6.1", // SDK Firebase
"jsonwebtoken": "^9.0.3", // JWT authentication
"bcrypt": "^6.0.0", // Hash passwords
"joi": "^18.0.2", // Validation schemas
"helmet": "^8.1.0", // Security headers
"cors": "^2.8.6", // CORS policy
"express-rate-limit": "^8.2.1", // Rate limiting
"winston": "^3.19.0", // Logging
"swagger-jsdoc": "^6.2.8", // API documentation
"swagger-ui-express": "^5.0.1", // Swagger UI
"dotenv": "^17.3.1", // Environment variables
"morgan": "^1.10.1", // HTTP request logger
"axios": "^1.13.5" // HTTP client
}{
"nodemon": "^3.1.14", // Auto-reload dev server
"eslint": "^10.0.1" // Code linting
}backend/
│
├── src/
│ ├── index.js # Point d'entrée, configuration Express
│ │
│ ├── config/
│ │ ├── firebase.js # Firebase Admin SDK init, getDb()
│ │ └── swagger.js # Configuration Swagger/OpenAPI
│ │
│ ├── middleware/
│ │ ├── auth.js # authenticateToken (JWT verification)
│ │ ├── validate.js # validate(schema) + tous les schémas Joi
│ │ └── errorHandler.js # Gestionnaire d'erreurs centralisé
│ │
│ ├── routes/
│ │ ├── index.js # Routes principales + /health
│ │ ├── auth.js # Login, logout, verify (3 routes)
│ │ ├── valentine.js # Demandes St-Valentin (5 routes)
│ │ ├── messages.js # Messages "Open When" (5 routes)
│ │ ├── coupons.js # Bons cadeaux (7 routes)
│ │ ├── planning.js # Événements/dates (5 routes)
│ │ ├── quiz.js # Quiz questions + réponses (9 routes)
│ │ └── playlist.js # Chansons playlist (7 routes)
│ │
│ └── utils/
│ ├── logger.js # Winston logger configuré
│ └── tokenBlacklist.js # Set() pour tokens révoqués
│
├── seed-coupons.js # Seed 6 coupons
├── seed-messages.js # Seed 7 messages
├── seed-planning.js # Seed 5 événements
├── seed-quiz.js # Seed 8 questions
├── seed-playlist.js # Seed 7 chansons
├── test-api.js # Tests endpoints
├── package.json # Dépendances NPM
├── .env # Variables d'environnement
├── .env.example # Template .env
├── .eslintrc.js # Config ESLint
└── princess-project-...-adminsdk.json # Clé Firebase Service Account
# Port du serveur
PORT=2106
# Sécurité - Authentification
APP_PASSWORD=your_secure_password_here
JWT_SECRET=your_super_secret_jwt_key_minimum_32_characters
# Firebase Configuration
FIREBASE_PROJECT_ID=princess-project-210622
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_KEY\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxxxx@princess-project.iam.gserviceaccount.com
# CORS - Frontend URL
FRONTEND_URL=http://localhost:1308
# Environnement
NODE_ENV=development # ou 'production'import admin from 'firebase-admin';
import dotenv from 'dotenv';
dotenv.config();
// Mode PRODUCTION : Variables d'environnement
if (process.env.FIREBASE_PROJECT_ID && process.env.FIREBASE_PRIVATE_KEY) {
admin.initializeApp({
credential: admin.credential.cert({
projectId: process.env.FIREBASE_PROJECT_ID,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
})
});
}
// Mode DEVELOPMENT : Fichier JSON
else {
const serviceAccount = await import('../princess-project-xxx-adminsdk.json', {
assert: { type: 'json' }
});
admin.initializeApp({
credential: admin.credential.cert(serviceAccount.default)
});
}
export const getDb = () => admin.firestore();
export default admin;// Documentation OpenAPI 3.0
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Princess Project API',
version: '2.0.0',
description: 'API sécurisée pour Princess Project'
},
servers: [
{ url: 'http://localhost:2106/api', description: 'Dev server' },
{ url: 'https://your-production-url.com/api', description: 'Production' }
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT'
}
}
}
},
apis: ['./src/routes/*.js']
};- Type: JSON Web Token (JWT)
- Durée de vie: 7 jours (
expiresIn: '7d') - Algorithme: HS256
- Secret:
process.env.JWT_SECRET(min 32 caractères) - Stockage client: localStorage (
princess_token)
1. Login (POST /api/auth/login)
Client envoie: { "password": "xxx" }
↓
Serveur compare bcrypt(password) avec APP_PASSWORD
↓
Si OK: Génère JWT avec payload { authenticated: true }
↓
Retourne: { "success": true, "token": "eyJhbG..." }
2. Requêtes Protégées (avec authenticateToken middleware)
Client envoie: Header "Authorization: Bearer eyJhbG..."
↓
Serveur vérifie:
- Token présent ?
- Token dans blacklist ?
- Token valide (signature + expiration) ?
↓
Si OK: req.user = decoded payload, next()
Si KO: res.status(401).json({ error: "..." })
3. Logout (POST /api/auth/logout)
Client envoie token dans header
↓
Serveur ajoute token à tokenBlacklist (Set)
↓
Retourne: { "success": true }
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
if (!token) {
return res.status(401).json({ error: 'Token d\'authentification manquant' });
}
// Vérifier si token blacklisté (logout)
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token révoqué' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expiré' });
}
return res.status(403).json({ error: 'Token invalide' });
}
};| Module | Nombre Routes | Authentification |
|---|---|---|
| Health | 1 | ❌ Non |
| Auth | 3 | |
| Valentine | 5 | ✅ Oui |
| Messages | 5 | ✅ Oui |
| Coupons | 7 | ✅ Oui |
| Planning | 5 | ✅ Oui |
| Quiz | 9 | ✅ Oui |
| Playlist | 7 | ✅ Oui |
| TOTAL | 42 | - |
Base URL: /api/health
- Auth: ❌ Non
- Description: Vérifier l'état du serveur
- Response:
{
"status": "ok",
"timestamp": "2026-02-23T20:00:00.000Z",
"uptime": 3600,
"modules": {
"auth": true,
"valentine": true,
"messages": true,
"planning": true,
"coupons": true,
"quiz": true,
"playlist": true
}
}Base URL: /api/auth
- Auth: ❌ Non
- Description: S'authentifier avec le mot de passe
- Body:
{
"password": "your_secure_password"
}- Response Success (200):
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"message": "Connexion réussie"
}- Response Error (401):
{
"success": false,
"error": "Mot de passe incorrect"
}- Auth: ✅ Oui (Bearer Token)
- Description: Vérifier la validité du token
- Response Success (200):
{
"valid": true,
"user": { "authenticated": true }
}- Response Error (401/403):
{
"valid": false,
"error": "Token invalide"
}- Auth: ✅ Oui (Bearer Token)
- Description: Déconnexion (blacklist le token)
- Response Success (200):
{
"success": true,
"message": "Déconnexion réussie"
}Base URL: /api/valentine
- Auth: ✅ Oui
- Description: Créer une nouvelle demande de St-Valentin
- Body:
{
"from": "Prince",
"to": "Princess",
"message": "Veux-tu être ma Valentine ?",
"status": "pending"
}- Response (201):
{
"success": true,
"id": "abc123xyz",
"message": "Demande créée avec succès"
}- Auth: ✅ Oui
- Description: Récupérer toutes les demandes Valentine
- Query Params:
?status=accepted(optional: pending/accepted/rejected) - Response (200):
[
{
"id": "abc123",
"from": "Prince",
"to": "Princess",
"message": "Veux-tu être ma Valentine ?",
"status": "accepted",
"createdAt": "2026-02-14T12:00:00.000Z",
"updatedAt": "2026-02-14T13:00:00.000Z"
}
]- Auth: ✅ Oui
- Description: Récupérer une demande spécifique
- Response (200): Objet Valentine complet
- Auth: ✅ Oui
- Description: Modifier une demande existante
- Body: Mêmes champs que POST
- Response (200):
{
"success": true,
"message": "Demande mise à jour"
}- Auth: ✅ Oui
- Description: Supprimer une demande
- Response (200):
{
"success": true,
"message": "Demande supprimée"
}Base URL: /api/messages
- Auth: ✅ Oui
- Description: Créer un nouveau message
- Body:
{
"title": "Ouvre quand tu es triste",
"category": "triste",
"content": "Mon amour, je suis là...",
"lockedUntil": 1740000000000,
"backgroundColor": "#FFE5E5"
}- Response (201): ID du message créé
- Auth: ✅ Oui
- Description: Récupérer tous les messages
- Query Params:
?category=triste(optional) - Response (200):
{
"messages": [
{
"id": "msg123",
"title": "Ouvre quand tu es triste",
"category": "triste",
"content": "Mon amour...",
"isLocked": false,
"lockedUntil": null,
"backgroundColor": "#FFE5E5",
"createdAt": "2026-02-23T10:00:00.000Z"
}
]
}- Auth: ✅ Oui
- Description: Récupérer un message spécifique
- Response (200): Objet Message complet
- Auth: ✅ Oui
- Description: Modifier un message
- Body: Mêmes champs que POST
- Response (200): Confirmation
- Auth: ✅ Oui
- Description: Supprimer un message
- Response (200): Confirmation
Base URL: /api/coupons
- Auth: ✅ Oui
- Description: Créer un nouveau coupon
- Body:
{
"title": "Massage VIP",
"description": "30 min de massage relaxant",
"icon": "💆♀️",
"expirationDate": null
}- Auth: ✅ Oui
- Description: Récupérer tous les coupons
- Query Params:
?status=available(optional: available/redeemed/expired) - Response (200):
{
"coupons": [
{
"id": "coup123",
"title": "Massage VIP",
"description": "30 min de massage",
"icon": "💆♀️",
"isRedeemed": false,
"expirationDate": null,
"createdAt": "2026-02-23T10:00:00.000Z",
"redeemedAt": null
}
]
}- Auth: ✅ Oui
- Description: Récupérer un coupon spécifique
- Auth: ✅ Oui
- Description: Modifier un coupon
- Auth: ✅ Oui
- Description: Utiliser un coupon (marquer comme utilisé)
- Response (200):
{
"success": true,
"message": "Coupon utilisé avec succès ! Profitez-en bien 💖",
"coupon": { /* coupon avec isRedeemed: true */ }
}- Auth: ✅ Oui
- Description: Réinitialiser un coupon (le rendre disponible)
- Auth: ✅ Oui
- Description: Supprimer un coupon
Base URL: /api/planning
- Auth: ✅ Oui
- Description: Créer un nouvel événement
- Body:
{
"title": "Cinéma",
"description": "Voir le dernier Marvel",
"date": "2026-03-15T19:00:00.000Z",
"location": "Cinéma Gaumont",
"category": "cinema",
"status": "planned"
}- Auth: ✅ Oui
- Description: Récupérer tous les événements
- Query Params:
?upcoming=true(seulement événements futurs)?category=cinema(filtrer par catégorie)?status=done(filtrer par status: planned/done/cancelled)
- Response (200):
{
"events": [
{
"id": "evt123",
"title": "Cinéma",
"description": "Voir Marvel",
"date": "2026-03-15T19:00:00.000Z",
"location": "Gaumont",
"category": "cinema",
"status": "planned",
"createdAt": "2026-02-23T10:00:00.000Z"
}
]
}- Auth: ✅ Oui
- Description: Récupérer un événement spécifique
- Auth: ✅ Oui
- Description: Modifier un événement
- Auth: ✅ Oui
- Description: Supprimer un événement
Base URL: /api/quiz
- Auth: ✅ Oui
- Description: Créer une nouvelle question
- Body:
{
"question": "Quelle est ma couleur préférée ?",
"options": ["Rose", "Bleu", "Vert", "Rouge"],
"correctAnswer": "Rose",
"difficulty": "easy"
}- Auth: ✅ Oui
- Description: Récupérer toutes les questions
- Query Params:
?difficulty=easy(optional: easy/medium/hard) - Response (200):
{
"questions": [
{
"id": "q123",
"question": "Quelle est ma couleur préférée ?",
"options": ["Rose", "Bleu", "Vert", "Rouge"],
"correctAnswer": "Rose",
"difficulty": "easy",
"createdAt": "2026-02-23T10:00:00.000Z"
}
]
}- Auth: ✅ Oui
- Description: Récupérer une question aléatoire
- Query Params:
?difficulty=medium(optional)
- Auth: ✅ Oui
- Description: Récupérer une question spécifique
- Auth: ✅ Oui
- Description: Modifier une question
- Auth: ✅ Oui
- Description: Supprimer une question
- Auth: ✅ Oui
- Description: Soumettre une réponse et l'enregistrer
- Body:
{
"questionId": "q123",
"answer": "Rose",
"isCorrect": true
}- Response (201):
{
"success": true,
"isCorrect": true,
"message": "Bonne réponse ! 🎉"
}- Auth: ✅ Oui
- Description: Récupérer les statistiques globales
- Response (200):
{
"totalQuestions": 8,
"totalAnswers": 15,
"correctAnswers": 12,
"score": 80
}- Auth: ✅ Oui
- Description: Récupérer l'historique des réponses
- Query Params:
?limit=10(optional) - Response (200):
{
"history": [
{
"id": "ans123",
"questionId": "q123",
"question": "Quelle est ma couleur préférée ?",
"answer": "Rose",
"isCorrect": true,
"answeredAt": "2026-02-23T10:00:00.000Z"
}
]
}Base URL: /api/playlist
- Auth: ✅ Oui
- Description: Ajouter une nouvelle chanson
- Body:
{
"title": "Our Song",
"artist": "Taylor Swift",
"album": "Taylor Swift",
"duration": "3:22",
"reason": "Notre première danse",
"spotifyUrl": "https://open.spotify.com/track/xxx",
"isFavorite": true
}- Auth: ✅ Oui
- Description: Récupérer toutes les chansons
- Query Params:
?sortBy=playCount(default: createdAt, options: title/artist/playCount)?favorite=true(seulement les favorites)
- Response (200):
{
"songs": [
{
"id": "song123",
"title": "Our Song",
"artist": "Taylor Swift",
"album": "Taylor Swift",
"duration": "3:22",
"reason": "Notre première danse",
"spotifyUrl": "https://open.spotify.com/track/xxx",
"isFavorite": true,
"playCount": 42,
"lastPlayedAt": "2026-02-23T10:00:00.000Z",
"createdAt": "2026-02-01T10:00:00.000Z"
}
]
}- Auth: ✅ Oui
- Description: Récupérer une chanson spécifique
- Auth: ✅ Oui
- Description: Modifier une chanson
- Auth: ✅ Oui
- Description: Toggle le statut favorite d'une chanson
- Response (200):
{
"success": true,
"isFavorite": true
}- Auth: ✅ Oui
- Description: Incrémenter le compteur de lecture
- Response (200):
{
"success": true,
"playCount": 43
}- Auth: ✅ Oui
- Description: Supprimer une chanson
// Valentine Schema
export const valentineSchema = Joi.object({
from: Joi.string().required(),
to: Joi.string().required(),
message: Joi.string().required(),
status: Joi.string().valid('pending', 'accepted', 'rejected').default('pending')
});
// Login Schema
export const loginSchema = Joi.object({
password: Joi.string().required().min(6)
});
// Message Schema
export const messageSchema = Joi.object({
title: Joi.string().required(),
category: Joi.string().valid('triste', 'manque', 'fachee', 'rire', 'doute', 'motivation', 'special').required(),
content: Joi.string().required(),
lockedUntil: Joi.number().optional().allow(null),
backgroundColor: Joi.string().optional()
});
// Planning Schema
export const planningSchema = Joi.object({
title: Joi.string().required(),
description: Joi.string().allow('').optional(),
date: Joi.string().isoDate().required(),
location: Joi.string().allow('').optional(),
category: Joi.string().valid('cinema', 'restaurant', 'voyage', 'sport', 'culture', 'autre').default('autre'),
status: Joi.string().valid('planned', 'done', 'cancelled').default('planned')
});
// Coupon Schema
export const couponSchema = Joi.object({
title: Joi.string().required(),
description: Joi.string().allow('').optional(),
icon: Joi.string().optional(),
expirationDate: Joi.string().isoDate().optional().allow(null, '')
});
// Quiz Question Schema
export const quizSchema = Joi.object({
question: Joi.string().required(),
options: Joi.array().items(Joi.string()).length(4).required(),
correctAnswer: Joi.string().required(),
difficulty: Joi.string().valid('easy', 'medium', 'hard').default('medium')
});
// Quiz Answer Schema
export const answerSchema = Joi.object({
questionId: Joi.string().required(),
answer: Joi.string().required(),
isCorrect: Joi.boolean().required()
});
// Playlist Song Schema
export const playlistSchema = Joi.object({
title: Joi.string().required(),
artist: Joi.string().required(),
album: Joi.string().allow('').optional(),
duration: Joi.string().pattern(/^\d+:\d{2}$/).optional(),
reason: Joi.string().allow('').optional(),
spotifyUrl: Joi.string().uri().optional().allow(''),
isFavorite: Joi.boolean().default(false)
});export const validate = (schema) => {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false,
stripUnknown: true
});
if (error) {
const errors = error.details.map(detail => detail.message);
return res.status(400).json({
error: 'Validation échouée',
details: errors
});
}
req.body = value;
next();
};
};import jwt from 'jsonwebtoken';
import { tokenBlacklist } from '../utils/tokenBlacklist.js';
export const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token d\'authentification manquant' });
}
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token révoqué' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expiré' });
}
return res.status(403).json({ error: 'Token invalide' });
}
};const errorHandler = (err, req, res, next) => {
// Log l'erreur
logger.error('Erreur serveur', {
error: err.message,
stack: err.stack,
method: req.method,
path: req.path,
ip: req.ip
});
// Erreur de validation
if (err.name === 'ValidationError') {
return res.status(400).json({
error: 'Validation échouée',
details: err.details
});
}
// Erreur JWT
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Token invalide' });
}
// Erreur générique
res.status(err.status || 500).json({
error: err.message || 'Erreur interne du serveur',
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
});
};
export default errorHandler;{
id: "auto-generated",
from: "Prince",
to: "Princess",
message: "Veux-tu être ma Valentine ?",
status: "pending" | "accepted" | "rejected",
createdAt: Timestamp,
updatedAt: Timestamp
}{
id: "auto-generated",
title: "Ouvre quand tu es triste",
category: "triste" | "manque" | "fachee" | "rire" | "doute" | "motivation" | "special",
content: "Mon amour, je suis là pour toi...",
lockedUntil: 1740000000000 | null, // Timestamp en millisecondes
backgroundColor: "#FFE5E5",
createdAt: Timestamp
}{
id: "auto-generated",
title: "Massage VIP",
description: "30 min de massage relaxant",
icon: "💆♀️",
isRedeemed: false,
expirationDate: "2026-12-31" | null,
createdAt: Timestamp,
redeemedAt: Timestamp | null
}{
id: "auto-generated",
title: "Cinéma",
description: "Voir le dernier Marvel",
date: "2026-03-15T19:00:00.000Z",
location: "Cinéma Gaumont",
category: "cinema" | "restaurant" | "voyage" | "sport" | "culture" | "autre",
status: "planned" | "done" | "cancelled",
createdAt: Timestamp
}{
id: "auto-generated",
question: "Quelle est ma couleur préférée ?",
options: ["Rose", "Bleu", "Vert", "Rouge"],
correctAnswer: "Rose",
difficulty: "easy" | "medium" | "hard",
createdAt: Timestamp
}{
id: "auto-generated",
questionId: "q123",
answer: "Rose",
isCorrect: true,
answeredAt: Timestamp
}{
id: "auto-generated",
title: "Our Song",
artist: "Taylor Swift",
album: "Taylor Swift",
duration: "3:22",
reason: "Notre première danse",
spotifyUrl: "https://open.spotify.com/track/xxx",
isFavorite: true,
playCount: 42,
lastPlayedAt: Timestamp,
createdAt: Timestamp
}import { getDb } from './config/firebase.js';
import admin from './config/firebase.js';
const db = getDb();
// CREATE
const docRef = await db.collection('coupons').add({
title: "Massage",
createdAt: admin.firestore.FieldValue.serverTimestamp()
});
console.log('Created with ID:', docRef.id);
// READ ALL
const snapshot = await db.collection('coupons').get();
snapshot.forEach(doc => {
console.log(doc.id, doc.data());
});
// READ ONE
const doc = await db.collection('coupons').doc('abc123').get();
if (doc.exists) {
console.log(doc.data());
}
// UPDATE
await db.collection('coupons').doc('abc123').update({
isRedeemed: true,
redeemedAt: admin.firestore.FieldValue.serverTimestamp()
});
// DELETE
await db.collection('coupons').doc('abc123').delete();
// QUERY
const querySnapshot = await db.collection('coupons')
.where('isRedeemed', '==', false)
.orderBy('createdAt', 'desc')
.limit(10)
.get();Tous les 5 seed scripts suivent le même pattern depuis les corrections :
- ✅ Connexion directe à Firebase (pas de HTTP)
- ✅ Ajout de
createdAt: admin.firestore.FieldValue.serverTimestamp() - ✅ Pas besoin du backend lancé
- ✅ Logs clairs avec émojis
import admin from './src/config/firebase.js';
import { getDb } from './src/config/firebase.js';
const db = getDb();
const coupons = [
{
title: "Massage VIP",
description: "Valable pour 30 min de massage relaxant (dos ou pieds au choix).",
icon: "💆♀️",
expirationDate: null,
isRedeemed: false,
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
// ... 5 autres coupons
];
async function seedCoupons() {
console.log('🎟️ Création des coupons...');
for (const coupon of coupons) {
await db.collection('coupons').add(coupon);
console.log(`✅ 🎁 ${coupon.title}`);
}
console.log('🎉 Tous les coupons ont été créés avec succès !');
process.exit(0);
}
seedCoupons();// 6 messages déverrouillés + 1 message secret verrouillé
const messages = [
{
title: "Ouvre quand tu es triste 💙",
category: "triste",
content: "Mon amour...",
lockedUntil: null,
backgroundColor: "#E3F2FD",
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
// ... 5 autres messages déverrouillés
{
title: "Message Secret 🎁",
category: "special",
content: "Ce message sera déverrouillé le 14 février 2027 💖",
lockedUntil: new Date('2027-02-14').getTime(),
backgroundColor: "#F3E5F5",
createdAt: admin.firestore.FieldValue.serverTimestamp()
}
];const events = [
{
title: "Ciné Date Night 🎬",
description: "On va voir le dernier film que tu veux !",
date: new Date('2026-03-15T19:00:00').toISOString(),
location: "Cinéma Gaumont",
category: "cinema",
status: "planned",
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
// ... 4 autres événements
];const questions = [
{
question: "Quelle est ma couleur préférée ?",
options: ["Rose", "Bleu", "Vert", "Rouge"],
correctAnswer: "Rose",
difficulty: "easy",
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
// ... 7 autres questions
];const songs = [
{
title: "Perfect",
artist: "Ed Sheeran",
album: "÷ (Divide)",
duration: "4:23",
reason: "Notre chanson, celle qui nous fait danser 💕",
spotifyUrl: "https://open.spotify.com/track/0tgVpDi06FyKpA1z0VMD4v",
isFavorite: true,
playCount: 0,
lastPlayedAt: null,
createdAt: admin.firestore.FieldValue.serverTimestamp()
},
// ... 6 autres chansons
];cd backend
# Exécuter individuellement
node seed-coupons.js
node seed-messages.js
node seed-planning.js
node seed-quiz.js
node seed-playlist.js
# Ou exécuter tous d'un coup (PowerShell)
"seed-coupons", "seed-messages", "seed-planning", "seed-quiz", "seed-playlist" | ForEach-Object { node "$_.js" }import helmet from 'helmet';
app.use(helmet());
// Ajoute automatiquement:
// - X-DNS-Prefetch-Control
// - X-Frame-Options: DENY
// - Strict-Transport-Security
// - X-Content-Type-Options: nosniff
// - X-XSS-Protectionconst allowedOrigins = [
'http://localhost:1308',
process.env.FRONTEND_URL,
'https://princess-project-chi.vercel.app'
].filter(Boolean);
const corsOptions = {
origin: (origin, callback) => {
if (!origin) return callback(null, true);
if (origin.includes('vercel.app')) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Non autorisé par CORS'));
}
},
credentials: true
};
app.use(cors(corsOptions));import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requêtes max
message: 'Trop de requêtes, réessayez plus tard',
standardHeaders: true,
legacyHeaders: false
});
app.use('/api', limiter);app.set('trust proxy', 1);
// Important pour Railway, Heroku, Vercel
// Permet d'obtenir la vraie IP du clientapp.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));// src/utils/tokenBlacklist.js
export const tokenBlacklist = new Set();
// Lors du logout
tokenBlacklist.add(token);
// Vérification dans authenticateToken
if (tokenBlacklist.has(token)) {
return res.status(401).json({ error: 'Token révoqué' });
}import winston from 'winston';
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
),
defaultMeta: { service: 'princess-project-backend' },
transports: [
// Fichier d'erreurs
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
// Fichier combiné
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// Logs console en développement
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;import logger from '../utils/logger.js';
// Info
logger.info('Coupon créé', {
id: docRef.id,
title: 'Massage VIP',
ip: req.ip
});
// Erreur
logger.error('Erreur création coupon', {
error: error.message,
stack: error.stack,
ip: req.ip
});
// Debug
logger.debug('Requête reçue', {
method: req.method,
path: req.path
});{
"scripts": {
"start": "node src/index.js", // Production
"dev": "nodemon src/index.js", // Dev avec auto-reload
"test": "echo \"No tests yet\"", // Placeholder
"test:api": "node test-api.js", // Tests endpoints
"lint": "eslint src/" // Linting ESLint
}
}# Développement (avec nodemon)
npm run dev
# Production
npm start
# Linting
npm run lint
# Tests API
npm run test:apiPORT=2106
APP_PASSWORD=your_password
JWT_SECRET=your_jwt_secret_32_chars_minimum
FIREBASE_PROJECT_ID=princess-project-210622
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
FIREBASE_CLIENT_EMAIL=firebase-adminsdk@...
FRONTEND_URL=https://your-frontend-url.vercel.app
NODE_ENV=production- Railway: Excellent pour Node.js + Firebase
- Render: Alternative gratuite
- Heroku: Option classique
- Vercel: Possible avec serverless functions
- Variables d'environnement configurées
- Clé Firebase Service Account ajoutée
- FRONTEND_URL mis à jour
- JWT_SECRET fort (32+ caractères)
- NODE_ENV=production
- Seed scripts exécutés
- Logs configurés
- CORS configuré pour production
URL locale: http://localhost:2106/api-docs
Documentation OpenAPI 3.0 complète avec tous les endpoints, schémas, exemples de requêtes/réponses.
Interface interactive permettant de tester les endpoints directement depuis le navigateur.
- Architecture: MVC modulaire avec 8 modules de routes
- Authentification: JWT avec blacklist, durée 7 jours
- Base de données: Firebase Firestore, 7 collections
- Validation: Joi schemas pour toutes les entrées
- Sécurité: Helmet + CORS + Rate Limiting + Trust Proxy
- Total routes: 42 endpoints (41 protégés + 1 publique)
- Seed data: 33 items au total (6+7+5+8+7)
- Logging: Winston pour tous les événements
- Documentation: Swagger UI à
/api-docs - Pattern uniforme: Tous les seed scripts utilisent Firebase direct
- Health Check:
GET http://localhost:2106/api/health - Swagger Docs:
http://localhost:2106/api-docs - Firebase Console:
https://console.firebase.google.com/project/princess-project-210622
Date de dernière mise à jour: 23 février 2026
Auteur: The Prince
Version: 2.0.0