Files
hotel777-manager/server.js
2026-05-03 18:59:12 +05:00

251 lines
9.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`);
});