fix Analyse
This commit is contained in:
149
backend/analytics-db.js
Normal file
149
backend/analytics-db.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
4
backend/analytics.json
Normal file
4
backend/analytics.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"events": [],
|
||||||
|
"_counter": 0
|
||||||
|
}
|
||||||
28
backend/middleware.js
Normal file
28
backend/middleware.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
42
backend/routes/analytics.js
Normal file
42
backend/routes/analytics.js
Normal file
@@ -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;
|
||||||
Reference in New Issue
Block a user