This commit is contained in:
219
server/routes/auth.js
Normal file
219
server/routes/auth.js
Normal file
@@ -0,0 +1,219 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user