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:
Claude committed 2025-11-11 15:52:39 +00:00
1 parent 820b0cfeda
commit e3755d273d
7 files changed
+726 -1

No files matched your search

+41
View File
@@ -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');
+159
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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 -->
+324
View File
@@ -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;
+99
View File
@@ -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;
+2
View File
@@ -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);