Files
hotell777_260507/server.js
2026-05-07 21:10:34 +05:00

286 lines
13 KiB
JavaScript

const express = require('express');
const path = require('path');
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const sharp = require('sharp');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
const API_KEY = process.env.HOTEL777KEY;
const ADMIN_LOGIN = process.env.ADMIN_LOGIN;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET || 'fallback-secret-change-in-production';
if (!API_KEY) {
console.error('FATAL: HOTEL777KEY environment variable not set');
process.exit(1);
}
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir);
const dbPath = path.join(dataDir, 'bookings.db');
const db = new sqlite3.Database(dbPath);
db.run(`CREATE TABLE IF NOT EXISTS bookings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
phone TEXT NOT NULL,
adults INTEGER NOT NULL,
children INTEGER NOT NULL,
checkin_date TEXT NOT NULL,
checkout_date TEXT NOT NULL,
wishes TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) console.error('Table creation error:', err);
});
db.run(`ALTER TABLE bookings ADD COLUMN wishes TEXT`, (err) => {
if (err && !err.message.includes('duplicate column name')) {
console.error('Migration error:', err);
}
});
db.run(`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
login TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
full_name TEXT,
email TEXT,
role TEXT NOT NULL DEFAULT 'user',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`, (err) => {
if (err) console.error('Users table creation error:', err);
else syncAdmin();
});
function syncAdmin() {
if (!ADMIN_LOGIN || !ADMIN_PASSWORD) {
console.warn('WARNING: ADMIN_LOGIN or ADMIN_PASSWORD not set, skipping admin sync');
return;
}
const hash = bcrypt.hashSync(ADMIN_PASSWORD, 10);
db.get(`SELECT id, role FROM users WHERE login = ?`, [ADMIN_LOGIN], (err, row) => {
if (err) { console.error('Admin sync error:', err); return; }
if (row) {
db.run(`UPDATE users SET password_hash = ?, role = 'admin' WHERE login = ?`, [hash, ADMIN_LOGIN], (err) => {
if (err) console.error('Admin update error:', err);
else console.log(`✅ Superadmin "${ADMIN_LOGIN}" updated from .env`);
});
} else {
db.run(`INSERT INTO users (login, password_hash, full_name, email, role) VALUES (?, ?, 'Администратор', NULL, 'admin')`,
[ADMIN_LOGIN, hash], (err) => {
if (err) console.error('Admin creation error:', err);
else console.log(`✅ Superadmin "${ADMIN_LOGIN}" created from .env`);
});
}
});
}
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid or expired token' });
req.user = user;
next();
});
}
function requireAdmin(req, res, next) {
if (req.user.role !== 'admin') return res.status(403).json({ error: 'Admin access required' });
next();
}
async function convertImages() {
const imgDir = path.join(__dirname, 'public', 'img');
if (!fs.existsSync(imgDir)) {
console.log('Папка img не найдена, пропускаем конвертацию.');
return;
}
const files = fs.readdirSync(imgDir);
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (ext === '.jpg' || ext === '.jpeg' || ext === '.png') {
const name = path.parse(file).name;
const webpPath = path.join(imgDir, `${name}.webp`);
if (!fs.existsSync(webpPath)) {
try {
await sharp(path.join(imgDir, file))
.webp({ quality: 85 })
.toFile(webpPath);
console.log(`✅ Сконвертировано: ${file} -> ${name}.webp`);
} catch (err) {
console.error(`❌ Ошибка при конвертации ${file}:`, err);
}
}
}
}
}
app.post('/api/bookings', (req, res) => {
const { name, phone, adults, children, checkin, checkout, wishes } = req.body;
if (!name || !phone || !adults || !checkin || !checkout) {
return res.status(400).json({ error: 'Missing required fields' });
}
const stmt = db.prepare(`INSERT INTO bookings (name, phone, adults, children, checkin_date, checkout_date, wishes)
VALUES (?, ?, ?, ?, ?, ?, ?)`);
stmt.run(name, phone, parseInt(adults), parseInt(children || 0), checkin, checkout, wishes || null, function(err) {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.status(201).json({ id: this.lastID, message: 'Booking saved' });
});
stmt.finalize();
});
app.get('/api/bookings', (req, res) => {
const providedKey = req.headers['x-api-key'];
if (!providedKey || providedKey !== API_KEY) {
return res.status(401).json({ error: 'Invalid or missing API key' });
}
db.all(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, created_at
FROM bookings ORDER BY created_at DESC`, (err, rows) => {
if (err) {
console.error(err);
return res.status(500).json({ error: 'Database error' });
}
res.json(rows);
});
});
app.post('/api/auth/login', (req, res) => {
const { login, password } = req.body;
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
db.get(`SELECT id, login, password_hash, full_name, email, role FROM users WHERE login = ?`, [login], (err, user) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const match = bcrypt.compareSync(password, user.password_hash);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
const token = jwt.sign({ id: user.id, login: user.login, role: user.role }, JWT_SECRET, { expiresIn: '24h' });
res.json({
token,
user: { id: user.id, login: user.login, full_name: user.full_name, email: user.email, role: user.role }
});
});
});
app.put('/api/auth/me', authenticateToken, (req, res) => {
const { full_name, email } = req.body;
db.run(`UPDATE users SET full_name = COALESCE(?, full_name), email = COALESCE(?, email) WHERE id = ?`,
[full_name || null, email || null, req.user.id], function(err) {
if (err) return res.status(500).json({ error: 'Database error' });
db.get(`SELECT id, login, full_name, email, role FROM users WHERE id = ?`, [req.user.id], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Profile updated', user: row });
});
});
});
app.post('/api/auth/change-password', authenticateToken, (req, res) => {
const { current_password, new_password } = req.body;
if (!current_password || !new_password) return res.status(400).json({ error: 'Current and new password required' });
if (new_password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
db.get(`SELECT password_hash FROM users WHERE id = ?`, [req.user.id], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
const match = bcrypt.compareSync(current_password, row.password_hash);
if (!match) return res.status(401).json({ error: 'Current password is incorrect' });
const hash = bcrypt.hashSync(new_password, 10);
db.run(`UPDATE users SET password_hash = ? WHERE id = ?`, [hash, req.user.id], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Password changed successfully' });
});
});
});
app.get('/api/admin/users', authenticateToken, requireAdmin, (req, res) => {
db.all(`SELECT id, login, full_name, email, role, created_at FROM users ORDER BY created_at DESC`, [], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json(rows);
});
});
app.post('/api/admin/users', authenticateToken, requireAdmin, (req, res) => {
const { login, password, full_name, email, role } = req.body;
if (!login || !password) return res.status(400).json({ error: 'Login and password required' });
if (!['admin', 'user'].includes(role)) return res.status(400).json({ error: 'Role must be "admin" or "user"' });
if (password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
const hash = bcrypt.hashSync(password, 10);
db.run(`INSERT INTO users (login, password_hash, full_name, email, role) VALUES (?, ?, ?, ?, ?)`,
[login, hash, full_name || null, email || null, role], function(err) {
if (err) {
if (err.message.includes('UNIQUE constraint')) return res.status(409).json({ error: 'Login already exists' });
return res.status(500).json({ error: 'Database error' });
}
db.get(`SELECT id, login, full_name, email, role, created_at FROM users WHERE id = ?`, [this.lastID], (err, row) => {
res.status(201).json({ message: 'User created', user: row });
});
});
});
app.put('/api/admin/users/:id', authenticateToken, requireAdmin, (req, res) => {
const userId = parseInt(req.params.id);
const { full_name, email, role, password } = req.body;
if (role && !['admin', 'user'].includes(role)) return res.status(400).json({ error: 'Role must be "admin" or "user"' });
db.get(`SELECT id FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
let fields = [];
let values = [];
if (full_name !== undefined) { fields.push('full_name = ?'); values.push(full_name || null); }
if (email !== undefined) { fields.push('email = ?'); values.push(email || null); }
if (role !== undefined) { fields.push('role = ?'); values.push(role); }
if (password) {
if (password.length < 4) return res.status(400).json({ error: 'Password must be at least 4 characters' });
fields.push('password_hash = ?');
values.push(bcrypt.hashSync(password, 10));
}
if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' });
values.push(userId);
db.run(`UPDATE users SET ${fields.join(', ')} WHERE id = ?`, values, (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
db.get(`SELECT id, login, full_name, email, role, created_at FROM users WHERE id = ?`, [userId], (err, row) => {
res.json({ message: 'User updated', user: row });
});
});
});
});
app.delete('/api/admin/users/:id', authenticateToken, requireAdmin, (req, res) => {
const userId = parseInt(req.params.id);
if (userId === req.user.id) return res.status(400).json({ error: 'Cannot delete yourself' });
db.get(`SELECT login FROM users WHERE id = ?`, [userId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' });
if (!row) return res.status(404).json({ error: 'User not found' });
if (row.login === ADMIN_LOGIN) return res.status(403).json({ error: 'Cannot delete superadmin defined in .env' });
db.run(`DELETE FROM users WHERE id = ?`, [userId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'User deleted' });
});
});
});
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
app.get('/admin', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'admin.html'));
});
convertImages().then(() => {
app.listen(PORT, () => {
console.log('✅ HOTEL777KEY is', API_KEY);
console.log(`✅ Hotel 777 server running on http://localhost:${PORT}`);
});
});