diff --git a/backend/app.js b/backend/app.js index afbdbb4..420dc32 100644 --- a/backend/app.js +++ b/backend/app.js @@ -21,6 +21,7 @@ import taxiHighscoreRouter from './routers/taxiHighscoreRouter.js'; import termineRouter from './routers/termineRouter.js'; import vocabRouter from './routers/vocabRouter.js'; import dashboardRouter from './routers/dashboardRouter.js'; +import newsRouter from './routers/newsRouter.js'; import cors from 'cors'; import './jobs/sessionCleanup.js'; @@ -80,6 +81,7 @@ app.use('/api/models', modelsProxyRouter); app.use('/api/blog', blogRouter); app.use('/api/termine', termineRouter); app.use('/api/dashboard', dashboardRouter); +app.use('/api/news', newsRouter); // Serve frontend SPA for non-API routes to support history mode clean URLs // /models/* nicht statisch ausliefern – nur über /api/models (Proxy mit Komprimierung) diff --git a/backend/controllers/newsController.js b/backend/controllers/newsController.js new file mode 100644 index 0000000..e7bda0e --- /dev/null +++ b/backend/controllers/newsController.js @@ -0,0 +1,21 @@ +import newsService from '../services/newsService.js'; + +/** + * GET /api/news?counter=0&language=de&category=top + * counter = wievieltes News-Widget aufgerufen wird (0, 1, 2, …), damit keine doppelten Artikel. + */ +export default { + async getNews(req, res) { + const counter = Math.max(0, parseInt(req.query.counter, 10) || 0); + const language = (req.query.language || 'de').slice(0, 10); + const category = (req.query.category || 'top').slice(0, 50); + + try { + const { results, nextPage } = await newsService.getNews({ counter, language, category }); + res.json({ results, nextPage }); + } catch (error) { + console.error('News getNews:', error); + res.status(500).json({ error: error.message || 'News konnten nicht geladen werden.' }); + } + } +}; diff --git a/backend/routers/newsRouter.js b/backend/routers/newsRouter.js new file mode 100644 index 0000000..7851805 --- /dev/null +++ b/backend/routers/newsRouter.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import { authenticate } from '../middleware/authMiddleware.js'; +import newsController from '../controllers/newsController.js'; + +const router = Router(); + +router.get('/', authenticate, newsController.getNews.bind(newsController)); + +export default router; diff --git a/backend/services/newsService.js b/backend/services/newsService.js new file mode 100644 index 0000000..2cc1353 --- /dev/null +++ b/backend/services/newsService.js @@ -0,0 +1,70 @@ +/** + * Proxy für newsdata.io API. + * Endpoint: https://newsdata.io/api/1/news?apikey=...&language=...&category=... + * Pagination: counter = wievieltes Widget dieser Art (0 = erste Seite, 1 = zweite, …), damit News nicht doppelt gezeigt werden. + */ + +const NEWS_BASE = 'https://newsdata.io/api/1/news'; + +/** + * @param {object} options + * @param {number} options.counter - 0 = erste Seite, 1 = zweite, … (für Pagination/nextPage) + * @param {string} [options.language] - z. B. de, en + * @param {string} [options.category] - z. B. top, technology + * @returns {Promise<{ results: Array, nextPage: string|null }>} + */ +async function fetchNewsPage({ counter, language = 'de', category = 'top', nextPageToken = null }) { + const apiKey = process.env.NEWSDATA_IO_API_KEY; + if (!apiKey || !apiKey.trim()) { + throw new Error('NEWSDATA_IO_API_KEY is not set in .env'); + } + const params = new URLSearchParams(); + params.set('apikey', apiKey.trim()); + params.set('language', String(language)); + params.set('category', String(category)); + if (nextPageToken) params.set('page', nextPageToken); + + const url = `${NEWS_BASE}?${params.toString()}`; + const res = await fetch(url); + if (!res.ok) { + const text = await res.text(); + throw new Error(`newsdata.io: ${res.status} ${text.slice(0, 200)}`); + } + const data = await res.json(); + return { + results: data.results ?? [], + nextPage: data.nextPage ?? null + }; +} + +/** + * Liefert die Seite für das „counter.“-te Widget (0 = erste Seite, 1 = zweite, …). + * Ruft die API ggf. mehrfach auf (nextPage), damit jede Widget-Instanz andere Artikel bekommt. + * + * @param {object} options + * @param {number} options.counter + * @param {string} [options.language] + * @param {string} [options.category] + * @returns {Promise<{ results: Array, nextPage: string|null }>} + */ +async function getNews({ counter = 0, language = 'de', category = 'top' }) { + let nextPageToken = null; + let lastResult = { results: [], nextPage: null }; + + for (let i = 0; i <= counter; i++) { + lastResult = await fetchNewsPage({ + counter: i, + language, + category, + nextPageToken: nextPageToken || undefined + }); + nextPageToken = lastResult.nextPage; + if (!nextPageToken && i < counter) { + break; + } + } + + return lastResult; +} + +export default { getNews }; diff --git a/backend/utils/initializeWidgetTypes.js b/backend/utils/initializeWidgetTypes.js index ae60bc9..157e675 100644 --- a/backend/utils/initializeWidgetTypes.js +++ b/backend/utils/initializeWidgetTypes.js @@ -2,7 +2,8 @@ import WidgetType from '../models/type/widget_type.js'; const DEFAULT_WIDGET_TYPES = [ { label: 'Termine', endpoint: '/api/termine', description: 'Bevorstehende Termine', orderId: 1 }, - { label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 } + { label: 'Falukant', endpoint: '/api/falukant/dashboard-widget', description: 'Charakter, Geld, Nachrichten, Kinder', orderId: 2 }, + { label: 'News', endpoint: '/api/news?language=de&category=top', description: 'Nachrichten (newsdata.io), Counter für Pagination', orderId: 3 } ]; /** diff --git a/frontend/src/components/DashboardWidget.vue b/frontend/src/components/DashboardWidget.vue index f2a1cae..d43e096 100644 --- a/frontend/src/components/DashboardWidget.vue +++ b/frontend/src/components/DashboardWidget.vue @@ -14,19 +14,29 @@