1
This commit is contained in:
24
auth.js
24
auth.js
@@ -1,4 +1,5 @@
|
||||
const session = require('express-session');
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db } = require('./db');
|
||||
|
||||
const sessionMiddleware = session({
|
||||
@@ -8,23 +9,30 @@ const sessionMiddleware = session({
|
||||
cookie: { secure: false, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }
|
||||
});
|
||||
|
||||
function ensureAdmin() {
|
||||
/**
|
||||
* Гарантирует существование администратора, заданного переменными окружения
|
||||
* (ADMIN_LOGIN / ADMIN_PASSWORD). Пароль всегда хэшируется.
|
||||
*/
|
||||
async function ensureAdmin() {
|
||||
const login = process.env.ADMIN_LOGIN || 'admin';
|
||||
const password = process.env.ADMIN_PASSWORD || 'admin';
|
||||
const saltRounds = 10;
|
||||
|
||||
// Таблица admins уже создана в db.js, можно сразу искать
|
||||
const admin = db.prepare('SELECT id FROM admins WHERE login = ?').get(login);
|
||||
const hashed = await bcrypt.hash(password, saltRounds);
|
||||
|
||||
if (admin) {
|
||||
// Обновить пароль при каждом старте
|
||||
db.prepare('UPDATE admins SET password = ? WHERE login = ?').run(password, login);
|
||||
console.log(`Пароль администратора "${login}" обновлён`);
|
||||
db.prepare('UPDATE admins SET password = ? WHERE login = ?').run(hashed, login);
|
||||
console.log(`Пароль администратора "${login}" обновлён (хэширован)`);
|
||||
} else {
|
||||
// Создать запись
|
||||
db.prepare('INSERT INTO admins (login, password) VALUES (?, ?)').run(login, password);
|
||||
console.log(`Администратор "${login}" создан`);
|
||||
db.prepare('INSERT INTO admins (login, password) VALUES (?, ?)').run(login, hashed);
|
||||
console.log(`Администратор "${login}" создан (хэширован)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware для проверки прав администратора.
|
||||
*/
|
||||
function requireAdmin(req, res, next) {
|
||||
if (req.session && req.session.isAdmin) {
|
||||
return next();
|
||||
|
||||
72
mailer.js
72
mailer.js
@@ -1,20 +1,69 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
let transporter = null;
|
||||
let emailEnabled = true; // по умолчанию включено, при ошибке станет false
|
||||
let verificationDone = false; // проверка выполнена хотя бы раз
|
||||
|
||||
function createTransporter() {
|
||||
if (!process.env.SMTP_HOST || !process.env.SMTP_USER || !process.env.SMTP_PASS) {
|
||||
console.warn('[MAILER] SMTP не настроен (отсутствуют переменные окружения). Почтовые уведомления отключены.');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Функция проверки соединения с почтовым сервером
|
||||
async function verifyEmailConnection() {
|
||||
if (verificationDone) return emailEnabled;
|
||||
|
||||
transporter = createTransporter();
|
||||
if (!transporter) {
|
||||
emailEnabled = false;
|
||||
verificationDone = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await transporter.verify();
|
||||
console.log('[MAILER] SMTP подключение успешно, уведомления активны.');
|
||||
emailEnabled = true;
|
||||
} catch (err) {
|
||||
console.error('[MAILER] Ошибка подключения к SMTP:', err.message);
|
||||
console.warn('[MAILER] Почтовые уведомления будут отключены до следующего перезапуска.');
|
||||
emailEnabled = false;
|
||||
transporter = null; // сбросим, чтобы не использовать нерабочий
|
||||
} finally {
|
||||
verificationDone = true;
|
||||
}
|
||||
return emailEnabled;
|
||||
}
|
||||
|
||||
async function sendNotification(emails, subject, text) {
|
||||
if (!emailEnabled) {
|
||||
// Молча пропускаем, т.к. проверка уже показала неработоспособность
|
||||
return;
|
||||
}
|
||||
if (!emails) return;
|
||||
const recipients = emails.split(',').map(e => e.trim()).filter(Boolean);
|
||||
if (recipients.length === 0) return;
|
||||
|
||||
// Если транспортер ещё не создан (не было вызова verify), создадим его
|
||||
if (!transporter) {
|
||||
transporter = createTransporter();
|
||||
if (!transporter) {
|
||||
emailEnabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: process.env.SMTP_USER,
|
||||
@@ -25,6 +74,11 @@ async function sendNotification(emails, subject, text) {
|
||||
console.log(`Уведомление отправлено на ${recipients.join(', ')}`);
|
||||
} catch (err) {
|
||||
console.error('Ошибка отправки письма:', err);
|
||||
// Если при отправке возникла ошибка, считаем почту нерабочей до перезапуска
|
||||
if (emailEnabled) {
|
||||
console.warn('[MAILER] Отключение почтовых уведомлений из-за ошибки отправки.');
|
||||
emailEnabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +103,4 @@ async function notifyBookingUpdate(booking, changes) {
|
||||
await sendNotification(process.env.EMAIL_NOTIFY_UPDATE, subject, text);
|
||||
}
|
||||
|
||||
module.exports = { notifyNewBooking, notifyBookingUpdate };
|
||||
module.exports = { verifyEmailConnection, notifyNewBooking, notifyBookingUpdate };
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -6,6 +6,7 @@
|
||||
"": {
|
||||
"dependencies": {
|
||||
"axios": "^1.16.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
@@ -87,6 +88,20 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/better-sqlite3": {
|
||||
"version": "12.9.0",
|
||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
|
||||
@@ -1067,6 +1082,17 @@
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.7",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.16.0",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.9.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru" data-title="Панель управления" data-description="Администрирование заявок гостиницы">
|
||||
<html lang="ru" data-title="Панель управления" data-description="Администрирование заявок и пользователей гостиницы">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@@ -12,6 +12,40 @@
|
||||
<p>Сервис: <span id="serviceName"></span></p>
|
||||
<button id="syncBtn">Запустить синхронизацию заявок</button>
|
||||
<p id="syncStatus"></p>
|
||||
|
||||
<hr style="margin: 2rem 0;">
|
||||
|
||||
<h2>Управление администраторами</h2>
|
||||
<table id="adminsTable">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>Логин</th><th>Действия</th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
|
||||
<h3>Добавить нового администратора</h3>
|
||||
<form id="addAdminForm">
|
||||
<label>Логин</label>
|
||||
<input type="text" id="newLogin" required>
|
||||
<label>Пароль</label>
|
||||
<input type="password" id="newPassword" required>
|
||||
<button type="submit">Добавить</button>
|
||||
</form>
|
||||
|
||||
<!-- Модальное окно для смены пароля -->
|
||||
<div id="editPasswordModal" class="modal" style="display:none;">
|
||||
<div class="modal-content">
|
||||
<h2>Смена пароля</h2>
|
||||
<form id="editPasswordForm">
|
||||
<input type="hidden" id="editAdminId">
|
||||
<label>Новый пароль</label>
|
||||
<input type="password" id="editPassword" required>
|
||||
<button type="submit">Сохранить</button>
|
||||
<button type="button" id="closePasswordModal">Отмена</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<script src="nav.js"></script>
|
||||
<script src="seo.js"></script>
|
||||
@@ -21,6 +55,7 @@
|
||||
if (!data.isAdmin) window.location.href = '/login.html';
|
||||
});
|
||||
|
||||
// --- Синхронизация ---
|
||||
document.getElementById('syncBtn').addEventListener('click', async () => {
|
||||
const status = document.getElementById('syncStatus');
|
||||
status.textContent = 'Синхронизация...';
|
||||
@@ -28,6 +63,106 @@
|
||||
if (res.ok) status.textContent = 'Синхронизация завершена';
|
||||
else status.textContent = 'Ошибка синхронизации';
|
||||
});
|
||||
|
||||
// --- Управление администраторами ---
|
||||
const adminsTbody = document.querySelector('#adminsTable tbody');
|
||||
const addForm = document.getElementById('addAdminForm');
|
||||
const modal = document.getElementById('editPasswordModal');
|
||||
const closeModalBtn = document.getElementById('closePasswordModal');
|
||||
|
||||
async function loadAdmins() {
|
||||
const res = await fetch('/api/admins');
|
||||
const admins = await res.json();
|
||||
adminsTbody.innerHTML = '';
|
||||
for (const admin of admins) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${admin.id}</td>
|
||||
<td>${escapeHtml(admin.login)}</td>
|
||||
<td>
|
||||
<button class="changePasswordBtn" data-id="${admin.id}" data-login="${escapeHtml(admin.login)}">Сменить пароль</button>
|
||||
<button class="deleteAdminBtn" data-id="${admin.id}">Удалить</button>
|
||||
</td>
|
||||
`;
|
||||
adminsTbody.appendChild(tr);
|
||||
}
|
||||
// Привязываем обработчики к динамическим кнопкам
|
||||
document.querySelectorAll('.changePasswordBtn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.getElementById('editAdminId').value = btn.dataset.id;
|
||||
document.getElementById('editPassword').value = '';
|
||||
modal.style.display = 'flex';
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('.deleteAdminBtn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const id = btn.dataset.id;
|
||||
if (confirm('Удалить администратора? Вы не сможете удалить самого себя и последнего админа.')) {
|
||||
const res = await fetch(`/api/admins/${id}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
loadAdmins();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Ошибка: ' + (err.error || 'неизвестная'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const login = document.getElementById('newLogin').value.trim();
|
||||
const password = document.getElementById('newPassword').value;
|
||||
const res = await fetch('/api/admins', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ login, password })
|
||||
});
|
||||
if (res.ok) {
|
||||
alert('Администратор добавлен');
|
||||
addForm.reset();
|
||||
loadAdmins();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Ошибка: ' + (err.error || 'неизвестная'));
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('editPasswordForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('editAdminId').value;
|
||||
const password = document.getElementById('editPassword').value;
|
||||
const res = await fetch(`/api/admins/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
if (res.ok) {
|
||||
alert('Пароль изменён');
|
||||
modal.style.display = 'none';
|
||||
loadAdmins();
|
||||
} else {
|
||||
const err = await res.json();
|
||||
alert('Ошибка: ' + (err.error || 'неизвестная'));
|
||||
}
|
||||
});
|
||||
|
||||
closeModalBtn.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,3 +21,6 @@ label { font-weight: 500; }
|
||||
header { padding: 0.5rem 1rem; }
|
||||
nav { flex-direction: column; }
|
||||
}
|
||||
hr { margin: 2rem 0; border: none; border-top: 1px solid #ddd; }
|
||||
#adminsTable button { margin-right: 0.5rem; }
|
||||
#addAdminForm { margin-top: 1rem; background: #fff; padding: 1rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.05); max-width: 400px; }
|
||||
107
server.js
107
server.js
@@ -1,11 +1,13 @@
|
||||
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 } = require('./mailer');
|
||||
const cron = require('node-cron');
|
||||
const { notifyBookingUpdate, verifyEmailConnection } = require('./mailer');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
@@ -17,17 +19,26 @@ app.use(sessionMiddleware);
|
||||
// Статика из public
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Инициализация администратора
|
||||
ensureAdmin();
|
||||
// === Инициализация ===
|
||||
// Асинхронное создание/обновление администратора из .env
|
||||
(async () => {
|
||||
await ensureAdmin();
|
||||
})();
|
||||
|
||||
// Проверка работоспособности SMTP при старте (отключит уведомления при ошибке)
|
||||
verifyEmailConnection().catch(err => {
|
||||
console.error('Ошибка при проверке SMTP:', err);
|
||||
});
|
||||
|
||||
// === API ===
|
||||
|
||||
// Вход администратора
|
||||
app.post('/api/login', (req, res) => {
|
||||
// Вход администратора (с хэшированием и сохранением adminId)
|
||||
app.post('/api/login', async (req, res) => {
|
||||
const { login, password } = req.body;
|
||||
const admin = db.prepare('SELECT * FROM admins WHERE login = ? AND password = ?').get(login, password);
|
||||
if (admin) {
|
||||
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: 'Неверный логин или пароль' });
|
||||
@@ -45,7 +56,71 @@ 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';
|
||||
@@ -75,10 +150,10 @@ app.get('/api/bookings/:id', requireAdmin, (req, res) => {
|
||||
res.json(booking);
|
||||
});
|
||||
|
||||
// Обновление заявки (статус, комментарии, перепривязка клиента)
|
||||
// Обновление заявки (статус, комментарий, перепривязка клиента)
|
||||
app.put('/api/bookings/:id', requireAdmin, (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { status, comments, phone } = req.body; // phone для перепривязки карточки
|
||||
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: 'Заявка не найдена' });
|
||||
@@ -125,7 +200,9 @@ app.put('/api/bookings/:id', requireAdmin, (req, res) => {
|
||||
res.json(updatedBooking);
|
||||
});
|
||||
|
||||
// Список карточек клиентов
|
||||
// --- Карточки клиентов ---
|
||||
|
||||
// Список клиентов
|
||||
app.get('/api/clients', requireAdmin, (req, res) => {
|
||||
const { search } = req.query;
|
||||
let query = 'SELECT * FROM users WHERE 1=1';
|
||||
@@ -139,7 +216,7 @@ app.get('/api/clients', requireAdmin, (req, res) => {
|
||||
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: 'Клиент не найден' });
|
||||
@@ -153,13 +230,15 @@ app.get('/api/clients/:id', requireAdmin, (req, res) => {
|
||||
res.json({ client, bookings });
|
||||
});
|
||||
|
||||
// --- Синхронизация ---
|
||||
|
||||
// Ручной запуск синхронизации
|
||||
app.post('/api/admin/sync', requireAdmin, async (req, res) => {
|
||||
await syncBookings();
|
||||
res.json({ success: true, message: 'Синхронизация запущена' });
|
||||
});
|
||||
|
||||
// Планировщик синхронизации каждые 5 минут (опционально)
|
||||
// Планировщик синхронизации каждые 5 минут
|
||||
cron.schedule('*/5 * * * *', () => {
|
||||
console.log('Автосинхронизация...');
|
||||
syncBookings().catch(console.error);
|
||||
|
||||
Reference in New Issue
Block a user