From e0ef0344636ed326f9073a92f1958bf0c01d4342 Mon Sep 17 00:00:00 2001 From: nadja Date: Wed, 26 Aug 2026 12:53:53 +0200 Subject: [PATCH] fix Analyse --- backend/analytics-db.js | 149 ++++++++++++++++++++++++++++++++++++ backend/analytics.json | 4 + backend/middleware.js | 28 +++++++ backend/routes/analytics.js | 42 ++++++++++ 4 files changed, 223 insertions(+) create mode 100644 backend/analytics-db.js create mode 100644 backend/analytics.json create mode 100644 backend/middleware.js create mode 100644 backend/routes/analytics.js diff --git a/backend/analytics-db.js b/backend/analytics-db.js new file mode 100644 index 0000000..8465f78 --- /dev/null +++ b/backend/analytics-db.js @@ -0,0 +1,149 @@ +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DB_FILE = process.env.ANALYTICS_PATH || join(__dirname, 'analytics.json'); + +function loadDB() { + if (existsSync(DB_FILE)) { + return JSON.parse(readFileSync(DB_FILE, 'utf-8')); + } + return { events: [], _counter: 0 }; +} + +function saveDB(data) { + writeFileSync(DB_FILE, JSON.stringify(data, null, 2)); +} + +let db = loadDB(); + +function nextId() { + db._counter = (db._counter || 0) + 1; + return db._counter; +} + +function now() { + return new Date().toISOString().replace('T', ' ').slice(0, 19); +} + +export function trackEvent(type, userId, metadata = {}) { + const event = { + id: nextId(), + type, + user_id: userId || null, + timestamp: now(), + ...metadata, + }; + db.events.push(event); + saveDB(db); + return event; +} + +export function getEvents({ from, to, type, user_id, limit = 100 } = {}) { + let results = [...db.events]; + + if (from) { + results = results.filter(e => e.timestamp >= from); + } + if (to) { + results = results.filter(e => e.timestamp <= to); + } + if (type) { + results = results.filter(e => e.type === type); + } + if (user_id) { + results = results.filter(e => e.user_id === Number(user_id)); + } + + results.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); + return results.slice(0, limit); +} + +export function getOverview() { + const today = new Date().toISOString().slice(0, 10); + const weekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10); + const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().slice(0, 10); + + const pageviews = db.events.filter(e => e.type === 'page_view'); + const pageviewsToday = pageviews.filter(e => e.timestamp.startsWith(today)).length; + const pageviewsYesterday = pageviews.filter(e => e.timestamp.startsWith(yesterday)).length; + const pageviewsWeek = pageviews.filter(e => e.timestamp.slice(0, 10) >= weekAgo).length; + const pageviewsTotal = pageviews.length; + + const activeUsers = new Set(db.events.filter(e => e.user_id).map(e => e.user_id)); + const activeUsersToday = new Set( + db.events.filter(e => e.user_id && e.timestamp.startsWith(today)).map(e => e.user_id) + ).size; + + const recipesCreated = db.events.filter(e => e.type === 'recipe_created').length; + const recipesUpdated = db.events.filter(e => e.type === 'recipe_updated').length; + const recipesDeleted = db.events.filter(e => e.type === 'recipe_deleted').length; + const logins = db.events.filter(e => e.type === 'user_login').length; + + return { + pageviews: { today: pageviewsToday, yesterday: pageviewsYesterday, week: pageviewsWeek, total: pageviewsTotal }, + activeUsers: { total: activeUsers.size, today: activeUsersToday }, + recipes: { created: recipesCreated, updated: recipesUpdated, deleted: recipesDeleted }, + logins, + }; +} + +export function getPageviewsByDay(from, to) { + const pageviews = db.events.filter(e => e.type === 'page_view'); + const days = {}; + + for (const e of pageviews) { + const day = e.timestamp.slice(0, 10); + if (from && day < from) continue; + if (to && day > to) continue; + days[day] = (days[day] || 0) + 1; + } + + return Object.entries(days) + .map(([date, count]) => ({ date, count })) + .sort((a, b) => a.date.localeCompare(b.date)); +} + +export function getTopUsers(limit = 10) { + const userActivity = {}; + + for (const e of db.events) { + if (!e.user_id) continue; + if (!userActivity[e.user_id]) { + userActivity[e.user_id] = { user_id: e.user_id, logins: 0, recipes_created: 0, recipes_updated: 0, total_events: 0 }; + } + userActivity[e.user_id].total_events++; + if (e.type === 'user_login') userActivity[e.user_id].logins++; + if (e.type === 'recipe_created') userActivity[e.user_id].recipes_created++; + if (e.type === 'recipe_updated') userActivity[e.user_id].recipes_updated++; + } + + return Object.values(userActivity) + .sort((a, b) => b.total_events - a.total_events) + .slice(0, limit); +} + +export function getTopRecipes(limit = 10) { + const recipeViews = {}; + + for (const e of db.events) { + if (e.type === 'page_view' && e.path?.startsWith('/recipe/')) { + const id = e.path.split('/')[2]; + if (id) { + recipeViews[id] = (recipeViews[id] || 0) + 1; + } + } + } + + return Object.entries(recipeViews) + .map(([recipe_id, views]) => ({ recipe_id: Number(recipe_id), views })) + .sort((a, b) => b.views - a.views) + .slice(0, limit); +} + +export function getRecentActivity(limit = 50) { + return db.events + .sort((a, b) => b.timestamp.localeCompare(a.timestamp)) + .slice(0, limit); +} diff --git a/backend/analytics.json b/backend/analytics.json new file mode 100644 index 0000000..063b41d --- /dev/null +++ b/backend/analytics.json @@ -0,0 +1,4 @@ +{ + "events": [], + "_counter": 0 +} diff --git a/backend/middleware.js b/backend/middleware.js new file mode 100644 index 0000000..9c235d0 --- /dev/null +++ b/backend/middleware.js @@ -0,0 +1,28 @@ +import jwt from 'jsonwebtoken'; +import { getUserByUsername } from './db.js'; + +const JWT_SECRET = process.env.JWT_SECRET || 'tiktok-rezepte-secret-key-change-in-production'; + +export function authMiddleware(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Nicht authentifiziert' }); + } + try { + const token = authHeader.split(' ')[1]; + const decoded = jwt.verify(token, JWT_SECRET); + const user = getUserByUsername(decoded.username); + if (!user) return res.status(401).json({ error: 'Benutzer nicht gefunden' }); + req.user = user; + next(); + } catch { + res.status(401).json({ error: 'Ungültiges Token' }); + } +} + +export function adminMiddleware(req, res, next) { + if (!req.user || !req.user.is_admin) { + return res.status(403).json({ error: 'Keine Berechtigung' }); + } + next(); +} diff --git a/backend/routes/analytics.js b/backend/routes/analytics.js new file mode 100644 index 0000000..6e8b7ec --- /dev/null +++ b/backend/routes/analytics.js @@ -0,0 +1,42 @@ +import { Router } from 'express'; +import { + getOverview, getPageviewsByDay, getTopUsers, + getTopRecipes, getRecentActivity, getEvents +} from '../analytics-db.js'; +import { getUserById } from '../db.js'; + +const router = Router(); + +router.get('/overview', (req, res) => { + res.json(getOverview()); +}); + +router.get('/pageviews', (req, res) => { + const { from, to } = req.query; + res.json(getPageviewsByDay(from, to)); +}); + +router.get('/users', (req, res) => { + const topUsers = getTopUsers(20); + const enriched = topUsers.map(u => { + const user = getUserById(u.user_id); + return { ...u, username: user ? user.username : `User #${u.user_id}` }; + }); + res.json(enriched); +}); + +router.get('/recipes', (req, res) => { + res.json(getTopRecipes(20)); +}); + +router.get('/activity', (req, res) => { + const { user_id, limit } = req.query; + const events = getEvents({ user_id, limit: limit ? Number(limit) : 50 }); + const enriched = events.map(e => { + const user = e.user_id ? getUserById(e.user_id) : null; + return { ...e, username: user ? user.username : null }; + }); + res.json(enriched); +}); + +export default router;