mirror of
https://github.com/R0m1k3/noteflow.git
synced 2026-10-11 17:29:37 +02:00
Backend: Système complet RSS + OpenRouter + Administration améliorée
## Base de données: - Table settings (key-value pour paramètres globaux) - Table rss_feeds (url, title, description, enabled, last_fetched_at) - Table rss_articles (feed_id, title, link, description, pub_date, content) - Index optimisés pour performance ## Routes Settings (/api/settings): - GET /api/settings - Récupérer tous les paramètres - GET /api/settings/:key - Récupérer un paramètre - PUT /api/settings/:key - Mettre à jour un paramètre - DELETE /api/settings/:key - Supprimer un paramètre - Middleware requireAdmin sur toutes les routes ## Routes RSS (/api/rss): - GET /api/rss/feeds - Liste des flux RSS (admin) - POST /api/rss/feeds - Ajouter flux RSS avec parsing validation (admin) - PUT /api/rss/feeds/:id - Activer/désactiver flux (admin) - DELETE /api/rss/feeds/:id - Supprimer flux (admin) - POST /api/rss/fetch - Récupérer articles de tous flux actifs (admin) - GET /api/rss/articles - Récupérer 10 derniers articles (tous users) - POST /api/rss/summarize - Générer résumé OpenRouter (admin) - GET /api/rss/models - Liste modèles OpenRouter (admin) ## Intégration OpenRouter: - Appel API avec clé, modèle et prompt configurables - Résumé intelligent des 10 derniers articles - Prompt par défaut optimisé pour résumés d'actualités - Support GPT-4, Claude 3, Gemini, Llama 3, Mistral, etc. - Gestion erreurs et timeout ## Parser RSS: - rss-parser installé pour parsing flux - Gestion articles avec contenu, description, date - Détection doublons par URL - Update automatique infos flux ## Frontend HTML: - Modal admin avec système à onglets (Users/RSS/OpenRouter) - Modal changement mot de passe sans prompt navigateur - Box RSS dans page principale affichage 10 derniers articles - Bouton résumé IA dans header box RSS - Inputs pour gestion flux RSS - Configuration OpenRouter (API key, modèle, prompt, activation) - Sélecteur modèles avec recherche Prochaine étape: CSS + JavaScript pour fonctionnalités
This commit is contained in:
7 files changed
+726
-1
No files matched your search
@@ -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
|
// 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_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_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_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_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');
|
logger.info('✓ Tables de base de données créées avec succès');
|
||||||
|
|
||||||
|
|||||||
Generated
+159
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rss-parser": "^3.13.0",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
@@ -295,6 +297,23 @@
|
|||||||
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
"integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@@ -656,6 +675,18 @@
|
|||||||
"color-support": "bin.js"
|
"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": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -771,6 +802,15 @@
|
|||||||
"node": ">=4.0.0"
|
"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": {
|
"node_modules/delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
@@ -887,6 +927,15 @@
|
|||||||
"once": "^1.4.0"
|
"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": {
|
"node_modules/env-paths": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
@@ -934,6 +983,21 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/escape-html": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
@@ -1081,6 +1145,42 @@
|
|||||||
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
"integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1286,6 +1386,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/has-unicode": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz",
|
||||||
@@ -2456,6 +2571,12 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -2587,6 +2708,16 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"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": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2622,6 +2753,12 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.3",
|
"version": "7.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
@@ -3384,6 +3521,28 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
@@ -31,6 +32,7 @@
|
|||||||
"helmet": "^7.1.0",
|
"helmet": "^7.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
|
"rss-parser": "^3.13.0",
|
||||||
"sqlite3": "^5.1.7",
|
"sqlite3": "^5.1.7",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+99
-1
@@ -146,6 +146,24 @@
|
|||||||
<!-- Todos will be inserted here -->
|
<!-- Todos will be inserted here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- RSS Box -->
|
||||||
|
<div class="rss-box">
|
||||||
|
<div class="rss-box-header">
|
||||||
|
<h2 class="rss-box-title">📰 Actualités</h2>
|
||||||
|
<button class="btn-summarize" id="summarizeRssBtn" title="Générer un résumé avec IA">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 2L2 7l10 5 10-5-10-5z"></path>
|
||||||
|
<path d="M2 17l10 5 10-5"></path>
|
||||||
|
<path d="M2 12l10 5 10-5"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rss-articles-list" id="rssArticlesList">
|
||||||
|
<p class="rss-empty">Aucun article pour le moment</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -153,10 +171,18 @@
|
|||||||
<div class="modal-backdrop" id="adminModal" style="display: none;">
|
<div class="modal-backdrop" id="adminModal" style="display: none;">
|
||||||
<div class="modal-content admin-modal">
|
<div class="modal-content admin-modal">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2>Administration des utilisateurs</h2>
|
<h2>Administration</h2>
|
||||||
<button class="modal-close" id="closeAdminModal">✕</button>
|
<button class="modal-close" id="closeAdminModal">✕</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="admin-tabs">
|
||||||
|
<button class="admin-tab active" data-tab="users">👥 Utilisateurs</button>
|
||||||
|
<button class="admin-tab" data-tab="rss">📰 Flux RSS</button>
|
||||||
|
<button class="admin-tab" data-tab="openrouter">🤖 OpenRouter</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglet Utilisateurs -->
|
||||||
|
<div class="admin-tab-content" id="tab-users">
|
||||||
<div class="admin-actions">
|
<div class="admin-actions">
|
||||||
<button class="action-btn" id="createUserBtn">+ Créer un utilisateur</button>
|
<button class="action-btn" id="createUserBtn">+ Créer un utilisateur</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,6 +204,78 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglet RSS -->
|
||||||
|
<div class="admin-tab-content" id="tab-rss" style="display: none;">
|
||||||
|
<div class="admin-actions">
|
||||||
|
<input type="url" id="rssUrlInput" class="rss-url-input" placeholder="https://example.com/feed.xml">
|
||||||
|
<button class="action-btn" id="addRssFeedBtn">+ Ajouter un flux</button>
|
||||||
|
<button class="action-btn btn-secondary" id="fetchRssBtn">🔄 Récupérer les articles</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rss-feeds-list" id="rssFeedsList">
|
||||||
|
<!-- Feeds will be inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglet OpenRouter -->
|
||||||
|
<div class="admin-tab-content" id="tab-openrouter" style="display: none;">
|
||||||
|
<div class="openrouter-settings">
|
||||||
|
<div class="setting-group">
|
||||||
|
<label for="openrouterApiKey">Clé API OpenRouter</label>
|
||||||
|
<input type="password" id="openrouterApiKey" class="setting-input" placeholder="sk-or-...">
|
||||||
|
<small>Obtenez votre clé sur <a href="https://openrouter.ai" target="_blank">openrouter.ai</a></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label for="openrouterModel">Modèle</label>
|
||||||
|
<input type="text" id="modelSearch" class="setting-input" placeholder="Rechercher un modèle...">
|
||||||
|
<select id="openrouterModel" class="setting-select" size="8">
|
||||||
|
<!-- Models will be inserted here -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label for="rssSummaryEnabled">
|
||||||
|
<input type="checkbox" id="rssSummaryEnabled">
|
||||||
|
Activer les résumés automatiques
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-group">
|
||||||
|
<label for="rssSummaryPrompt">Prompt de résumé</label>
|
||||||
|
<textarea id="rssSummaryPrompt" class="setting-textarea" rows="10" placeholder="Le prompt sera chargé..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="action-btn" id="saveOpenRouterSettings">💾 Sauvegarder les paramètres</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Change Password -->
|
||||||
|
<div class="modal-backdrop" id="changePasswordModal" style="display: none;">
|
||||||
|
<div class="modal-content small-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Changer le mot de passe</h3>
|
||||||
|
<button class="modal-close" id="closeChangePasswordModal">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="changePasswordUsername"></p>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="newPassword">Nouveau mot de passe</label>
|
||||||
|
<input type="password" id="newPassword" class="form-input" placeholder="Minimum 6 caractères">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirmPassword">Confirmer le mot de passe</label>
|
||||||
|
<input type="password" id="confirmPassword" class="form-input">
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-cancel" id="cancelChangePassword">Annuler</button>
|
||||||
|
<button class="btn-confirm" id="confirmChangePassword">Changer</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Modal de Confirmation -->
|
<!-- Modal de Confirmation -->
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -82,6 +82,8 @@ app.use('/api/auth', require('./routes/auth.routes'));
|
|||||||
app.use('/api/users', require('./routes/users.routes'));
|
app.use('/api/users', require('./routes/users.routes'));
|
||||||
app.use('/api/notes', require('./routes/notes.routes'));
|
app.use('/api/notes', require('./routes/notes.routes'));
|
||||||
app.use('/api/todos', require('./routes/todos.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
|
// Route de recherche
|
||||||
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
app.get('/api/search', require('./routes/notes.routes').searchNotes);
|
||||||
|
|||||||
Reference in new issue
Block a user