require('dotenv').config(); const express = require('express'); const path = require('path'); const bcrypt = require('bcrypt'); const cron = require('node-cron'); const { db, normalizePhone, logAction } = require('./db'); const { sessionMiddleware, ensureAdmin, requireAdmin } = require('./auth'); const { syncBookings } = require('./sync'); const { notifyBookingUpdate, verifyEmailConnection } = require('./mailer'); const app = express(); const PORT = process.env.PORT || 3000; // Middleware app.use(express.json()); app.use(sessionMiddleware); // Статика из public app.use(express.static(path.join(__dirname, 'public'))); // === Инициализация === // Асинхронное создание/обновление администратора из .env (async () => { await ensureAdmin(); })(); // Проверка работоспособности SMTP при старте (отключит уведомления при ошибке) verifyEmailConnection().catch(err => { console.error('Ошибка при проверке SMTP:', err); }); // === API === // Вход администратора (с хэшированием и сохранением adminId) app.post('/api/login', async (req, res) => { const { login, password } = req.body; const admin = db.prepare('SELECT id, login, password FROM admins WHERE login = ?').get(login); if (admin && await bcrypt.compare(password, admin.password)) { req.session.isAdmin = true; req.session.adminId = admin.id; res.json({ success: true }); } else { res.status(401).json({ error: 'Неверный логин или пароль' }); } }); // Выход app.post('/api/logout', (req, res) => { req.session.destroy(); res.json({ success: true }); }); // Проверка сессии app.get('/api/me', (req, res) => { res.json({ isAdmin: !!req.session.isAdmin }); }); // --- Управление администраторами (CRUD) --- // Получить список всех администраторов (только id и login) app.get('/api/admins', requireAdmin, (req, res) => { const admins = db.prepare('SELECT id, login FROM admins ORDER BY id').all(); res.json(admins); }); // Добавить нового администратора app.post('/api/admins', requireAdmin, async (req, res) => { const { login, password } = req.body; if (!login || !password) { return res.status(400).json({ error: 'Логин и пароль обязательны' }); } const existing = db.prepare('SELECT id FROM admins WHERE login = ?').get(login); if (existing) { return res.status(409).json({ error: 'Администратор с таким логином уже существует' }); } const saltRounds = 10; const hashed = await bcrypt.hash(password, saltRounds); const stmt = db.prepare('INSERT INTO admins (login, password) VALUES (?, ?)'); const info = stmt.run(login, hashed); res.status(201).json({ id: info.lastInsertRowid, login }); }); // Изменить пароль администратора app.put('/api/admins/:id', requireAdmin, async (req, res) => { const { id } = req.params; const { password } = req.body; if (!password) { return res.status(400).json({ error: 'Новый пароль обязателен' }); } const admin = db.prepare('SELECT id, login FROM admins WHERE id = ?').get(id); if (!admin) { return res.status(404).json({ error: 'Администратор не найден' }); } const saltRounds = 10; const hashed = await bcrypt.hash(password, saltRounds); db.prepare('UPDATE admins SET password = ? WHERE id = ?').run(hashed, id); res.json({ id: admin.id, login: admin.login }); }); // Удалить администратора app.delete('/api/admins/:id', requireAdmin, (req, res) => { const { id } = req.params; // Проверка: нельзя удалить последнего администратора const count = db.prepare('SELECT COUNT(*) as cnt FROM admins').get().cnt; if (count <= 1) { return res.status(400).json({ error: 'Нельзя удалить последнего администратора' }); } // Нельзя удалить самого себя if (req.session.adminId && req.session.adminId == id) { return res.status(400).json({ error: 'Нельзя удалить свою учётную запись' }); } const stmt = db.prepare('DELETE FROM admins WHERE id = ?'); const result = stmt.run(id); if (result.changes === 0) { return res.status(404).json({ error: 'Администратор не найден' }); } res.json({ success: true }); }); // --- Заявки (bookings) --- // Список заявок с фильтрами app.get('/api/bookings', requireAdmin, (req, res) => { const { status, search, client_id } = req.query; let query = 'SELECT * FROM bookings WHERE 1=1'; const params = []; if (status) { query += ' AND status = ?'; params.push(status); } if (client_id) { query += ' AND user_id = ?'; params.push(client_id); } if (search) { query += ' AND (name LIKE ? OR phone_raw LIKE ? OR comments LIKE ?)'; params.push(`%${search}%`, `%${search}%`, `%${search}%`); } query += ' ORDER BY created_at DESC'; const bookings = db.prepare(query).all(...params); res.json(bookings); }); // Детали заявки app.get('/api/bookings/:id', requireAdmin, (req, res) => { const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(req.params.id); if (!booking) return res.status(404).json({ error: 'Заявка не найдена' }); res.json(booking); }); // Обновление заявки (статус, комментарий, перепривязка клиента) app.put('/api/bookings/:id', requireAdmin, (req, res) => { const { id } = req.params; const { status, comments, phone } = req.body; const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id); if (!booking) return res.status(404).json({ error: 'Заявка не найдена' }); const changes = {}; // Обновление статуса if (status && status !== booking.status) { const allowed = ['Новая', 'В работе', 'Подтверждена', 'Заселение', 'Завершена', 'Отменена']; if (!allowed.includes(status)) { return res.status(400).json({ error: 'Недопустимый статус' }); } db.prepare('UPDATE bookings SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(status, id); changes.status = { from: booking.status, to: status }; } // Обновление комментария if (comments !== undefined && comments !== booking.comments) { db.prepare('UPDATE bookings SET comments = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(comments, id); changes.comments = { from: booking.comments, to: comments }; } // Перепривязка к другой карточке по номеру телефона if (phone) { const normPhone = normalizePhone(phone); const user = db.prepare('SELECT id FROM users WHERE phone = ?').get(normPhone); if (!user) { return res.status(400).json({ error: 'Карточка клиента с таким телефоном не найдена' }); } if (user.id !== booking.user_id) { db.prepare('UPDATE bookings SET user_id = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?').run(user.id, id); changes.user_id = { from: booking.user_id, to: user.id }; } } // Если были изменения – логируем и шлём уведомление if (Object.keys(changes).length > 0) { logAction(`Изменение заявки #${id}`, changes); const updated = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id); notifyBookingUpdate(updated, changes); } const updatedBooking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id); res.json(updatedBooking); }); // --- Карточки клиентов --- // Список клиентов app.get('/api/clients', requireAdmin, (req, res) => { const { search } = req.query; let query = 'SELECT * FROM users WHERE 1=1'; const params = []; if (search) { query += ' AND (phone LIKE ? OR name LIKE ?)'; params.push(`%${search}%`, `%${search}%`); } query += ' ORDER BY name ASC'; const clients = db.prepare(query).all(...params); res.json(clients); }); // Профиль клиента с его заявками app.get('/api/clients/:id', requireAdmin, (req, res) => { const client = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id); if (!client) return res.status(404).json({ error: 'Клиент не найден' }); const bookings = db.prepare(` SELECT * FROM bookings WHERE user_id = ? ORDER BY created_at DESC, status ASC `).all(req.params.id); res.json({ client, bookings }); }); // --- Синхронизация --- // Ручной запуск синхронизации app.post('/api/admin/sync', requireAdmin, async (req, res) => { await syncBookings(); res.json({ success: true, message: 'Синхронизация запущена' }); }); // Планировщик синхронизации каждые 5 минут cron.schedule('*/5 * * * *', () => { console.log('Автосинхронизация...'); syncBookings().catch(console.error); }); // Запуск сервера app.listen(PORT, () => { console.log(`Сервис "${process.env.SERVICE_NAME}" запущен на порту ${PORT}`); console.log(`Ссылка: ${process.env.SERVICE_URL}`); });