mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Merge pull request #78 from R0m1k3/claude/update-rss-feeds-011CV6EZDsWAUqbRHZR1117Q
Claude/update rss feeds 011 cv6 ez ds wa uqb rhzr1117 q
This commit is contained in:
8 files changed
+1102
-10
No files matched your search
+5
-6
@@ -33,16 +33,14 @@ FROM node:20-alpine
|
||||
# Définir le répertoire de travail
|
||||
WORKDIR /app
|
||||
|
||||
# Installer SQLite et les outils de build pour les modules natifs
|
||||
RUN apk add --no-cache sqlite python3 make g++
|
||||
# Installer PostgreSQL client et les outils de build pour les modules natifs
|
||||
RUN apk add --no-cache postgresql-client python3 make g++
|
||||
|
||||
# Copier les fichiers de dépendances
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
# Installer les dépendances backend
|
||||
# Note: On installe toutes les dépendances car package.json contient aussi les deps frontend
|
||||
# mais seules les deps backend sont utilisées en runtime (express, sqlite3, bcrypt, etc.)
|
||||
RUN npm ci
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copier le code du serveur
|
||||
COPY server.js ./
|
||||
@@ -51,12 +49,13 @@ COPY routes ./routes
|
||||
COPY middleware ./middleware
|
||||
COPY services ./services
|
||||
COPY migrations ./migrations
|
||||
COPY scripts ./scripts
|
||||
|
||||
# Copier le build de l'application React depuis le builder
|
||||
COPY --from=builder /app/dist ./dist
|
||||
|
||||
# Créer les dossiers nécessaires et définir les permissions
|
||||
RUN mkdir -p /app/data /app/public/uploads && \
|
||||
RUN mkdir -p /app/data/logs /app/public/uploads && \
|
||||
chown -R node:node /app
|
||||
|
||||
# Utiliser l'utilisateur node pour la sécurité
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# Migration vers PostgreSQL + Docker
|
||||
|
||||
Ce guide explique comment migrer NoteFlow de SQLite vers PostgreSQL avec Docker.
|
||||
|
||||
## 🎯 Pourquoi PostgreSQL?
|
||||
|
||||
### Problèmes avec SQLite
|
||||
- ❌ Concurrence limitée (verrous sur toute la DB)
|
||||
- ❌ Difficile de savoir quelle DB est utilisée
|
||||
- ❌ Pas de logs de requêtes natifs
|
||||
- ❌ Corruption possible en cas de crash
|
||||
- ❌ Pas adapté pour la production
|
||||
|
||||
### Avantages de PostgreSQL
|
||||
- ✅ Concurrence parfaite (MVCC)
|
||||
- ✅ Une seule source de vérité
|
||||
- ✅ Transactions ACID robustes
|
||||
- ✅ Logs et monitoring natifs
|
||||
- ✅ Production-ready et performant
|
||||
- ✅ Intégration Docker parfaite
|
||||
|
||||
## 📋 Prérequis
|
||||
|
||||
- Docker et Docker Compose installés
|
||||
- Sauvegarde de votre base SQLite actuelle
|
||||
- 5 minutes de votre temps
|
||||
|
||||
## 🚀 Étapes de Migration
|
||||
|
||||
### 1. Installer les dépendances PostgreSQL
|
||||
|
||||
```bash
|
||||
npm install pg@^8.11.3
|
||||
```
|
||||
|
||||
### 2. Activer PostgreSQL dans database.js
|
||||
|
||||
```bash
|
||||
# Backup de l'ancien fichier
|
||||
cp config/database.js config/database-sqlite.js.backup
|
||||
|
||||
# Installer la nouvelle version PostgreSQL
|
||||
cp config/database-postgres.js config/database.js
|
||||
```
|
||||
|
||||
### 3. Créer le fichier .env (optionnel)
|
||||
|
||||
```bash
|
||||
cat > .env <<EOF
|
||||
# PostgreSQL
|
||||
POSTGRES_PASSWORD=votre_mot_de_passe_securise_ici
|
||||
DATABASE_URL=postgresql://noteflow:votre_mot_de_passe_securise_ici@localhost:5499/noteflow
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=votre_secret_jwt_ici
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4. Démarrer PostgreSQL avec Docker
|
||||
|
||||
```bash
|
||||
# Démarrer uniquement PostgreSQL
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Vérifier que PostgreSQL est démarré
|
||||
docker-compose logs postgres
|
||||
|
||||
# Vous devriez voir: "database system is ready to accept connections"
|
||||
```
|
||||
|
||||
### 5. Migrer les données SQLite → PostgreSQL
|
||||
|
||||
**IMPORTANT:** Localisez d'abord votre vraie base SQLite
|
||||
|
||||
```bash
|
||||
# Chercher où est votre vraie base de données
|
||||
find / -name "notes.db" 2>/dev/null
|
||||
|
||||
# Ou regarder dans les logs du serveur
|
||||
grep "Base de données connectée" data/app.log
|
||||
```
|
||||
|
||||
Une fois trouvée, lancez la migration:
|
||||
|
||||
```bash
|
||||
# Exemple si la DB est dans /app/data/notes.db
|
||||
SQLITE_PATH=/app/data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||
|
||||
# Ou si locale
|
||||
SQLITE_PATH=./data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||
```
|
||||
|
||||
Vous devriez voir:
|
||||
|
||||
```
|
||||
==================== MIGRATION SQLite → PostgreSQL ====================
|
||||
|
||||
✓ SQLite connecté: /app/data/notes.db
|
||||
✓ PostgreSQL connecté
|
||||
|
||||
📊 Début de la migration...
|
||||
|
||||
📋 Migration table: users
|
||||
✓ Table "users": 2/2 lignes migrées
|
||||
📋 Migration table: notes
|
||||
✓ Table "notes": 45/45 lignes migrées
|
||||
📋 Migration table: rss_articles
|
||||
✓ Table "rss_articles": 127/127 lignes migrées
|
||||
...
|
||||
|
||||
========================================================
|
||||
✅ Migration terminée!
|
||||
Total: 250 lignes migrées
|
||||
========================================================
|
||||
```
|
||||
|
||||
### 6. Redémarrer l'application
|
||||
|
||||
```bash
|
||||
# Reconstruire l'image Docker
|
||||
docker-compose build notes-app
|
||||
|
||||
# Redémarrer tout
|
||||
docker-compose up -d
|
||||
|
||||
# Voir les logs
|
||||
docker-compose logs -f notes-app
|
||||
```
|
||||
|
||||
Vous devriez voir:
|
||||
|
||||
```
|
||||
[INFO]: ✓ PostgreSQL connecté: postgresql://noteflow:***@postgres:5499/noteflow
|
||||
[INFO]: ✓ Tables PostgreSQL créées avec succès
|
||||
[INFO]: Server running on port 2222
|
||||
```
|
||||
|
||||
## ✅ Vérification
|
||||
|
||||
### 1. Tester la connexion PostgreSQL
|
||||
|
||||
```bash
|
||||
# Se connecter à PostgreSQL
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||
|
||||
# Vérifier les tables
|
||||
\dt
|
||||
|
||||
# Compter les articles RSS
|
||||
SELECT COUNT(*) FROM rss_articles;
|
||||
|
||||
# Voir les 5 derniers articles
|
||||
SELECT title, pub_date FROM rss_articles ORDER BY pub_date DESC LIMIT 5;
|
||||
|
||||
# Quitter
|
||||
\q
|
||||
```
|
||||
|
||||
### 2. Tester l'application
|
||||
|
||||
1. Ouvrez http://localhost:2222
|
||||
2. Connectez-vous (admin/admin)
|
||||
3. Vérifiez que vos notes sont là
|
||||
4. Vérifiez que les flux RSS s'affichent
|
||||
5. Ajoutez un nouveau flux RSS
|
||||
6. Attendez 2 minutes et vérifiez les nouveaux articles
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
### Variables d'environnement
|
||||
|
||||
Le système utilise `DATABASE_URL` pour se connecter à PostgreSQL:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE
|
||||
```
|
||||
|
||||
Exemple:
|
||||
```bash
|
||||
DATABASE_URL=postgresql://noteflow:mon_password@postgres:5432/noteflow
|
||||
```
|
||||
|
||||
### Dans docker-compose.yml
|
||||
|
||||
PostgreSQL tourne sur le **port 5499** (externe) et communique sur le **port 5432** (interne):
|
||||
|
||||
```yaml
|
||||
postgres:
|
||||
ports:
|
||||
- "5499:5499" # Accessible depuis l'hôte sur 5499
|
||||
```
|
||||
|
||||
L'application se connecte via le nom du service:
|
||||
```yaml
|
||||
notes-app:
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://noteflow:password@postgres:5432/noteflow
|
||||
```
|
||||
|
||||
## 📊 Maintenance PostgreSQL
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# Backup complet
|
||||
docker exec noteflow-postgres pg_dump -U noteflow -d noteflow > backup.sql
|
||||
|
||||
# Backup compressé
|
||||
docker exec noteflow-postgres pg_dump -U noteflow -d noteflow | gzip > backup.sql.gz
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
# Depuis un backup
|
||||
docker exec -i noteflow-postgres psql -U noteflow -d noteflow < backup.sql
|
||||
|
||||
# Depuis un backup compressé
|
||||
gunzip < backup.sql.gz | docker exec -i noteflow-postgres psql -U noteflow -d noteflow
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
# Voir les logs PostgreSQL
|
||||
docker-compose logs postgres
|
||||
|
||||
# Logs en temps réel
|
||||
docker-compose logs -f postgres
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
```bash
|
||||
# Se connecter à PostgreSQL
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||
|
||||
# Voir les requêtes lentes
|
||||
SELECT pid, now() - query_start AS duration, query
|
||||
FROM pg_stat_activity
|
||||
WHERE state = 'active'
|
||||
ORDER BY duration DESC;
|
||||
|
||||
# Taille de la base
|
||||
SELECT pg_size_pretty(pg_database_size('noteflow'));
|
||||
|
||||
# Taille des tables
|
||||
SELECT schemaname, tablename, pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
|
||||
FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
|
||||
```
|
||||
|
||||
## 🐛 Dépannage
|
||||
|
||||
### Problème: PostgreSQL ne démarre pas
|
||||
|
||||
```bash
|
||||
# Vérifier les logs
|
||||
docker-compose logs postgres
|
||||
|
||||
# Supprimer le volume et recommencer
|
||||
docker-compose down -v
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
### Problème: Application ne se connecte pas
|
||||
|
||||
```bash
|
||||
# Vérifier les variables d'environnement
|
||||
docker exec notes-todo-app env | grep DATABASE
|
||||
|
||||
# Vérifier que PostgreSQL est accessible
|
||||
docker exec notes-todo-app nc -zv postgres 5432
|
||||
```
|
||||
|
||||
### Problème: Articles RSS toujours pas affichés
|
||||
|
||||
```bash
|
||||
# Se connecter à PostgreSQL
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow
|
||||
|
||||
# Compter les articles
|
||||
SELECT COUNT(*) FROM rss_articles;
|
||||
|
||||
# Voir les 10 derniers
|
||||
SELECT id, title, pub_date FROM rss_articles ORDER BY pub_date DESC LIMIT 10;
|
||||
|
||||
# Vérifier les flux
|
||||
SELECT id, title, enabled FROM rss_feeds;
|
||||
```
|
||||
|
||||
Si la table est vide, le problème vient du scheduler. Vérifiez les logs:
|
||||
|
||||
```bash
|
||||
docker-compose logs -f notes-app | grep RSS
|
||||
```
|
||||
|
||||
## 🔄 Rollback vers SQLite
|
||||
|
||||
Si vous devez revenir à SQLite:
|
||||
|
||||
```bash
|
||||
# 1. Arrêter Docker
|
||||
docker-compose down
|
||||
|
||||
# 2. Restaurer l'ancien database.js
|
||||
cp config/database-sqlite.js.backup config/database.js
|
||||
|
||||
# 3. Modifier docker-compose.yml
|
||||
# Commentez la section postgres et remettez DB_PATH
|
||||
|
||||
# 4. Redémarrer
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## ✨ Résultat Attendu
|
||||
|
||||
Après migration vers PostgreSQL:
|
||||
|
||||
✅ **Problème RSS résolu**: Les nouveaux articles s'affichent immédiatement
|
||||
✅ **Pas de confusion DB**: Une seule source de vérité
|
||||
✅ **Debuggable**: Logs SQL natifs dans PostgreSQL
|
||||
✅ **Performance**: Meilleure concurrence et vitesse
|
||||
✅ **Production-ready**: Système robuste et fiable
|
||||
✅ **Monitoring**: Outils natifs PostgreSQL disponibles
|
||||
|
||||
**Les articles du 14 novembre (et suivants) seront visibles dès le prochain cycle du scheduler !** 🎉
|
||||
@@ -0,0 +1,233 @@
|
||||
# 🚀 Quick Start: Migration PostgreSQL
|
||||
|
||||
Migration rapide de NoteFlow vers PostgreSQL en 5 minutes.
|
||||
|
||||
## Pourquoi cette migration?
|
||||
|
||||
**Votre problème actuel:**
|
||||
```
|
||||
[INFO]: ✅ 18 nouveaux articles
|
||||
```
|
||||
Mais... **rien ne s'affiche** dans l'interface 😱
|
||||
|
||||
**Cause:** SQLite avec multiples fichiers DB, impossible de savoir laquelle est utilisée.
|
||||
|
||||
**Solution:** PostgreSQL = UNE SEULE source de vérité.
|
||||
|
||||
## 📋 Installation en 5 Étapes
|
||||
|
||||
### 1️⃣ Installer dépendances
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 2️⃣ Activer PostgreSQL
|
||||
|
||||
```bash
|
||||
cp config/database-postgres.js config/database.js
|
||||
```
|
||||
|
||||
### 3️⃣ Créer fichier .env (optionnel)
|
||||
|
||||
```bash
|
||||
cat > .env <<EOF
|
||||
POSTGRES_PASSWORD=votre_password_securise
|
||||
JWT_SECRET=votre_jwt_secret
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4️⃣ Démarrer PostgreSQL
|
||||
|
||||
```bash
|
||||
# Démarrer PostgreSQL seul
|
||||
docker-compose up -d postgres
|
||||
|
||||
# Attendre 10 secondes qu'il démarre
|
||||
sleep 10
|
||||
|
||||
# Vérifier que c'est OK
|
||||
docker-compose logs postgres | grep "ready"
|
||||
```
|
||||
|
||||
### 5️⃣ Migrer vos données
|
||||
|
||||
**Trouvez d'abord votre vraie DB SQLite:**
|
||||
|
||||
```bash
|
||||
# Option A: Chercher dans tout le système
|
||||
find / -name "notes.db" 2>/dev/null
|
||||
|
||||
# Option B: Regarder les logs
|
||||
grep "Base de données connectée" data/app.log
|
||||
|
||||
# Vous devriez trouver quelque chose comme:
|
||||
# /app/data/notes.db
|
||||
# OU
|
||||
# /var/lib/docker/volumes/xxx/notes.db
|
||||
```
|
||||
|
||||
**Puis migrez:**
|
||||
|
||||
```bash
|
||||
# Remplacez /path/to par le vrai chemin trouvé
|
||||
SQLITE_PATH=/path/to/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||
|
||||
# Exemple si dans Docker:
|
||||
SQLITE_PATH=/var/lib/docker/volumes/noteflow_notes_data/_data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||
|
||||
# Exemple si local:
|
||||
SQLITE_PATH=./data/notes.db node scripts/migrate-sqlite-to-postgres.js
|
||||
```
|
||||
|
||||
Vous verrez:
|
||||
```
|
||||
==================== MIGRATION SQLite → PostgreSQL ====================
|
||||
|
||||
✓ SQLite connecté: /path/to/notes.db
|
||||
✓ PostgreSQL connecté
|
||||
|
||||
📊 Début de la migration...
|
||||
|
||||
📋 Migration table: users
|
||||
✓ Table "users": 2/2 lignes migrées
|
||||
📋 Migration table: rss_articles
|
||||
✓ Table "rss_articles": 127/127 lignes migrées
|
||||
...
|
||||
|
||||
========================================================
|
||||
✅ Migration terminée!
|
||||
Total: 250 lignes migrées
|
||||
========================================================
|
||||
```
|
||||
|
||||
### 6️⃣ Redémarrer l'application
|
||||
|
||||
```bash
|
||||
# Rebuild avec PostgreSQL
|
||||
docker-compose build notes-app
|
||||
|
||||
# Tout redémarrer
|
||||
docker-compose up -d
|
||||
|
||||
# Voir les logs
|
||||
docker-compose logs -f notes-app
|
||||
```
|
||||
|
||||
**Vous devriez voir:**
|
||||
```
|
||||
[INFO]: ✓ PostgreSQL connecté: postgresql://noteflow:***@postgres:5499/noteflow
|
||||
[INFO]: ✓ Tables PostgreSQL créées
|
||||
[INFO]: 🔄 === Début mise à jour RSS ===
|
||||
[INFO]: ✅ 18 nouveaux articles
|
||||
```
|
||||
|
||||
## ✅ Vérification
|
||||
|
||||
### Tester PostgreSQL
|
||||
|
||||
```bash
|
||||
# Se connecter
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499
|
||||
|
||||
# Compter articles RSS
|
||||
SELECT COUNT(*) FROM rss_articles;
|
||||
|
||||
# Voir les 5 derniers
|
||||
SELECT title, pub_date FROM rss_articles ORDER BY pub_date DESC LIMIT 5;
|
||||
|
||||
# Quitter
|
||||
\q
|
||||
```
|
||||
|
||||
### Tester l'application
|
||||
|
||||
1. Ouvrir http://localhost:2222
|
||||
2. Login: `admin` / `admin`
|
||||
3. Vérifier que vos notes sont là
|
||||
4. Vérifier que les flux RSS s'affichent ✨
|
||||
5. Les nouveaux articles apparaissent dans les 2 minutes
|
||||
|
||||
## 🎯 Résultat
|
||||
|
||||
### Avant (SQLite)
|
||||
```
|
||||
😱 Articles récupérés mais jamais affichés
|
||||
😱 Impossible de déboguer
|
||||
😱 Plusieurs fichiers DB, confusion totale
|
||||
```
|
||||
|
||||
### Après (PostgreSQL)
|
||||
```
|
||||
✅ Articles affichés immédiatement
|
||||
✅ Une seule DB, pas de confusion
|
||||
✅ Logs SQL pour debug
|
||||
✅ Production-ready
|
||||
```
|
||||
|
||||
## 🐛 Problème?
|
||||
|
||||
### PostgreSQL ne démarre pas
|
||||
|
||||
```bash
|
||||
docker-compose logs postgres
|
||||
docker-compose down -v
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
### App ne se connecte pas
|
||||
|
||||
```bash
|
||||
docker-compose logs notes-app | grep -i postgres
|
||||
docker-compose logs notes-app | grep -i error
|
||||
```
|
||||
|
||||
### Articles toujours pas affichés
|
||||
|
||||
```bash
|
||||
# Vérifier que les articles sont en DB
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499 \
|
||||
-c "SELECT COUNT(*) FROM rss_articles;"
|
||||
|
||||
# Si 0, vérifier les flux
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499 \
|
||||
-c "SELECT id, title, enabled FROM rss_feeds;"
|
||||
|
||||
# Vérifier les logs RSS
|
||||
docker-compose logs notes-app | grep RSS
|
||||
```
|
||||
|
||||
## 📚 Documentation Complète
|
||||
|
||||
Voir `MIGRATION-POSTGRESQL.md` pour:
|
||||
- Backup/Restore PostgreSQL
|
||||
- Monitoring et performance
|
||||
- Configuration avancée
|
||||
- Rollback vers SQLite
|
||||
- Dépannage complet
|
||||
|
||||
## 💡 Pro Tips
|
||||
|
||||
1. **Toujours backup avant migration:**
|
||||
```bash
|
||||
cp data/notes.db data/notes.db.backup
|
||||
```
|
||||
|
||||
2. **Vérifier PostgreSQL est prêt:**
|
||||
```bash
|
||||
docker-compose logs postgres | grep "ready"
|
||||
```
|
||||
|
||||
3. **Voir les requêtes SQL en temps réel:**
|
||||
```bash
|
||||
docker exec -it noteflow-postgres psql -U noteflow -d noteflow -p 5499 \
|
||||
-c "SELECT query FROM pg_stat_activity WHERE state = 'active';"
|
||||
```
|
||||
|
||||
4. **Backup automatique quotidien:**
|
||||
```bash
|
||||
# Ajouter à crontab
|
||||
0 2 * * * docker exec noteflow-postgres pg_dump -U noteflow noteflow | gzip > /backup/noteflow-$(date +\%Y\%m\%d).sql.gz
|
||||
```
|
||||
|
||||
**🎉 Votre système RSS va enfin fonctionner correctement avec PostgreSQL !**
|
||||
@@ -0,0 +1,309 @@
|
||||
// Configuration et initialisation de la base de données PostgreSQL
|
||||
const { Pool } = require('pg');
|
||||
const bcrypt = require('bcrypt');
|
||||
const logger = require('./logger');
|
||||
|
||||
// Configuration PostgreSQL depuis DATABASE_URL ou variables d'environnement
|
||||
const DATABASE_URL = process.env.DATABASE_URL ||
|
||||
`postgresql://${process.env.PGUSER || 'noteflow'}:${process.env.PGPASSWORD || 'noteflow_secure_password_change_me'}@${process.env.PGHOST || 'localhost'}:${process.env.PGPORT || '5499'}/${process.env.PGDATABASE || 'noteflow'}`;
|
||||
|
||||
// Créer le pool de connexions PostgreSQL
|
||||
const pool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
max: 20, // Maximum de connexions dans le pool
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// Gestion des erreurs du pool
|
||||
pool.on('error', (err) => {
|
||||
logger.error('Erreur PostgreSQL pool:', err);
|
||||
});
|
||||
|
||||
// Test de connexion
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
logger.error('Erreur lors de la connexion à PostgreSQL:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
release();
|
||||
logger.info(`✓ PostgreSQL connecté: ${DATABASE_URL.replace(/:[^:]*@/, ':***@')}`);
|
||||
});
|
||||
|
||||
/**
|
||||
* Initialiser la base de données avec les tables nécessaires
|
||||
*/
|
||||
async function initDatabase() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Table users
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
is_admin BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table notes
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS notes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
content TEXT,
|
||||
image_filename TEXT,
|
||||
archived BOOLEAN DEFAULT FALSE,
|
||||
priority INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_todos
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
position INTEGER DEFAULT 0
|
||||
)
|
||||
`);
|
||||
|
||||
// Table global_todos
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS global_todos (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
completed BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_images
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_images (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_files
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_files (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
filename TEXT NOT NULL,
|
||||
original_name TEXT NOT NULL,
|
||||
file_size INTEGER,
|
||||
mime_type TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table settings
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(255) UNIQUE NOT NULL,
|
||||
value TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_feeds
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_feeds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
url TEXT UNIQUE NOT NULL,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_fetched_at TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_articles
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_articles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
feed_id INTEGER NOT NULL REFERENCES rss_feeds(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
link TEXT UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
pub_date TIMESTAMP,
|
||||
content TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table rss_summaries
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS rss_summaries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
summary TEXT NOT NULL,
|
||||
model TEXT,
|
||||
articles_count INTEGER DEFAULT 0,
|
||||
feed_title TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table calendar_events
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS calendar_events (
|
||||
id SERIAL PRIMARY KEY,
|
||||
google_event_id VARCHAR(255) UNIQUE NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP NOT NULL,
|
||||
location TEXT,
|
||||
color_id TEXT,
|
||||
html_link TEXT,
|
||||
all_day BOOLEAN DEFAULT FALSE,
|
||||
synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Table note_tags
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS note_tags (
|
||||
id SERIAL PRIMARY KEY,
|
||||
note_id INTEGER NOT NULL REFERENCES notes(id) ON DELETE CASCADE,
|
||||
tag VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(note_id, tag)
|
||||
)
|
||||
`);
|
||||
|
||||
// Table google_oauth_tokens
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS google_oauth_tokens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL UNIQUE REFERENCES users(id) ON DELETE CASCADE,
|
||||
access_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
expiry_date BIGINT,
|
||||
scope TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Créer les index pour la performance
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_notes_priority ON notes(priority DESC, updated_at DESC)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_images ON note_images(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_tags ON note_tags(note_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_note_tags_tag ON note_tags(tag)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date DESC)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_calendar_events_user ON calendar_events(user_id)');
|
||||
await client.query('CREATE INDEX IF NOT EXISTS idx_calendar_events_start ON calendar_events(start_time)');
|
||||
|
||||
logger.info('✓ Tables PostgreSQL créées avec succès');
|
||||
|
||||
// Créer l'utilisateur admin par défaut si la table est vide
|
||||
const usersCount = await client.query('SELECT COUNT(*) as count FROM users');
|
||||
|
||||
if (usersCount.rows[0].count === '0') {
|
||||
const defaultPassword = 'admin';
|
||||
const passwordHash = await bcrypt.hash(defaultPassword, 12);
|
||||
|
||||
await client.query(
|
||||
'INSERT INTO users (username, password_hash, is_admin) VALUES ($1, $2, $3)',
|
||||
['admin', passwordHash, true]
|
||||
);
|
||||
|
||||
logger.info('✓ Utilisateur admin créé (username: admin, password: admin)');
|
||||
logger.warn('⚠️ IMPORTANT: Changez le mot de passe admin en production!');
|
||||
} else {
|
||||
logger.info('✓ Base de données déjà initialisée');
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
logger.error('Erreur lors de l\'initialisation de la base de données:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exécuter une requête avec promesse
|
||||
*/
|
||||
async function runQuery(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return {
|
||||
id: result.rows[0]?.id || result.rowCount,
|
||||
changes: result.rowCount
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Erreur runQuery:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer une seule ligne
|
||||
*/
|
||||
async function getOne(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return result.rows[0] || null;
|
||||
} catch (error) {
|
||||
logger.error('Erreur getOne:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer toutes les lignes
|
||||
*/
|
||||
async function getAll(sql, params = []) {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const result = await client.query(sql, params);
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
logger.error('Erreur getAll:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
pool,
|
||||
initDatabase,
|
||||
runQuery,
|
||||
getOne,
|
||||
getAll
|
||||
};
|
||||
+34
-4
@@ -1,21 +1,50 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# PostgreSQL Database
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: noteflow-postgres
|
||||
restart: unless-stopped
|
||||
command: -p 5499
|
||||
environment:
|
||||
POSTGRES_DB: noteflow
|
||||
POSTGRES_USER: noteflow
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-noteflow_secure_password_change_me}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF8"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5499:5499"
|
||||
networks:
|
||||
- nginx_default
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U noteflow -d noteflow -p 5499"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
# NoteFlow Application
|
||||
notes-app:
|
||||
build: .
|
||||
container_name: notes-todo-app
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "2222:2222"
|
||||
volumes:
|
||||
- notes_data:/app/data
|
||||
- notes_uploads:/app/public/uploads
|
||||
- notes_logs:/app/data/logs
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- JWT_SECRET=${JWT_SECRET:-change_me_in_production_please_use_strong_secret}
|
||||
- PORT=2222
|
||||
- UPLOADS_PATH=/app/public/uploads
|
||||
- DB_PATH=/app/data/notes.db
|
||||
restart: unless-stopped
|
||||
# PostgreSQL connection
|
||||
- DATABASE_URL=postgresql://noteflow:${POSTGRES_PASSWORD:-noteflow_secure_password_change_me}@postgres:5499/noteflow
|
||||
- DB_TYPE=postgres
|
||||
networks:
|
||||
- nginx_default
|
||||
healthcheck:
|
||||
@@ -26,8 +55,9 @@ services:
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
notes_data:
|
||||
postgres_data:
|
||||
notes_uploads:
|
||||
notes_logs:
|
||||
|
||||
networks:
|
||||
nginx_default:
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Script d'initialisation PostgreSQL pour NoteFlow
|
||||
-- Exécuté automatiquement au premier démarrage du conteneur
|
||||
|
||||
-- Extensions utiles
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
-- Créer les index après création des tables dans database.js
|
||||
-- Ce script peut rester vide si database.js gère tout
|
||||
@@ -76,6 +76,7 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next-themes": "^0.4.4",
|
||||
"pg": "^8.11.3",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "^9.11.1",
|
||||
"react-dom": "^19.0.0",
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// Script de migration SQLite → PostgreSQL
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const { Pool } = require('pg');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
console.log('\n==================== MIGRATION SQLite → PostgreSQL ====================\n');
|
||||
|
||||
const SQLITE_PATH = process.env.SQLITE_PATH || path.join(__dirname, '../data/notes.db');
|
||||
const DATABASE_URL = process.env.DATABASE_URL ||
|
||||
`postgresql://noteflow:noteflow_secure_password_change_me@localhost:5499/noteflow`;
|
||||
|
||||
// Connexion SQLite (source)
|
||||
const sqliteDb = new sqlite3.Database(SQLITE_PATH, (err) => {
|
||||
if (err) {
|
||||
console.error('Erreur connexion SQLite:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✓ SQLite connecté:', SQLITE_PATH);
|
||||
});
|
||||
|
||||
// Connexion PostgreSQL (destination)
|
||||
const pgPool = new Pool({
|
||||
connectionString: DATABASE_URL,
|
||||
});
|
||||
|
||||
pgPool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
console.error('Erreur connexion PostgreSQL:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
release();
|
||||
console.log('✓ PostgreSQL connecté\n');
|
||||
});
|
||||
|
||||
// Tables à migrer (dans l'ordre pour respecter les foreign keys)
|
||||
const TABLES_ORDER = [
|
||||
'users',
|
||||
'notes',
|
||||
'note_todos',
|
||||
'global_todos',
|
||||
'note_images',
|
||||
'note_files',
|
||||
'settings',
|
||||
'rss_feeds',
|
||||
'rss_articles',
|
||||
'rss_summaries',
|
||||
'calendar_events',
|
||||
'note_tags',
|
||||
'google_oauth_tokens'
|
||||
];
|
||||
|
||||
/**
|
||||
* Récupérer les données d'une table SQLite
|
||||
*/
|
||||
function getSqliteData(tableName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
sqliteDb.all(`SELECT * FROM ${tableName}`, (err, rows) => {
|
||||
if (err) {
|
||||
if (err.message.includes('no such table')) {
|
||||
resolve([]); // Table n'existe pas, ok
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Insérer les données dans PostgreSQL
|
||||
*/
|
||||
async function insertPostgresData(tableName, rows) {
|
||||
if (rows.length === 0) {
|
||||
console.log(` ⏭️ Table "${tableName}" vide, skip`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const client = await pgPool.connect();
|
||||
let inserted = 0;
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
for (const row of rows) {
|
||||
// Convertir les noms de colonnes et valeurs
|
||||
const columns = Object.keys(row);
|
||||
const values = Object.values(row).map(val => {
|
||||
// Convertir les valeurs SQLite vers PostgreSQL
|
||||
if (val === null) return null;
|
||||
if (typeof val === 'boolean') return val;
|
||||
if (typeof val === 'number') return val;
|
||||
// Dates
|
||||
if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}/.test(val)) {
|
||||
return val; // Déjà au bon format
|
||||
}
|
||||
return val;
|
||||
});
|
||||
|
||||
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
||||
const columnsStr = columns.join(', ');
|
||||
|
||||
const sql = `
|
||||
INSERT INTO ${tableName} (${columnsStr})
|
||||
VALUES (${placeholders})
|
||||
ON CONFLICT DO NOTHING
|
||||
`;
|
||||
|
||||
try {
|
||||
await client.query(sql, values);
|
||||
inserted++;
|
||||
} catch (err) {
|
||||
console.warn(` ⚠️ Erreur insertion ligne (ignorée):`, err.message.substring(0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Reset des séquences pour les colonnes SERIAL
|
||||
try {
|
||||
await client.query(`
|
||||
SELECT setval(pg_get_serial_sequence('${tableName}', 'id'),
|
||||
(SELECT MAX(id) FROM ${tableName}))
|
||||
`);
|
||||
} catch (err) {
|
||||
// Ignore si pas de colonne id
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log(` ✓ Table "${tableName}": ${inserted}/${rows.length} lignes migrées`);
|
||||
return inserted;
|
||||
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error(` ✗ Erreur table "${tableName}":`, err.message);
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration principale
|
||||
*/
|
||||
async function migrate() {
|
||||
let totalRows = 0;
|
||||
|
||||
try {
|
||||
console.log('📊 Début de la migration...\n');
|
||||
|
||||
for (const table of TABLES_ORDER) {
|
||||
console.log(`📋 Migration table: ${table}`);
|
||||
|
||||
const rows = await getSqliteData(table);
|
||||
const inserted = await insertPostgresData(table, rows);
|
||||
totalRows += inserted;
|
||||
}
|
||||
|
||||
console.log('\n========================================================');
|
||||
console.log(`✅ Migration terminée!`);
|
||||
console.log(`Total: ${totalRows} lignes migrées`);
|
||||
console.log('========================================================\n');
|
||||
|
||||
} catch (err) {
|
||||
console.error('\n❌ Erreur lors de la migration:', err);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
sqliteDb.close();
|
||||
await pgPool.end();
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Vérifier que la DB SQLite existe
|
||||
if (!fs.existsSync(SQLITE_PATH)) {
|
||||
console.error(`❌ Fichier SQLite introuvable: ${SQLITE_PATH}`);
|
||||
console.log('\nUtilisation:');
|
||||
console.log(' SQLITE_PATH=/path/to/notes.db node scripts/migrate-sqlite-to-postgres.js');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Lancer la migration
|
||||
migrate();
|
||||
Reference in new issue
Block a user