Update language, add LICENSE and README
All checks were successful
Build & Push Docker Image / build (push) Successful in 1m9s

This commit is contained in:
2026-02-24 21:04:19 +01:00
parent 2ef6a9f30b
commit 7426ae8088
8 changed files with 668 additions and 106 deletions

View File

@@ -25,17 +25,17 @@ router.post('/register', async (req, res) => {
const { name, email, password } = req.body;
if (!name || !email || !password) {
return res.status(400).json({ error: 'Alle Felder sind erforderlich' });
return res.status(400).json({ error: 'All fields are required' });
}
if (password.length < 6) {
return res.status(400).json({ error: 'Passwort muss mindestens 6 Zeichen lang sein' });
return res.status(400).json({ error: 'Password must be at least 6 characters long' });
}
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' });
return res.status(409).json({ error: 'Email is already in use' });
}
const hash = bcrypt.hashSync(password, 12);
@@ -63,7 +63,7 @@ router.post('/register', async (req, res) => {
await sendVerificationEmail(email.toLowerCase(), name, verifyUrl, appName);
return res.status(201).json({ needsVerification: true, message: 'Verifizierungs-E-Mail wurde gesendet' });
return res.status(201).json({ needsVerification: true, message: 'Verification email has been sent' });
}
// No SMTP configured register and login immediately (legacy behaviour)
@@ -78,7 +78,7 @@ router.post('/register', async (req, res) => {
res.status(201).json({ token, user });
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Registrierung fehlgeschlagen' });
res.status(500).json({ error: 'Registration failed' });
}
});
@@ -87,7 +87,7 @@ router.get('/verify-email', async (req, res) => {
try {
const { token } = req.query;
if (!token) {
return res.status(400).json({ error: 'Token fehlt' });
return res.status(400).json({ error: 'Token is missing' });
}
const db = getDb();
@@ -97,11 +97,11 @@ router.get('/verify-email', async (req, res) => {
);
if (!user) {
return res.status(400).json({ error: 'Ungültiger oder bereits verwendeter Token' });
return res.status(400).json({ error: 'Invalid or already used token' });
}
if (new Date(user.verification_token_expires) < new Date()) {
return res.status(400).json({ error: 'Token ist abgelaufen. Bitte registriere dich erneut.' });
return res.status(400).json({ error: 'Token has expired. Please register again.' });
}
await db.run(
@@ -109,10 +109,10 @@ router.get('/verify-email', async (req, res) => {
[user.id]
);
res.json({ verified: true, message: 'E-Mail erfolgreich verifiziert' });
res.json({ verified: true, message: 'Email verified successfully' });
} catch (err) {
console.error('Verify email error:', err);
res.status(500).json({ error: 'Verifizierung fehlgeschlagen' });
res.status(500).json({ error: 'Verification failed' });
}
});
@@ -121,11 +121,11 @@ router.post('/resend-verification', async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'E-Mail ist erforderlich' });
return res.status(400).json({ error: 'Email is required' });
}
if (!isMailerConfigured()) {
return res.status(400).json({ error: 'SMTP ist nicht konfiguriert' });
return res.status(400).json({ error: 'SMTP is not configured' });
}
const db = getDb();
@@ -133,7 +133,7 @@ router.post('/resend-verification', async (req, res) => {
if (!user || user.email_verified) {
// Don't reveal whether account exists
return res.json({ message: 'Falls ein Konto existiert, wurde eine neue E-Mail gesendet.' });
return res.json({ message: 'If an account exists, a new email has been sent.' });
}
const verificationToken = uuidv4();
@@ -155,10 +155,10 @@ router.post('/resend-verification', async (req, res) => {
await sendVerificationEmail(email.toLowerCase(), user.name, verifyUrl, appName);
res.json({ message: 'Falls ein Konto existiert, wurde eine neue E-Mail gesendet.' });
res.json({ message: 'If an account exists, a new email has been sent.' });
} catch (err) {
console.error('Resend verification error:', err);
res.status(500).json({ error: 'E-Mail konnte nicht gesendet werden' });
res.status(500).json({ error: 'Email could not be sent' });
}
});
@@ -168,18 +168,18 @@ router.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'E-Mail und Passwort sind erforderlich' });
return res.status(400).json({ error: 'Email and password are required' });
}
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' });
return res.status(401).json({ error: 'Invalid credentials' });
}
if (!user.email_verified && isMailerConfigured()) {
return res.status(403).json({ error: 'E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach.', needsVerification: true });
return res.status(403).json({ error: 'Email address not yet verified. Please check your inbox.', needsVerification: true });
}
const token = generateToken(user.id);
@@ -188,7 +188,7 @@ router.post('/login', async (req, res) => {
res.json({ token, user: safeUser });
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Anmeldung fehlgeschlagen' });
res.status(500).json({ error: 'Login failed' });
}
});
@@ -206,7 +206,7 @@ router.put('/profile', authenticateToken, async (req, res) => {
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' });
return res.status(409).json({ error: 'Email is already in use' });
}
}
@@ -225,7 +225,7 @@ router.put('/profile', authenticateToken, async (req, res) => {
res.json({ user: updated });
} catch (err) {
console.error('Profile update error:', err);
res.status(500).json({ error: 'Profil konnte nicht aktualisiert werden' });
res.status(500).json({ error: 'Profile could not be updated' });
}
});
@@ -237,20 +237,20 @@ router.put('/password', authenticateToken, async (req, res) => {
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' });
return res.status(401).json({ error: 'Current password is incorrect' });
}
if (newPassword.length < 6) {
return res.status(400).json({ error: 'Neues Passwort muss mindestens 6 Zeichen lang sein' });
return res.status(400).json({ error: 'New password must be at least 6 characters long' });
}
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' });
res.json({ message: 'Password changed successfully' });
} catch (err) {
console.error('Password change error:', err);
res.status(500).json({ error: 'Passwort konnte nicht geändert werden' });
res.status(500).json({ error: 'Password could not be changed' });
}
});
@@ -267,12 +267,12 @@ router.post('/avatar', authenticateToken, async (req, res) => {
// Validate content type
const contentType = req.headers['content-type'];
if (!contentType || !contentType.startsWith('image/')) {
return res.status(400).json({ error: 'Nur Bilddateien sind erlaubt' });
return res.status(400).json({ error: 'Only image files are allowed' });
}
// Max 2MB
if (buffer.length > 2 * 1024 * 1024) {
return res.status(400).json({ error: 'Bild darf maximal 2MB groß sein' });
return res.status(400).json({ error: 'Image must not exceed 2MB' });
}
const ext = contentType.includes('png') ? 'png' : contentType.includes('gif') ? 'gif' : contentType.includes('webp') ? 'webp' : 'jpg';
@@ -295,7 +295,7 @@ router.post('/avatar', authenticateToken, async (req, res) => {
res.json({ user: updated });
} catch (err) {
console.error('Avatar upload error:', err);
res.status(500).json({ error: 'Avatar konnte nicht hochgeladen werden' });
res.status(500).json({ error: 'Avatar could not be uploaded' });
}
});
@@ -313,7 +313,7 @@ router.delete('/avatar', authenticateToken, async (req, res) => {
res.json({ user: updated });
} catch (err) {
console.error('Avatar delete error:', err);
res.status(500).json({ error: 'Avatar konnte nicht entfernt werden' });
res.status(500).json({ error: 'Avatar could not be removed' });
}
});
@@ -351,7 +351,7 @@ function generateColorFromName(name) {
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' });
return res.status(404).json({ error: 'Avatar not found' });
}
const ext = path.extname(filepath).slice(1);
const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp' };