1
This commit is contained in:
24
auth.js
24
auth.js
@@ -1,4 +1,5 @@
|
|||||||
const session = require('express-session');
|
const session = require('express-session');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
const { db } = require('./db');
|
const { db } = require('./db');
|
||||||
|
|
||||||
const sessionMiddleware = session({
|
const sessionMiddleware = session({
|
||||||
@@ -8,23 +9,30 @@ const sessionMiddleware = session({
|
|||||||
cookie: { secure: false, httpOnly: true, maxAge: 24 * 60 * 60 * 1000 }
|
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 login = process.env.ADMIN_LOGIN || 'admin';
|
||||||
const password = process.env.ADMIN_PASSWORD || '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 admin = db.prepare('SELECT id FROM admins WHERE login = ?').get(login);
|
||||||
|
const hashed = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
if (admin) {
|
if (admin) {
|
||||||
// Обновить пароль при каждом старте
|
db.prepare('UPDATE admins SET password = ? WHERE login = ?').run(hashed, login);
|
||||||
db.prepare('UPDATE admins SET password = ? WHERE login = ?').run(password, login);
|
console.log(`Пароль администратора "${login}" обновлён (хэширован)`);
|
||||||
console.log(`Пароль администратора "${login}" обновлён`);
|
|
||||||
} else {
|
} else {
|
||||||
// Создать запись
|
db.prepare('INSERT INTO admins (login, password) VALUES (?, ?)').run(login, hashed);
|
||||||
db.prepare('INSERT INTO admins (login, password) VALUES (?, ?)').run(login, password);
|
console.log(`Администратор "${login}" создан (хэширован)`);
|
||||||
console.log(`Администратор "${login}" создан`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware для проверки прав администратора.
|
||||||
|
*/
|
||||||
function requireAdmin(req, res, next) {
|
function requireAdmin(req, res, next) {
|
||||||
if (req.session && req.session.isAdmin) {
|
if (req.session && req.session.isAdmin) {
|
||||||
return next();
|
return next();
|
||||||
|
|||||||
72
mailer.js
72
mailer.js
@@ -1,20 +1,69 @@
|
|||||||
const nodemailer = require('nodemailer');
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
let transporter = null;
|
||||||
host: process.env.SMTP_HOST,
|
let emailEnabled = true; // по умолчанию включено, при ошибке станет false
|
||||||
port: parseInt(process.env.SMTP_PORT) || 587,
|
let verificationDone = false; // проверка выполнена хотя бы раз
|
||||||
secure: false,
|
|
||||||
auth: {
|
function createTransporter() {
|
||||||
user: process.env.SMTP_USER,
|
if (!process.env.SMTP_HOST || !process.env.SMTP_USER || !process.env.SMTP_PASS) {
|
||||||
pass: 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) {
|
async function sendNotification(emails, subject, text) {
|
||||||
|
if (!emailEnabled) {
|
||||||
|
// Молча пропускаем, т.к. проверка уже показала неработоспособность
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!emails) return;
|
if (!emails) return;
|
||||||
const recipients = emails.split(',').map(e => e.trim()).filter(Boolean);
|
const recipients = emails.split(',').map(e => e.trim()).filter(Boolean);
|
||||||
if (recipients.length === 0) return;
|
if (recipients.length === 0) return;
|
||||||
|
|
||||||
|
// Если транспортер ещё не создан (не было вызова verify), создадим его
|
||||||
|
if (!transporter) {
|
||||||
|
transporter = createTransporter();
|
||||||
|
if (!transporter) {
|
||||||
|
emailEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await transporter.sendMail({
|
await transporter.sendMail({
|
||||||
from: process.env.SMTP_USER,
|
from: process.env.SMTP_USER,
|
||||||
@@ -25,6 +74,11 @@ async function sendNotification(emails, subject, text) {
|
|||||||
console.log(`Уведомление отправлено на ${recipients.join(', ')}`);
|
console.log(`Уведомление отправлено на ${recipients.join(', ')}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка отправки письма:', 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);
|
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": {
|
"dependencies": {
|
||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
@@ -87,6 +88,20 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/better-sqlite3": {
|
||||||
"version": "12.9.0",
|
"version": "12.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
|
"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": "^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": {
|
"node_modules/nodemailer": {
|
||||||
"version": "8.0.7",
|
"version": "8.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.16.0",
|
"axios": "^1.16.0",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
"better-sqlite3": "^12.9.0",
|
"better-sqlite3": "^12.9.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru" data-title="Панель управления" data-description="Администрирование заявок гостиницы">
|
<html lang="ru" data-title="Панель управления" data-description="Администрирование заявок и пользователей гостиницы">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -12,6 +12,40 @@
|
|||||||
<p>Сервис: <span id="serviceName"></span></p>
|
<p>Сервис: <span id="serviceName"></span></p>
|
||||||
<button id="syncBtn">Запустить синхронизацию заявок</button>
|
<button id="syncBtn">Запустить синхронизацию заявок</button>
|
||||||
<p id="syncStatus"></p>
|
<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>
|
</main>
|
||||||
<script src="nav.js"></script>
|
<script src="nav.js"></script>
|
||||||
<script src="seo.js"></script>
|
<script src="seo.js"></script>
|
||||||
@@ -21,6 +55,7 @@
|
|||||||
if (!data.isAdmin) window.location.href = '/login.html';
|
if (!data.isAdmin) window.location.href = '/login.html';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Синхронизация ---
|
||||||
document.getElementById('syncBtn').addEventListener('click', async () => {
|
document.getElementById('syncBtn').addEventListener('click', async () => {
|
||||||
const status = document.getElementById('syncStatus');
|
const status = document.getElementById('syncStatus');
|
||||||
status.textContent = 'Синхронизация...';
|
status.textContent = 'Синхронизация...';
|
||||||
@@ -28,6 +63,106 @@
|
|||||||
if (res.ok) status.textContent = 'Синхронизация завершена';
|
if (res.ok) status.textContent = 'Синхронизация завершена';
|
||||||
else 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -20,4 +20,7 @@ label { font-weight: 500; }
|
|||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
header { padding: 0.5rem 1rem; }
|
header { padding: 0.5rem 1rem; }
|
||||||
nav { flex-direction: column; }
|
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();
|
require('dotenv').config();
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const bcrypt = require('bcrypt');
|
||||||
|
const cron = require('node-cron');
|
||||||
|
|
||||||
const { db, normalizePhone, logAction } = require('./db');
|
const { db, normalizePhone, logAction } = require('./db');
|
||||||
const { sessionMiddleware, ensureAdmin, requireAdmin } = require('./auth');
|
const { sessionMiddleware, ensureAdmin, requireAdmin } = require('./auth');
|
||||||
const { syncBookings } = require('./sync');
|
const { syncBookings } = require('./sync');
|
||||||
const { notifyBookingUpdate } = require('./mailer');
|
const { notifyBookingUpdate, verifyEmailConnection } = require('./mailer');
|
||||||
const cron = require('node-cron');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3000;
|
const PORT = process.env.PORT || 3000;
|
||||||
@@ -17,17 +19,26 @@ app.use(sessionMiddleware);
|
|||||||
// Статика из public
|
// Статика из public
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
// Инициализация администратора
|
// === Инициализация ===
|
||||||
ensureAdmin();
|
// Асинхронное создание/обновление администратора из .env
|
||||||
|
(async () => {
|
||||||
|
await ensureAdmin();
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Проверка работоспособности SMTP при старте (отключит уведомления при ошибке)
|
||||||
|
verifyEmailConnection().catch(err => {
|
||||||
|
console.error('Ошибка при проверке SMTP:', err);
|
||||||
|
});
|
||||||
|
|
||||||
// === API ===
|
// === API ===
|
||||||
|
|
||||||
// Вход администратора
|
// Вход администратора (с хэшированием и сохранением adminId)
|
||||||
app.post('/api/login', (req, res) => {
|
app.post('/api/login', async (req, res) => {
|
||||||
const { login, password } = req.body;
|
const { login, password } = req.body;
|
||||||
const admin = db.prepare('SELECT * FROM admins WHERE login = ? AND password = ?').get(login, password);
|
const admin = db.prepare('SELECT id, login, password FROM admins WHERE login = ?').get(login);
|
||||||
if (admin) {
|
if (admin && await bcrypt.compare(password, admin.password)) {
|
||||||
req.session.isAdmin = true;
|
req.session.isAdmin = true;
|
||||||
|
req.session.adminId = admin.id;
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} else {
|
} else {
|
||||||
res.status(401).json({ error: 'Неверный логин или пароль' });
|
res.status(401).json({ error: 'Неверный логин или пароль' });
|
||||||
@@ -45,7 +56,71 @@ app.get('/api/me', (req, res) => {
|
|||||||
res.json({ isAdmin: !!req.session.isAdmin });
|
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) => {
|
app.get('/api/bookings', requireAdmin, (req, res) => {
|
||||||
const { status, search, client_id } = req.query;
|
const { status, search, client_id } = req.query;
|
||||||
let query = 'SELECT * FROM bookings WHERE 1=1';
|
let query = 'SELECT * FROM bookings WHERE 1=1';
|
||||||
@@ -75,10 +150,10 @@ app.get('/api/bookings/:id', requireAdmin, (req, res) => {
|
|||||||
res.json(booking);
|
res.json(booking);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обновление заявки (статус, комментарии, перепривязка клиента)
|
// Обновление заявки (статус, комментарий, перепривязка клиента)
|
||||||
app.put('/api/bookings/:id', requireAdmin, (req, res) => {
|
app.put('/api/bookings/:id', requireAdmin, (req, res) => {
|
||||||
const { id } = req.params;
|
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);
|
const booking = db.prepare('SELECT * FROM bookings WHERE id = ?').get(id);
|
||||||
if (!booking) return res.status(404).json({ error: 'Заявка не найдена' });
|
if (!booking) return res.status(404).json({ error: 'Заявка не найдена' });
|
||||||
@@ -125,7 +200,9 @@ app.put('/api/bookings/:id', requireAdmin, (req, res) => {
|
|||||||
res.json(updatedBooking);
|
res.json(updatedBooking);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Список карточек клиентов
|
// --- Карточки клиентов ---
|
||||||
|
|
||||||
|
// Список клиентов
|
||||||
app.get('/api/clients', requireAdmin, (req, res) => {
|
app.get('/api/clients', requireAdmin, (req, res) => {
|
||||||
const { search } = req.query;
|
const { search } = req.query;
|
||||||
let query = 'SELECT * FROM users WHERE 1=1';
|
let query = 'SELECT * FROM users WHERE 1=1';
|
||||||
@@ -139,7 +216,7 @@ app.get('/api/clients', requireAdmin, (req, res) => {
|
|||||||
res.json(clients);
|
res.json(clients);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Профиль клиента с его заявками (сортировка: сначала по дате убыванию, потом по статусу в алфавитном порядке)
|
// Профиль клиента с его заявками
|
||||||
app.get('/api/clients/:id', requireAdmin, (req, res) => {
|
app.get('/api/clients/:id', requireAdmin, (req, res) => {
|
||||||
const client = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
const client = db.prepare('SELECT * FROM users WHERE id = ?').get(req.params.id);
|
||||||
if (!client) return res.status(404).json({ error: 'Клиент не найден' });
|
if (!client) return res.status(404).json({ error: 'Клиент не найден' });
|
||||||
@@ -153,13 +230,15 @@ app.get('/api/clients/:id', requireAdmin, (req, res) => {
|
|||||||
res.json({ client, bookings });
|
res.json({ client, bookings });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// --- Синхронизация ---
|
||||||
|
|
||||||
// Ручной запуск синхронизации
|
// Ручной запуск синхронизации
|
||||||
app.post('/api/admin/sync', requireAdmin, async (req, res) => {
|
app.post('/api/admin/sync', requireAdmin, async (req, res) => {
|
||||||
await syncBookings();
|
await syncBookings();
|
||||||
res.json({ success: true, message: 'Синхронизация запущена' });
|
res.json({ success: true, message: 'Синхронизация запущена' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Планировщик синхронизации каждые 5 минут (опционально)
|
// Планировщик синхронизации каждые 5 минут
|
||||||
cron.schedule('*/5 * * * *', () => {
|
cron.schedule('*/5 * * * *', () => {
|
||||||
console.log('Автосинхронизация...');
|
console.log('Автосинхронизация...');
|
||||||
syncBookings().catch(console.error);
|
syncBookings().catch(console.error);
|
||||||
|
|||||||
Reference in New Issue
Block a user