diff --git a/config/database.js b/config/database.js
index dbe6c19..ab552d7 100644
--- a/config/database.js
+++ b/config/database.js
@@ -88,11 +88,52 @@ function initDatabase() {
)
`);
+ // Table settings (paramètres globaux)
+ db.run(`
+ CREATE TABLE IF NOT EXISTS settings (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ key TEXT UNIQUE NOT NULL,
+ value TEXT,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ )
+ `);
+
+ // Table rss_feeds (flux RSS)
+ db.run(`
+ CREATE TABLE IF NOT EXISTS rss_feeds (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ url TEXT UNIQUE NOT NULL,
+ title TEXT,
+ description TEXT,
+ enabled BOOLEAN DEFAULT 1,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ last_fetched_at DATETIME
+ )
+ `);
+
+ // Table rss_articles (articles des flux RSS)
+ db.run(`
+ CREATE TABLE IF NOT EXISTS rss_articles (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ feed_id INTEGER NOT NULL,
+ title TEXT NOT NULL,
+ link TEXT UNIQUE NOT NULL,
+ description TEXT,
+ pub_date DATETIME,
+ content TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (feed_id) REFERENCES rss_feeds(id) ON DELETE CASCADE
+ )
+ `);
+
// Créer les index pour la performance
db.run('CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id)');
db.run('CREATE INDEX IF NOT EXISTS idx_note_todos ON note_todos(note_id)');
db.run('CREATE INDEX IF NOT EXISTS idx_global_todos_user ON global_todos(user_id)');
db.run('CREATE INDEX IF NOT EXISTS idx_note_files ON note_files(note_id)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_settings_key ON settings(key)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_feed ON rss_articles(feed_id)');
+ db.run('CREATE INDEX IF NOT EXISTS idx_rss_articles_date ON rss_articles(pub_date)');
logger.info('✓ Tables de base de données créées avec succès');
diff --git a/package-lock.json b/package-lock.json
index f67e263..9cef404 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "MIT",
"dependencies": {
+ "axios": "^1.13.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
@@ -17,6 +18,7 @@
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
+ "rss-parser": "^3.13.0",
"sqlite3": "^5.1.7",
"winston": "^3.11.0"
},
@@ -295,6 +297,23 @@
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
"license": "MIT"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -656,6 +675,18 @@
"color-support": "bin.js"
}
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -771,6 +802,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -887,6 +927,15 @@
"once": "^1.4.0"
}
},
+ "node_modules/entities": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+ "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+ "license": "BSD-2-Clause",
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@@ -934,6 +983,21 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -1081,6 +1145,42 @@
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
"license": "MIT"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1286,6 +1386,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/has-unicode": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
@@ -2456,6 +2571,12 @@
"node": ">= 0.10"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -2587,6 +2708,16 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/rss-parser": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz",
+ "integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^2.0.3",
+ "xml2js": "^0.5.0"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2622,6 +2753,12 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"license": "MIT"
},
+ "node_modules/sax": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
+ "integrity": "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ==",
+ "license": "BlueOak-1.0.0"
+ },
"node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
@@ -3384,6 +3521,28 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/xml2js": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
+ "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
diff --git a/package.json b/package.json
index a64ca13..db40eec 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"author": "",
"license": "MIT",
"dependencies": {
+ "axios": "^1.13.2",
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"express": "^4.18.2",
@@ -31,6 +32,7 @@
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
+ "rss-parser": "^3.13.0",
"sqlite3": "^5.1.7",
"winston": "^3.11.0"
},
@@ -40,4 +42,4 @@
"engines": {
"node": ">=20.0.0"
}
-}
\ No newline at end of file
+}
diff --git a/public/index.html b/public/index.html
index 0933e3d..e377ebd 100644
--- a/public/index.html
+++ b/public/index.html
@@ -146,6 +146,24 @@
+
+
+
@@ -153,29 +171,109 @@
-
-
+
+
+
+
-
-
-
-
- | ID |
- Nom d'utilisateur |
- Admin |
- Créé le |
- Actions |
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ | ID |
+ Nom d'utilisateur |
+ Admin |
+ Créé le |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/routes/rss.routes.js b/routes/rss.routes.js
new file mode 100644
index 0000000..a882668
--- /dev/null
+++ b/routes/rss.routes.js
@@ -0,0 +1,324 @@
+// Routes de gestion des flux RSS
+const express = require('express');
+const router = express.Router();
+const Parser = require('rss-parser');
+const axios = require('axios');
+const { getAll, getOne, runQuery } = require('../config/database');
+const { authenticateToken, requireAdmin } = require('../middleware/auth');
+const logger = require('../config/logger');
+
+const parser = new Parser();
+
+// Routes admin (gestion des flux)
+router.use(authenticateToken);
+
+/**
+ * GET /api/rss/feeds
+ * Liste tous les flux RSS
+ */
+router.get('/feeds', requireAdmin, async (req, res) => {
+ try {
+ const feeds = await getAll('SELECT * FROM rss_feeds ORDER BY created_at DESC');
+ res.json(feeds || []);
+ } catch (error) {
+ logger.error('Erreur lors de la récupération des flux RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * POST /api/rss/feeds
+ * Ajouter un nouveau flux RSS
+ */
+router.post('/feeds', requireAdmin, async (req, res) => {
+ try {
+ const { url } = req.body;
+
+ if (!url) {
+ return res.status(400).json({ error: 'URL requise' });
+ }
+
+ // Vérifier si le flux existe déjà
+ const existing = await getOne('SELECT id FROM rss_feeds WHERE url = ?', [url]);
+ if (existing) {
+ return res.status(409).json({ error: 'Ce flux existe déjà' });
+ }
+
+ // Tester le flux
+ try {
+ const feed = await parser.parseURL(url);
+
+ // Ajouter le flux
+ const result = await runQuery(
+ 'INSERT INTO rss_feeds (url, title, description) VALUES (?, ?, ?)',
+ [url, feed.title || url, feed.description || '']
+ );
+
+ logger.info(`Flux RSS ajouté: ${feed.title} (${url})`);
+
+ res.json({
+ id: result.id,
+ url,
+ title: feed.title || url,
+ description: feed.description || '',
+ enabled: 1
+ });
+ } catch (parseError) {
+ logger.error('Erreur lors du parsing du flux RSS:', parseError);
+ return res.status(400).json({ error: 'Impossible de parser ce flux RSS. Vérifiez l\'URL.' });
+ }
+ } catch (error) {
+ logger.error('Erreur lors de l\'ajout du flux RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * PUT /api/rss/feeds/:id
+ * Mettre à jour un flux RSS (activer/désactiver)
+ */
+router.put('/feeds/:id', requireAdmin, async (req, res) => {
+ try {
+ const { enabled } = req.body;
+
+ await runQuery(
+ 'UPDATE rss_feeds SET enabled = ? WHERE id = ?',
+ [enabled ? 1 : 0, req.params.id]
+ );
+
+ logger.info(`Flux RSS ${enabled ? 'activé' : 'désactivé'}: ${req.params.id}`);
+ res.json({ message: 'Flux mis à jour avec succès' });
+ } catch (error) {
+ logger.error('Erreur lors de la mise à jour du flux RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * DELETE /api/rss/feeds/:id
+ * Supprimer un flux RSS
+ */
+router.delete('/feeds/:id', requireAdmin, async (req, res) => {
+ try {
+ await runQuery('DELETE FROM rss_feeds WHERE id = ?', [req.params.id]);
+
+ logger.info(`Flux RSS supprimé: ${req.params.id}`);
+ res.json({ message: 'Flux supprimé avec succès' });
+ } catch (error) {
+ logger.error('Erreur lors de la suppression du flux RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * POST /api/rss/fetch
+ * Récupérer tous les articles des flux actifs
+ */
+router.post('/fetch', requireAdmin, async (req, res) => {
+ try {
+ const feeds = await getAll('SELECT * FROM rss_feeds WHERE enabled = 1');
+
+ let totalArticles = 0;
+
+ for (const feed of feeds) {
+ try {
+ const parsedFeed = await parser.parseURL(feed.url);
+
+ // Mettre à jour les infos du flux
+ await runQuery(
+ 'UPDATE rss_feeds SET title = ?, description = ?, last_fetched_at = CURRENT_TIMESTAMP WHERE id = ?',
+ [parsedFeed.title || feed.url, parsedFeed.description || '', feed.id]
+ );
+
+ // Ajouter les articles
+ for (const item of parsedFeed.items) {
+ try {
+ // Vérifier si l'article existe déjà
+ const existing = await getOne('SELECT id FROM rss_articles WHERE link = ?', [item.link]);
+
+ if (!existing) {
+ await runQuery(
+ 'INSERT INTO rss_articles (feed_id, title, link, description, pub_date, content) VALUES (?, ?, ?, ?, ?, ?)',
+ [
+ feed.id,
+ item.title || '',
+ item.link || '',
+ item.contentSnippet || item.description || '',
+ item.pubDate || item.isoDate || new Date().toISOString(),
+ item.content || item['content:encoded'] || ''
+ ]
+ );
+ totalArticles++;
+ }
+ } catch (articleError) {
+ // Ignorer les articles en double ou invalides
+ logger.warn(`Erreur lors de l'ajout d'un article: ${articleError.message}`);
+ }
+ }
+ } catch (feedError) {
+ logger.error(`Erreur lors du fetch du flux ${feed.url}:`, feedError);
+ }
+ }
+
+ logger.info(`${totalArticles} nouveaux articles récupérés`);
+ res.json({ message: `${totalArticles} nouveaux articles récupérés` });
+ } catch (error) {
+ logger.error('Erreur lors du fetch des flux RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * GET /api/rss/articles
+ * Récupérer les 10 derniers articles
+ */
+router.get('/articles', async (req, res) => {
+ try {
+ const articles = await getAll(`
+ SELECT
+ a.id, a.title, a.link, a.description, a.pub_date, a.content,
+ f.title as feed_title, f.url as feed_url
+ FROM rss_articles a
+ JOIN rss_feeds f ON a.feed_id = f.id
+ ORDER BY a.pub_date DESC
+ LIMIT 10
+ `);
+
+ res.json(articles || []);
+ } catch (error) {
+ logger.error('Erreur lors de la récupération des articles RSS:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * POST /api/rss/summarize
+ * Générer un résumé des 10 derniers articles avec OpenRouter
+ */
+router.post('/summarize', requireAdmin, async (req, res) => {
+ try {
+ // Récupérer les paramètres
+ const apiKey = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_api_key']);
+ const model = await getOne('SELECT value FROM settings WHERE key = ?', ['openrouter_model']);
+ const prompt = await getOne('SELECT value FROM settings WHERE key = ?', ['rss_summary_prompt']);
+
+ if (!apiKey || !apiKey.value) {
+ return res.status(400).json({ error: 'Clé API OpenRouter non configurée' });
+ }
+
+ // Récupérer les 10 derniers articles
+ const articles = await getAll(`
+ SELECT
+ a.title, a.description, a.link, a.pub_date,
+ f.title as feed_title
+ FROM rss_articles a
+ JOIN rss_feeds f ON a.feed_id = f.id
+ ORDER BY a.pub_date DESC
+ LIMIT 10
+ `);
+
+ if (articles.length === 0) {
+ return res.status(400).json({ error: 'Aucun article à résumer' });
+ }
+
+ // Construire le prompt
+ const defaultPrompt = prompt?.value || `Tu es un assistant spécialisé dans la synthèse d'actualités. Analyse les articles suivants et crée un résumé structuré et informatif.
+
+Pour chaque article, identifie:
+- Le sujet principal
+- Les points clés
+- L'importance de l'information
+
+Ensuite, génère un résumé global qui:
+1. Regroupe les thèmes communs
+2. Hiérarchise les informations par importance
+3. Présente une vue d'ensemble claire et concise
+4. Utilise un style professionnel mais accessible
+
+Format de sortie en Markdown avec des sections claires.`;
+
+ const articlesText = articles.map((a, i) =>
+ `## Article ${i + 1}: ${a.title}\n**Source**: ${a.feed_title}\n**Date**: ${new Date(a.pub_date).toLocaleDateString('fr-FR')}\n**Description**: ${a.description}\n**Lien**: ${a.link}\n`
+ ).join('\n---\n');
+
+ const fullPrompt = `${defaultPrompt}\n\n# Articles à résumer:\n\n${articlesText}`;
+
+ // Appeler OpenRouter
+ const selectedModel = model?.value || 'openai/gpt-3.5-turbo';
+
+ logger.info(`Génération du résumé avec le modèle ${selectedModel}...`);
+
+ const response = await axios.post(
+ 'https://openrouter.ai/api/v1/chat/completions',
+ {
+ model: selectedModel,
+ messages: [
+ {
+ role: 'user',
+ content: fullPrompt
+ }
+ ]
+ },
+ {
+ headers: {
+ 'Authorization': `Bearer ${apiKey.value}`,
+ 'HTTP-Referer': 'https://noteflow.app',
+ 'X-Title': 'NoteFlow RSS Summarizer',
+ 'Content-Type': 'application/json'
+ },
+ timeout: 60000
+ }
+ );
+
+ const summary = response.data.choices[0].message.content;
+
+ logger.info('Résumé généré avec succès');
+
+ res.json({
+ summary,
+ model: selectedModel,
+ articles_count: articles.length
+ });
+ } catch (error) {
+ logger.error('Erreur lors de la génération du résumé:', error);
+
+ if (error.response) {
+ logger.error('Réponse OpenRouter:', error.response.data);
+ return res.status(error.response.status).json({
+ error: 'Erreur API OpenRouter',
+ details: error.response.data
+ });
+ }
+
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * GET /api/rss/models
+ * Récupérer la liste des modèles OpenRouter
+ */
+router.get('/models', requireAdmin, async (req, res) => {
+ try {
+ // Liste des modèles populaires pour le résumé
+ const models = [
+ { id: 'openai/gpt-4-turbo', name: 'GPT-4 Turbo', provider: 'OpenAI' },
+ { id: 'openai/gpt-4', name: 'GPT-4', provider: 'OpenAI' },
+ { id: 'openai/gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'OpenAI' },
+ { id: 'anthropic/claude-3-opus', name: 'Claude 3 Opus', provider: 'Anthropic' },
+ { id: 'anthropic/claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'Anthropic' },
+ { id: 'anthropic/claude-3-haiku', name: 'Claude 3 Haiku', provider: 'Anthropic' },
+ { id: 'google/gemini-pro', name: 'Gemini Pro', provider: 'Google' },
+ { id: 'meta-llama/llama-3-70b-instruct', name: 'Llama 3 70B', provider: 'Meta' },
+ { id: 'mistralai/mistral-large', name: 'Mistral Large', provider: 'Mistral AI' },
+ { id: 'mistralai/mixtral-8x7b-instruct', name: 'Mixtral 8x7B', provider: 'Mistral AI' }
+ ];
+
+ res.json(models);
+ } catch (error) {
+ logger.error('Erreur lors de la récupération des modèles:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+module.exports = router;
diff --git a/routes/settings.routes.js b/routes/settings.routes.js
new file mode 100644
index 0000000..e3e9e2e
--- /dev/null
+++ b/routes/settings.routes.js
@@ -0,0 +1,99 @@
+// Routes de gestion des paramètres
+const express = require('express');
+const router = express.Router();
+const { getAll, getOne, runQuery } = require('../config/database');
+const { authenticateToken, requireAdmin } = require('../middleware/auth');
+const logger = require('../config/logger');
+
+// Toutes les routes nécessitent authentification admin
+router.use(authenticateToken);
+router.use(requireAdmin);
+
+/**
+ * GET /api/settings
+ * Récupérer tous les paramètres
+ */
+router.get('/', async (req, res) => {
+ try {
+ const settings = await getAll('SELECT key, value FROM settings');
+
+ // Convertir en objet key-value
+ const settingsObj = {};
+ settings.forEach(setting => {
+ settingsObj[setting.key] = setting.value;
+ });
+
+ res.json(settingsObj);
+ } catch (error) {
+ logger.error('Erreur lors de la récupération des paramètres:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * GET /api/settings/:key
+ * Récupérer un paramètre spécifique
+ */
+router.get('/:key', async (req, res) => {
+ try {
+ const setting = await getOne('SELECT value FROM settings WHERE key = ?', [req.params.key]);
+
+ if (!setting) {
+ return res.status(404).json({ error: 'Paramètre non trouvé' });
+ }
+
+ res.json({ key: req.params.key, value: setting.value });
+ } catch (error) {
+ logger.error('Erreur lors de la récupération du paramètre:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * PUT /api/settings/:key
+ * Mettre à jour un paramètre
+ */
+router.put('/:key', async (req, res) => {
+ try {
+ const { value } = req.body;
+
+ // Vérifier si le paramètre existe
+ const existing = await getOne('SELECT id FROM settings WHERE key = ?', [req.params.key]);
+
+ if (existing) {
+ await runQuery(
+ 'UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?',
+ [value, req.params.key]
+ );
+ } else {
+ await runQuery(
+ 'INSERT INTO settings (key, value) VALUES (?, ?)',
+ [req.params.key, value]
+ );
+ }
+
+ logger.info(`Paramètre mis à jour: ${req.params.key}`);
+ res.json({ key: req.params.key, value });
+ } catch (error) {
+ logger.error('Erreur lors de la mise à jour du paramètre:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+/**
+ * DELETE /api/settings/:key
+ * Supprimer un paramètre
+ */
+router.delete('/:key', async (req, res) => {
+ try {
+ await runQuery('DELETE FROM settings WHERE key = ?', [req.params.key]);
+
+ logger.info(`Paramètre supprimé: ${req.params.key}`);
+ res.json({ message: 'Paramètre supprimé avec succès' });
+ } catch (error) {
+ logger.error('Erreur lors de la suppression du paramètre:', error);
+ res.status(500).json({ error: 'Erreur serveur' });
+ }
+});
+
+module.exports = router;
diff --git a/server.js b/server.js
index fbd545e..54c23be 100644
--- a/server.js
+++ b/server.js
@@ -82,6 +82,8 @@ 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'));
// Route de recherche
app.get('/api/search', require('./routes/notes.routes').searchNotes);