Files
noteflow/server.js
T
Claude 3677bdfe70 Feat: Système de migrations automatiques au démarrage
Implémente un système de migrations automatiques qui s'exécute à chaque
démarrage de l'application Docker/Node.

Problème résolu :
- Les utilisateurs n'ont plus besoin d'exécuter manuellement les migrations
- Les nouveaux champs SQL sont ajoutés automatiquement
- Évite les erreurs "column does not exist"
- Garantit la cohérence entre le code et le schéma DB

Fonctionnement :
1. Au démarrage : initDatabase() → autoMigrate() → startSchedulers()
2. autoMigrate() vérifie chaque colonne via information_schema
3. Si la colonne n'existe pas, elle est ajoutée automatiquement
4. Les triggers et index sont également créés/mis à jour

Migrations incluses :
- Migration 1 : Champs de tracking (archived_at, completed_at)
  * notes.archived_at
  * global_todos.completed_at
  * note_todos.completed_at + created_at
  * Triggers automatiques

- Migration 2 : Champ priority pour tâches
  * global_todos.priority
  * note_todos.priority
  * Index de performance

Caractéristiques :
- ✅ Idempotent : Peut s'exécuter plusieurs fois sans problème
- ✅ Transactionnel : Utilise BEGIN/COMMIT/ROLLBACK
- ✅ Non-bloquant : L'application démarre même en cas d'erreur
- ✅ Intelligent : Vérifie avant de créer
- ✅ Sécurisé : Double protection avec rétrocompatibilité API
- ✅ Documenté : Guide complet dans docs/MIGRATIONS.md

Fichiers créés :
- scripts/auto-migrate.js - Script de migrations automatiques
- docs/MIGRATIONS.md - Documentation complète

Fichiers modifiés :
- server.js - Appel de autoMigrate() au démarrage

Logs au démarrage :
✓ Base de données initialisée avec succès
🔄 Vérification des migrations...
  → Ajout du champ priority à global_todos (si nécessaire)
✅ Migrations automatiques terminées avec succès
✓ Migrations automatiques appliquées

Impact :
Les utilisateurs peuvent maintenant simplement démarrer l'application avec
docker-compose up et tous les champs SQL seront automatiquement créés.
Plus besoin de lancer des scripts de migration manuellement !
2025-11-21 14:19:33 +00:00

185 lines
6.2 KiB
JavaScript

// Serveur Express principal pour l'application NoteFlow
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const path = require('path');
const fs = require('fs');
const logger = require('./config/logger');
const { initDatabase } = require('./config/database');
// Créer l'application Express
const app = express();
const PORT = process.env.PORT || 2222;
// Configurer Express pour faire confiance au proxy (nginx, etc.)
// Utiliser 1 pour indiquer qu'il y a 1 proxy devant (nginx/reverse proxy)
// Plus sécurisé que 'true' qui accepterait n'importe quel proxy
app.set('trust proxy', 1);
// Créer les dossiers nécessaires (silencieusement, ils peuvent déjà exister via les volumes Docker)
const dataDir = path.join(__dirname, 'data');
const uploadsDir = path.join(__dirname, 'public', 'uploads');
[dataDir, uploadsDir].forEach(dir => {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
logger.info(`Dossier créé: ${dir}`);
}
} catch (error) {
// Ignorer les erreurs si les dossiers existent déjà ou sont montés via Docker
logger.debug(`Dossier déjà existant: ${dir}`);
}
});
// Configuration de la sécurité avec Helmet
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
fontSrc: ["'self'"],
connectSrc: ["'self'"]
}
}
}));
// CORS - À ajuster selon vos besoins
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
credentials: true
}));
// Body parsers
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
// Rate limiting sur les endpoints d'authentification
const authLimiter = rateLimit({
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes par défaut
max: parseInt(process.env.RATE_LIMIT_MAX) || 100, // 100 requêtes par fenêtre
message: { error: 'Trop de tentatives, veuillez réessayer plus tard.' },
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/auth', authLimiter);
// Servir les fichiers statiques
// Servir les uploads depuis public/uploads
app.use('/uploads', express.static(uploadsDir));
// Servir l'application React depuis dist/
app.use(express.static(path.join(__dirname, 'dist')));
// Healthcheck endpoint pour Docker
app.get('/health', (req, res) => {
res.status(200).json({
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});
// Routes API
app.use('/api/auth', require('./routes/auth.routes'));
app.use('/api/users', require('./routes/users.routes'));
app.use('/api/notes', require('./routes/notes.routes'));
app.use('/api/todos', require('./routes/todos.routes'));
app.use('/api/settings', require('./routes/settings.routes'));
app.use('/api/rss', require('./routes/rss.routes'));
app.use('/api/calendar', require('./routes/calendar.routes'));
app.use('/api/openrouter', require('./routes/openrouter.routes'));
app.use('/api/timezone-logs', require('./routes/timezone-logs.routes'));
app.use('/api/admin', require('./routes/admin.routes'));
// Route de recherche
app.get('/api/search', require('./routes/notes.routes').searchNotes);
// SPA fallback - Servir index.html pour toutes les autres routes
app.get('*', (req, res) => {
// Ne pas servir index.html pour les requêtes d'API
if (req.path.startsWith('/api/')) {
return res.status(404).json({ error: 'Endpoint non trouvé' });
}
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
// Gestionnaire d'erreurs global
app.use((err, req, res, next) => {
logger.error('Erreur non gérée:', {
error: err.message,
stack: err.stack,
path: req.path,
method: req.method
});
// Ne pas exposer les détails de l'erreur en production
const errorMessage = process.env.NODE_ENV === 'production'
? 'Une erreur est survenue'
: err.message;
res.status(err.status || 500).json({
error: errorMessage
});
});
// Initialiser la base de données et démarrer le serveur
async function startServer() {
try {
// Initialiser la base de données
await initDatabase();
logger.info('✓ Base de données initialisée avec succès');
// Exécuter les migrations automatiques
const { autoMigrate } = require('./scripts/auto-migrate');
await autoMigrate();
logger.info('✓ Migrations automatiques appliquées');
// Démarrer le scheduler RSS
const rssScheduler = require('./services/rss-scheduler');
rssScheduler.startScheduler();
logger.info('✓ Scheduler RSS démarré');
// Démarrer le scheduler de purge automatique
const cleanupScheduler = require('./services/cleanup-scheduler');
cleanupScheduler.startScheduler();
logger.info('✓ Scheduler de purge automatique démarré');
// Démarrer le serveur
app.listen(PORT, '0.0.0.0', () => {
logger.info('═════════════════════════════════════════════');
logger.info(`✓ Serveur NoteFlow démarré sur le port ${PORT}`);
logger.info(`✓ Environnement: ${process.env.NODE_ENV || 'development'}`);
logger.info(`✓ URL: http://localhost:${PORT}`);
logger.info('═════════════════════════════════════════════');
if (process.env.NODE_ENV !== 'production') {
logger.info('Credentials par défaut:');
logger.info(' Username: admin');
logger.info(' Password: admin');
logger.info('═════════════════════════════════════════════');
}
});
} catch (error) {
logger.error('Erreur lors du démarrage du serveur:', error);
process.exit(1);
}
}
// Gestion propre de l'arrêt
process.on('SIGTERM', () => {
logger.info('Signal SIGTERM reçu, arrêt du serveur...');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('Signal SIGINT reçu, arrêt du serveur...');
process.exit(0);
});
// Démarrer l'application
startServer();