220 lines
7.9 KiB
JavaScript
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;
|