import { logger, apiLogger } from '../utils/logger.js'; import express from 'express'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import speakeasy from 'speakeasy'; import QRCode from 'qrcode'; import CryptoJS from 'crypto-js'; import nodemailer from 'nodemailer'; import { dbRun, dbGet } from '../database/init.js'; import { authenticate } from '../middleware/auth.js'; import { success, badRequest, unauthorized, serverError } from '../utils/response.js'; import { getSecret } from '../services/secrets/getSecret.js'; const router = express.Router(); // Register router.post('/register', async (req, res) => { try { const { email, password, name } = req.body; const existing = await dbGet('SELECT id FROM users WHERE email = ?', [email]); if (existing) { return badRequest(res, 'User already exists'); } const passwordHash = await bcrypt.hash(password, 10); const result = await dbRun( 'INSERT INTO users (email, password_hash, name, role) VALUES (?, ?, ?, ?)', [email, passwordHash, name, 'admin'] ); const userId = result.lastInsertRowid; // Create default business/workspace for the user (if not exists) const businessResult = await dbRun( 'INSERT INTO businesses (name, description, owner_id) VALUES (?, ?, ?)', ['My Workspace', 'Personal workspace', userId] ); const businessId = businessResult.lastID; // Encrypt the password for storage in Password Manager const encryptionKey = process.env.ENCRYPTION_KEY || process.env.JWT_SECRET; const encryptedPassword = CryptoJS.AES.encrypt(password, encryptionKey).toString(); const encryptedLogin = CryptoJS.AES.encrypt(email, encryptionKey).toString(); // Create first entry in Password Manager: GOD CRM Account await dbRun( `INSERT INTO services ( business_id, name, description, type, login_encrypted, password_encrypted, url ) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ businessId, 'GOD CRM Account', 'Your account credentials for this GOD CRM workspace', 'System', encryptedLogin, encryptedPassword, 'http://localhost:3000' ] ); return success(res, { userId }, 'Account created. Default workspace and password entry added.'); } catch (err) { logger.error('Registration error:', err); return serverError(res, err.message); } }); // Login router.post('/login', async (req, res) => { try { const { email, password, totpCode } = req.body; const user = await dbGet('SELECT * FROM users WHERE email = ?', [email]); if (!user) { return unauthorized(res, 'Invalid credentials'); } const validPassword = await bcrypt.compare(password, user.password_hash); if (!validPassword) { return unauthorized(res, 'Invalid credentials'); } // Check 2FA if enabled if (user.totp_enabled) { if (!totpCode) { return success(res, { requireTotp: true }); } const verified = speakeasy.totp.verify({ secret: user.totp_secret, encoding: 'base32', token: totpCode }); if (!verified) { return unauthorized(res, 'Invalid 2FA code'); } } const token = jwt.sign( { id: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: '7d' } ); return success(res, { token, user: { id: user.id, email: user.email, name: user.name, role: user.role } }); } catch (err) { return serverError(res, err.message); } }); // Setup 2FA router.post('/setup-2fa', authenticate, async (req, res) => { try { const secret = speakeasy.generateSecret({ name: `Business CRM (${req.user.email})` }); await dbRun( 'UPDATE users SET totp_secret = ? WHERE id = ?', [secret.base32, req.user.id] ); const qrCode = await QRCode.toDataURL(secret.otpauth_url); return success(res, { secret: secret.base32, qrCode }); } catch (err) { return serverError(res, err.message); } }); // Verify and enable 2FA router.post('/verify-2fa', authenticate, async (req, res) => { try { const { code } = req.body; const user = await dbGet('SELECT totp_secret FROM users WHERE id = ?', [req.user.id]); const verified = speakeasy.totp.verify({ secret: user.totp_secret, encoding: 'base32', token: code }); if (!verified) { return badRequest(res, 'Invalid code'); } await dbRun('UPDATE users SET totp_enabled = 1 WHERE id = ?', [req.user.id]); return success(res, null); } catch (err) { return serverError(res, err.message); } }); // Get current user router.get('/me', authenticate, async (req, res) => { try { const user = await dbGet( 'SELECT id, email, name, role, totp_enabled FROM users WHERE id = ?', [req.user.id] ); return success(res, user); } catch (err) { return serverError(res, err.message); } }); // Forgot password - send reset email router.post('/forgot-password', async (req, res) => { try { const { email } = req.body; logger.info('🔐 Password reset request for:', email); const user = await dbGet('SELECT id, email, name FROM users WHERE email = ?', [email]); if (!user) { // По соображениям безопасности не сообщаем что пользователь не найден return success(res, null, 'If the email exists, a reset link will be sent'); } // Генерируем токен для сброса пароля const resetToken = jwt.sign( { id: user.id, email: user.email, purpose: 'password-reset' }, process.env.JWT_SECRET, { expiresIn: '1h' } ); const resetLink = `${process.env.APP_URL || 'http://localhost:5173'}/reset-password?token=${resetToken}`; logger.info('🔗 Reset link:', resetLink); // Отправка email через nodemailer try { // ADR-0040: SMTP creds via vault (env fallback during transition). const smtpUser = await getSecret('smtp_user', 'SMTP_USER'); const smtpPass = await getSecret('smtp_pass', 'SMTP_PASS'); const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'smtp.gmail.com', port: parseInt(process.env.SMTP_PORT) || 587, secure: process.env.SMTP_SECURE === 'true', auth: { user: smtpUser, pass: smtpPass } }); const mailOptions = { from: process.env.SMTP_FROM || smtpUser, to: user.email, subject: 'Password Reset - GOD CRM', html: `
Hello ${user.name},
You requested to reset your password for GOD CRM.
Click the button below to reset your password:
Or copy this link to your browser:
${resetLink}
This link will expire in 1 hour.
If you didn't request this, please ignore this email.