Files
redlight/server/routes/auth.js
Michelle 54d6ee553a
Some checks failed
Build & Push Docker Image / build (push) Failing after 53s
Init v1.0.0
2026-02-24 18:14:16 +01:00

220 lines
7.9 KiB
JavaScript

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;