import { Router } from 'express'; import bcrypt from 'bcryptjs'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { getDb } from '../config/database.js'; import { authenticateToken, generateToken } from '../middleware/auth.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const uploadsDir = path.join(__dirname, '..', '..', 'uploads', 'avatars'); // Ensure uploads directory exists if (!fs.existsSync(uploadsDir)) { fs.mkdirSync(uploadsDir, { recursive: true }); } const router = Router(); // POST /api/auth/register router.post('/register', async (req, res) => { try { const { name, email, password } = req.body; if (!name || !email || !password) { return res.status(400).json({ error: 'Alle Felder sind erforderlich' }); } if (password.length < 6) { return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' }); } const db = getDb(); const existing = await db.get('SELECT id FROM users WHERE email = ?', [email]); if (existing) { return res.status(409).json({ error: 'E-Mail wird bereits verwendet' }); } const hash = bcrypt.hashSync(password, 12); const result = await db.run( 'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)', [name, email.toLowerCase(), hash] ); const token = generateToken(result.lastInsertRowid); const user = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [result.lastInsertRowid]); res.status(201).json({ token, user }); } catch (err) { console.error('Register error:', err); res.status(500).json({ error: 'Registrierung fehlgeschlagen' }); } }); // POST /api/auth/login router.post('/login', async (req, res) => { try { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' }); } const db = getDb(); const user = await db.get('SELECT * FROM users WHERE email = ?', [email.toLowerCase()]); if (!user || !bcrypt.compareSync(password, user.password_hash)) { return res.status(401).json({ error: 'Ungültige Anmeldedaten' }); } const token = generateToken(user.id); const { password_hash, ...safeUser } = user; res.json({ token, user: safeUser }); } catch (err) { console.error('Login error:', err); res.status(500).json({ error: 'Anmeldung fehlgeschlagen' }); } }); // GET /api/auth/me router.get('/me', authenticateToken, (req, res) => { res.json({ user: req.user }); }); // PUT /api/auth/profile router.put('/profile', authenticateToken, async (req, res) => { try { const { name, email, theme, language, avatar_color } = req.body; const db = getDb(); if (email && email !== req.user.email) { const existing = await db.get('SELECT id FROM users WHERE email = ? AND id != ?', [email.toLowerCase(), req.user.id]); if (existing) { return res.status(409).json({ error: 'E-Mail wird bereits verwendet' }); } } await db.run(` UPDATE users SET name = COALESCE(?, name), email = COALESCE(?, email), theme = COALESCE(?, theme), language = COALESCE(?, language), avatar_color = COALESCE(?, avatar_color), updated_at = CURRENT_TIMESTAMP WHERE id = ? `, [name, email?.toLowerCase(), theme, language, avatar_color, req.user.id]); const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); res.json({ user: updated }); } catch (err) { console.error('Profile update error:', err); res.status(500).json({ error: 'Profil konnte nicht aktualisiert werden' }); } }); // PUT /api/auth/password router.put('/password', authenticateToken, async (req, res) => { try { const { currentPassword, newPassword } = req.body; const db = getDb(); const user = await db.get('SELECT password_hash FROM users WHERE id = ?', [req.user.id]); if (!bcrypt.compareSync(currentPassword, user.password_hash)) { return res.status(401).json({ error: 'Aktuelles Passwort ist falsch' }); } if (newPassword.length < 6) { return res.status(400).json({ error: 'Neues Passwort muss mindestens 6 Zeichen lang sein' }); } const hash = bcrypt.hashSync(newPassword, 12); await db.run('UPDATE users SET password_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hash, req.user.id]); res.json({ message: 'Passwort erfolgreich geändert' }); } catch (err) { console.error('Password change error:', err); res.status(500).json({ error: 'Passwort konnte nicht geändert werden' }); } }); // POST /api/auth/avatar - Upload avatar image router.post('/avatar', authenticateToken, async (req, res) => { try { const buffer = await new Promise((resolve, reject) => { const chunks = []; req.on('data', chunk => chunks.push(chunk)); req.on('end', () => resolve(Buffer.concat(chunks))); req.on('error', reject); }); // Validate content type const contentType = req.headers['content-type']; if (!contentType || !contentType.startsWith('image/')) { return res.status(400).json({ error: 'Nur Bilddateien sind erlaubt' }); } // Max 2MB if (buffer.length > 2 * 1024 * 1024) { return res.status(400).json({ error: 'Bild darf maximal 2MB groß sein' }); } const ext = contentType.includes('png') ? 'png' : contentType.includes('gif') ? 'gif' : contentType.includes('webp') ? 'webp' : 'jpg'; const filename = `${req.user.id}_${Date.now()}.${ext}`; const filepath = path.join(uploadsDir, filename); // Remove old avatar if exists const db = getDb(); const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]); if (current?.avatar_image) { const oldPath = path.join(uploadsDir, current.avatar_image); if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); } fs.writeFileSync(filepath, buffer); await db.run('UPDATE users SET avatar_image = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [filename, req.user.id]); const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); res.json({ user: updated }); } catch (err) { console.error('Avatar upload error:', err); res.status(500).json({ error: 'Avatar konnte nicht hochgeladen werden' }); } }); // DELETE /api/auth/avatar - Remove avatar image router.delete('/avatar', authenticateToken, async (req, res) => { try { const db = getDb(); const current = await db.get('SELECT avatar_image FROM users WHERE id = ?', [req.user.id]); if (current?.avatar_image) { const oldPath = path.join(uploadsDir, current.avatar_image); if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath); } await db.run('UPDATE users SET avatar_image = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]); const updated = await db.get('SELECT id, name, email, role, theme, language, avatar_color, avatar_image FROM users WHERE id = ?', [req.user.id]); res.json({ user: updated }); } catch (err) { console.error('Avatar delete error:', err); res.status(500).json({ error: 'Avatar konnte nicht entfernt werden' }); } }); // GET /api/auth/avatar/:filename - Serve avatar image router.get('/avatar/:filename', (req, res) => { const filepath = path.join(uploadsDir, req.params.filename); if (!fs.existsSync(filepath)) { return res.status(404).json({ error: 'Avatar nicht gefunden' }); } const ext = path.extname(filepath).slice(1); const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' }; res.setHeader('Content-Type', mimeMap[ext] || 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=86400'); fs.createReadStream(filepath).pipe(res); }); export default router;