init push

This commit is contained in:
nadja
2026-08-25 20:29:14 +02:00
commit 2bb9aab82c
42 changed files with 3966 additions and 0 deletions

214
backend/routes/auth.js Normal file
View File

@@ -0,0 +1,214 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import {
getUserByUsername, getUserById, createUser, updateUserCredentials, deleteUser,
createInviteToken, getInviteToken, useInviteToken,
getInvitesByUser, getAllUsers, getInviteStats
} from '../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;
if (!username || !password || !inviteToken) {
return res.status(400).json({ error: 'Benutzername, Passwort und Einladungscode erforderlich' });
}
if (username.length < 3) {
return res.status(400).json({ error: 'Benutzername muss mindestens 3 Zeichen lang sein' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' });
}
const invite = getInviteToken(inviteToken);
if (!invite) {
return res.status(400).json({ error: 'Ungültiger Einladungscode' });
}
if (invite.used_by) {
return res.status(400).json({ error: 'Einladungscode wurde bereits verwendet' });
}
const existing = getUserByUsername(username);
if (existing) {
return res.status(409).json({ error: 'Benutzername ist bereits vergeben' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = createUser(username, hashedPassword);
useInviteToken(inviteToken, user.id);
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
res.status(201).json({ user, token });
} catch (error) {
res.status(500).json({ error: 'Registrierung fehlgeschlagen' });
}
});
router.post('/login', async (req, res) => {
try {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' });
}
const user = getUserByUsername(username);
if (!user) {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
res.json({
user: { id: user.id, username: user.username, is_admin: !!user.is_admin, created_at: user.created_at },
token,
});
} catch (error) {
res.status(500).json({ error: 'Anmeldung fehlgeschlagen' });
}
});
router.get('/me', (req, res) => {
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' });
res.json({ id: user.id, username: user.username, is_admin: !!user.is_admin, created_at: user.created_at });
} catch {
res.status(401).json({ error: 'Ungültiges Token' });
}
});
router.post('/invite', authMiddleware, adminMiddleware, (req, res) => {
try {
const { count = 1 } = req.body;
const amount = Math.min(Math.max(parseInt(count) || 1, 1), 20);
const tokens = [];
for (let i = 0; i < amount; i++) {
const token = crypto.randomBytes(16).toString('hex');
createInviteToken(token, req.user.id);
tokens.push(token);
}
res.json({ tokens });
} catch (error) {
res.status(500).json({ error: 'Einladung fehlgeschlagen' });
}
});
router.get('/invites', authMiddleware, adminMiddleware, (req, res) => {
try {
const invites = getInvitesByUser(req.user.id);
const stats = getInviteStats();
res.json({ invites, stats });
} catch (error) {
res.status(500).json({ error: 'Fehler beim Laden der Einladungen' });
}
});
router.get('/users', authMiddleware, adminMiddleware, (req, res) => {
try {
const users = getAllUsers();
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Fehler beim Laden der Benutzer' });
}
});
router.delete('/users/:id', authMiddleware, adminMiddleware, (req, res) => {
try {
const userId = Number(req.params.id);
if (userId === req.user.id) {
return res.status(400).json({ error: 'Du kannst dich nicht selbst löschen' });
}
const user = getUserById(userId);
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
deleteUser(userId);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: 'Fehler beim Löschen' });
}
});
router.put('/credentials', authMiddleware, async (req, res) => {
try {
const { currentPassword, newUsername, newPassword } = req.body;
const user = getUserByUsername(req.user.username);
if (!user) return res.status(404).json({ error: 'Benutzer nicht gefunden' });
if (currentPassword) {
const valid = await bcrypt.compare(currentPassword, user.password);
if (!valid) return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' });
}
if (newUsername && newUsername !== user.username) {
const exists = getUserByUsername(newUsername);
if (exists) return res.status(409).json({ error: 'Benutzername ist bereits vergeben' });
}
const updates = {};
if (newUsername) updates.username = newUsername;
if (newPassword) {
if (newPassword.length < 6) return res.status(400).json({ error: 'Neues Passwort muss mindestens 6 Zeichen lang sein' });
updates.password = await bcrypt.hash(newPassword, 10);
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({ error: 'Keine Änderungen angegeben' });
}
const updated = updateUserCredentials(user.id, updates);
const token = jwt.sign({ id: updated.id, username: updated.username }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
res.json({ user: updated, token });
} catch (error) {
res.status(500).json({ error: 'Fehler beim Aktualisieren' });
}
});
export default router;

91
backend/routes/recipes.js Normal file
View File

@@ -0,0 +1,91 @@
import { Router } from 'express';
import {
getAllRecipes, getRecipeById, createRecipe,
updateRecipe, deleteRecipe, getCategories
} from '../db.js';
import { getTikTokData } from '../services/tiktok.js';
import { isOllamaAvailable, extractRecipe } from '../services/ai.js';
const router = Router();
router.get('/', (req, res) => {
const { search, category } = req.query;
const recipes = getAllRecipes(search, category);
res.json(recipes.map(r => ({
...r,
ingredients: JSON.parse(r.ingredients),
steps: JSON.parse(r.steps),
})));
});
router.get('/categories', (req, res) => {
res.json(getCategories());
});
router.get('/ai-status', async (req, res) => {
const available = await isOllamaAvailable();
res.json({ ollama: available });
});
router.get('/:id', (req, res) => {
const recipe = getRecipeById(req.params.id);
if (!recipe) return res.status(404).json({ error: 'Rezept nicht gefunden' });
res.json({
...recipe,
ingredients: JSON.parse(recipe.ingredients),
steps: JSON.parse(recipe.steps),
});
});
router.post('/', (req, res) => {
const recipe = createRecipe(req.body);
res.status(201).json(recipe);
});
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);
res.json(updated);
});
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);
res.json({ success: true });
});
router.post('/from-tiktok', async (req, res) => {
try {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'URL erforderlich' });
const tiktokData = await getTikTokData(url);
const ollamaAvailable = await isOllamaAvailable();
let extracted = null;
let aiStatus = ollamaAvailable ? 'available' : 'unavailable';
if (ollamaAvailable && tiktokData.title) {
try {
extracted = await extractRecipe(tiktokData.title);
aiStatus = 'success';
} catch (e) {
console.error('Ollama Extraktion fehlgeschlagen:', e.message);
aiStatus = 'error';
}
}
res.json({
tiktok: tiktokData,
ollama: ollamaAvailable,
aiStatus,
extracted,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
export default router;