Add mail verification and use .env insteads of environment in compose
Some checks failed
Build & Push Docker Image / build (push) Has been cancelled

This commit is contained in:
2026-02-24 20:35:08 +01:00
parent 3898bf1b4b
commit 8be973a166
14 changed files with 388 additions and 19 deletions

View File

@@ -1,10 +1,12 @@
import { Router } from 'express';
import bcrypt from 'bcryptjs';
import { v4 as uuidv4 } from 'uuid';
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';
import { isMailerConfigured, sendVerificationEmail } from '../config/mailer.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -37,8 +39,36 @@ router.post('/register', async (req, res) => {
}
const hash = bcrypt.hashSync(password, 12);
// If SMTP is configured, require email verification
if (isMailerConfigured()) {
const verificationToken = uuidv4();
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await db.run(
'INSERT INTO users (name, email, password_hash, email_verified, verification_token, verification_token_expires) VALUES (?, ?, ?, 0, ?, ?)',
[name, email.toLowerCase(), hash, verificationToken, expires]
);
// Build verification URL
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
// Load app name from branding settings
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'branding'");
let appName = 'Redlight';
if (brandingSetting?.value) {
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
}
await sendVerificationEmail(email.toLowerCase(), name, verifyUrl, appName);
return res.status(201).json({ needsVerification: true, message: 'Verifizierungs-E-Mail wurde gesendet' });
}
// No SMTP configured register and login immediately (legacy behaviour)
const result = await db.run(
'INSERT INTO users (name, email, password_hash) VALUES (?, ?, ?)',
'INSERT INTO users (name, email, password_hash, email_verified) VALUES (?, ?, ?, 1)',
[name, email.toLowerCase(), hash]
);
@@ -52,6 +82,86 @@ router.post('/register', async (req, res) => {
}
});
// GET /api/auth/verify-email?token=...
router.get('/verify-email', async (req, res) => {
try {
const { token } = req.query;
if (!token) {
return res.status(400).json({ error: 'Token fehlt' });
}
const db = getDb();
const user = await db.get(
'SELECT id, verification_token_expires FROM users WHERE verification_token = ? AND email_verified = 0',
[token]
);
if (!user) {
return res.status(400).json({ error: 'Ungültiger oder bereits verwendeter Token' });
}
if (new Date(user.verification_token_expires) < new Date()) {
return res.status(400).json({ error: 'Token ist abgelaufen. Bitte registriere dich erneut.' });
}
await db.run(
'UPDATE users SET email_verified = 1, verification_token = NULL, verification_token_expires = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[user.id]
);
res.json({ verified: true, message: 'E-Mail erfolgreich verifiziert' });
} catch (err) {
console.error('Verify email error:', err);
res.status(500).json({ error: 'Verifizierung fehlgeschlagen' });
}
});
// POST /api/auth/resend-verification
router.post('/resend-verification', async (req, res) => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({ error: 'E-Mail ist erforderlich' });
}
if (!isMailerConfigured()) {
return res.status(400).json({ error: 'SMTP ist nicht konfiguriert' });
}
const db = getDb();
const user = await db.get('SELECT id, name, email_verified FROM users WHERE email = ?', [email.toLowerCase()]);
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.' });
}
const verificationToken = uuidv4();
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
await db.run(
'UPDATE users SET verification_token = ?, verification_token_expires = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
[verificationToken, expires, user.id]
);
const baseUrl = process.env.APP_URL || `${req.protocol}://${req.get('host')}`;
const verifyUrl = `${baseUrl}/verify-email?token=${verificationToken}`;
const brandingSetting = await db.get("SELECT value FROM settings WHERE key = 'branding'");
let appName = 'Redlight';
if (brandingSetting?.value) {
try { appName = JSON.parse(brandingSetting.value).appName || appName; } catch {}
}
await sendVerificationEmail(email.toLowerCase(), user.name, verifyUrl, appName);
res.json({ message: 'Falls ein Konto existiert, wurde eine neue E-Mail gesendet.' });
} catch (err) {
console.error('Resend verification error:', err);
res.status(500).json({ error: 'E-Mail konnte nicht gesendet werden' });
}
});
// POST /api/auth/login
router.post('/login', async (req, res) => {
try {
@@ -68,6 +178,10 @@ router.post('/login', async (req, res) => {
return res.status(401).json({ error: 'Ungültige Anmeldedaten' });
}
if (!user.email_verified && isMailerConfigured()) {
return res.status(403).json({ error: 'E-Mail-Adresse noch nicht verifiziert. Bitte prüfe dein Postfach.', needsVerification: true });
}
const token = generateToken(user.id);
const { password_hash, ...safeUser } = user;