From 18cf24ff93b82ecda89ddfcb04eecb3f47fee900 Mon Sep 17 00:00:00 2001 From: nadja Date: Wed, 26 Aug 2026 12:18:33 +0200 Subject: [PATCH] add Analyse --- .env.example | 4 ++ backend/db.js | 20 +++++++--- backend/routes/auth.js | 30 +++----------- backend/routes/recipes.js | 54 +++++++++++++++++++++++-- backend/server.js | 24 +++++++++++ docker-compose.yml | 2 + frontend/src/App.jsx | 12 +++++- frontend/src/api.js | 55 +++++++++++++++++++++++++- frontend/src/components/RecipeCard.jsx | 5 ++- frontend/src/pages/AddRecipe.jsx | 53 +++++++++++++++++++------ frontend/src/pages/Home.jsx | 41 ++++++++++++++++--- frontend/src/pages/RecipeDetail.jsx | 37 +++++++++++++---- 12 files changed, 279 insertions(+), 58 deletions(-) diff --git a/.env.example b/.env.example index a6aa602..20a8ec0 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,7 @@ ADMIN_PASS=DEIN-SICHERES-PASSWORT # Ollama KI-Modell (optional, Standard: mistral) # OLLAMA_MODEL=mistral + +# Instagram oEmbed Token (optional, für Instagram Reels Extraktion) +# Benötigt ein Facebook App Access Token +# INSTAGRAM_TOKEN=dein-token-hier diff --git a/backend/db.js b/backend/db.js index 6caf599..3ba7ca0 100644 --- a/backend/db.js +++ b/backend/db.js @@ -31,7 +31,7 @@ export function getDb() { return db; } -export function getAllRecipes(search, category) { +export function getAllRecipes(search, category, userId) { let results = [...db.recipes]; if (search) { @@ -47,6 +47,10 @@ export function getAllRecipes(search, category) { results = results.filter(r => r.category === category); } + if (userId) { + results = results.filter(r => r.user_id === Number(userId)); + } + results.sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); return results; } @@ -65,8 +69,11 @@ export function createRecipe(recipe) { ingredients: JSON.stringify(recipe.ingredients || []), steps: JSON.stringify(recipe.steps || []), category: recipe.category || 'Sonstiges', - tiktok_url: recipe.tiktok_url || null, - tiktok_author: recipe.tiktok_author || null, + source: recipe.source || 'tiktok', + source_url: recipe.source_url || recipe.tiktok_url || null, + source_author: recipe.source_author || recipe.tiktok_author || null, + tiktok_url: recipe.tiktok_url || recipe.source_url || null, + tiktok_author: recipe.tiktok_author || recipe.source_author || null, thumbnail_url: recipe.thumbnail_url || null, user_id: recipe.user_id || null, created_at: nowStr, @@ -87,8 +94,11 @@ export function updateRecipe(id, recipe) { ingredients: JSON.stringify(recipe.ingredients || []), steps: JSON.stringify(recipe.steps || []), category: recipe.category || 'Sonstiges', - tiktok_url: recipe.tiktok_url || null, - tiktok_author: recipe.tiktok_author || null, + source: recipe.source || db.recipes[idx].source || 'tiktok', + source_url: recipe.source_url || recipe.tiktok_url || db.recipes[idx].source_url || null, + source_author: recipe.source_author || recipe.tiktok_author || db.recipes[idx].source_author || null, + tiktok_url: recipe.tiktok_url || recipe.source_url || db.recipes[idx].tiktok_url || null, + tiktok_author: recipe.tiktok_author || recipe.source_author || db.recipes[idx].tiktok_author || null, thumbnail_url: recipe.thumbnail_url || null, updated_at: now(), }; diff --git a/backend/routes/auth.js b/backend/routes/auth.js index f9663d4..efdd54f 100644 --- a/backend/routes/auth.js +++ b/backend/routes/auth.js @@ -7,35 +7,13 @@ import { createInviteToken, getInviteToken, useInviteToken, getInvitesByUser, getAllUsers, getInviteStats } from '../db.js'; +import { authMiddleware, adminMiddleware } from '../middleware.js'; +import { trackEvent } from '../analytics-db.js'; const router = Router(); const JWT_SECRET = process.env.JWT_SECRET || 'tiktok-rezepte-secret-key-change-in-production'; const JWT_EXPIRES_IN = '7d'; -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' }); - } -} - -function adminMiddleware(req, res, next) { - if (!req.user || !req.user.is_admin) { - return res.status(403).json({ error: 'Keine Berechtigung' }); - } - next(); -} - router.post('/register', async (req, res) => { try { const { username, password, inviteToken } = req.body; @@ -69,6 +47,8 @@ router.post('/register', async (req, res) => { const user = createUser(username, hashedPassword); useInviteToken(inviteToken, user.id); + trackEvent('user_register', user.id, { username: user.username }); + const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); res.status(201).json({ user, token }); @@ -97,6 +77,8 @@ router.post('/login', async (req, res) => { const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN }); + trackEvent('user_login', user.id, { username: user.username }); + res.json({ user: { id: user.id, username: user.username, is_admin: !!user.is_admin, created_at: user.created_at }, token, diff --git a/backend/routes/recipes.js b/backend/routes/recipes.js index dbaf316..3b82db5 100644 --- a/backend/routes/recipes.js +++ b/backend/routes/recipes.js @@ -1,16 +1,18 @@ import { Router } from 'express'; import { getAllRecipes, getRecipeById, createRecipe, - updateRecipe, deleteRecipe, getCategories + updateRecipe, deleteRecipe, getCategories, getDb } from '../db.js'; import { getTikTokData } from '../services/tiktok.js'; +import { getInstagramData } from '../services/instagram.js'; import { isOllamaAvailable, extractRecipe } from '../services/ai.js'; +import { trackEvent } from '../analytics-db.js'; const router = Router(); router.get('/', (req, res) => { - const { search, category } = req.query; - const recipes = getAllRecipes(search, category); + const { search, category, user_id } = req.query; + const recipes = getAllRecipes(search, category, user_id); res.json(recipes.map(r => ({ ...r, ingredients: JSON.parse(r.ingredients), @@ -22,6 +24,16 @@ router.get('/categories', (req, res) => { res.json(getCategories()); }); +router.get('/authors', (req, res) => { + const db = getDb(); + const authorIds = [...new Set(db.recipes.filter(r => r.user_id).map(r => r.user_id))]; + const authors = authorIds.map(id => { + const user = db.users.find(u => u.id === id); + return { id, username: user ? user.username : `User #${id}` }; + }).sort((a, b) => a.username.localeCompare(b.username)); + res.json(authors); +}); + router.get('/ai-status', async (req, res) => { const available = await isOllamaAvailable(); res.json({ ollama: available }); @@ -39,6 +51,8 @@ router.get('/:id', (req, res) => { router.post('/', (req, res) => { const recipe = createRecipe(req.body); + const userId = req.body.user_id || null; + trackEvent('recipe_created', userId, { recipe_id: recipe.id, recipe_name: recipe.name }); res.status(201).json(recipe); }); @@ -46,6 +60,7 @@ router.put('/:id', (req, res) => { const existing = getRecipeById(req.params.id); if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden' }); const updated = updateRecipe(req.params.id, req.body); + trackEvent('recipe_updated', req.body.user_id || null, { recipe_id: Number(req.params.id), recipe_name: updated.name }); res.json(updated); }); @@ -53,6 +68,7 @@ router.delete('/:id', (req, res) => { const existing = getRecipeById(req.params.id); if (!existing) return res.status(404).json({ error: 'Rezept nicht gefunden' }); deleteRecipe(req.params.id); + trackEvent('recipe_deleted', null, { recipe_id: Number(req.params.id) }); res.json({ success: true }); }); @@ -88,4 +104,36 @@ router.post('/from-tiktok', async (req, res) => { } }); +router.post('/from-instagram', async (req, res) => { + try { + const { url } = req.body; + if (!url) return res.status(400).json({ error: 'URL erforderlich' }); + + const instagramData = await getInstagramData(url); + + const ollamaAvailable = await isOllamaAvailable(); + let extracted = null; + let aiStatus = ollamaAvailable ? 'available' : 'unavailable'; + + if (ollamaAvailable && instagramData.title) { + try { + extracted = await extractRecipe(instagramData.title); + aiStatus = 'success'; + } catch (e) { + console.error('Ollama Extraktion fehlgeschlagen:', e.message); + aiStatus = 'error'; + } + } + + res.json({ + instagram: instagramData, + ollama: ollamaAvailable, + aiStatus, + extracted, + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + export default router; diff --git a/backend/server.js b/backend/server.js index a707556..8eaba26 100644 --- a/backend/server.js +++ b/backend/server.js @@ -1,18 +1,42 @@ import express from 'express'; import cors from 'cors'; import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; import recipesRouter from './routes/recipes.js'; import authRouter from './routes/auth.js'; +import analyticsRouter from './routes/analytics.js'; import { getUserByUsername, createUser } from './db.js'; +import { trackEvent } from './analytics-db.js'; +import { authMiddleware, adminMiddleware } from './middleware.js'; const app = express(); const PORT = process.env.PORT || 3001; +const JWT_SECRET = process.env.JWT_SECRET || 'tiktok-rezepte-secret-key-change-in-production'; app.use(cors()); app.use(express.json()); +app.use((req, res, next) => { + if (req.method === 'OPTIONS' || !req.path.startsWith('/api/')) return next(); + if (req.path.startsWith('/api/analytics')) return next(); + + let userId = null; + const authHeader = req.headers.authorization; + if (authHeader && authHeader.startsWith('Bearer ')) { + try { + const decoded = jwt.verify(authHeader.split(' ')[1], JWT_SECRET); + const user = getUserByUsername(decoded.username); + if (user) userId = user.id; + } catch {} + } + + trackEvent('page_view', userId, { path: req.path, method: req.method }); + next(); +}); + app.use('/api/auth', authRouter); app.use('/api/recipes', recipesRouter); +app.use('/api/analytics', authMiddleware, adminMiddleware, analyticsRouter); const adminUser = process.env.ADMIN_USER || 'admin'; const adminPass = process.env.ADMIN_PASS || 'admin123'; diff --git a/docker-compose.yml b/docker-compose.yml index 7b25fc5..efb4a11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,13 @@ services: environment: - PORT=3001 - DB_PATH=/app/data/data.json + - ANALYTICS_PATH=/app/data/analytics.json - OLLAMA_URL=http://ollama:11434 - OLLAMA_MODEL=${OLLAMA_MODEL:-mistral} - JWT_SECRET=${JWT_SECRET} - ADMIN_USER=${ADMIN_USER:-admin} - ADMIN_PASS=${ADMIN_PASS} + - INSTAGRAM_TOKEN=${INSTAGRAM_TOKEN:-} volumes: - ./data:/app/data depends_on: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index d76527d..c8c53b1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import Login from './pages/Login'; import Register from './pages/Register'; import Admin from './pages/Admin'; import Profile from './pages/Profile'; +import Analytics from './pages/Analytics'; function ProtectedRoute({ children }) { const { user, loading } = useAuth(); @@ -41,7 +42,7 @@ function AppContent() {