This commit is contained in:
2026-05-08 23:49:27 +05:00
parent 8f52c3d4c1
commit 0a9a8387e4
4 changed files with 69 additions and 25 deletions

View File

@@ -8,25 +8,26 @@ Hotel 777 — это веб-приложение для бронирования
- **База данных**: SQLite3 - **База данных**: SQLite3
- **Аутентификация**: JWT (jsonwebtoken) + bcryptjs - **Аутентификация**: JWT (jsonwebtoken) + bcryptjs
- **Обработка изображений**: Sharp (конвертация в WebP) - **Обработка изображений**: Sharp (конвертация в WebP)
- **Загрузка файлов**: Multer
- **Frontend**: Bootstrap 5, Font Awesome, кастомные шрифты (Inter, Playfair Display) - **Frontend**: Bootstrap 5, Font Awesome, кастомные шрифты (Inter, Playfair Display)
- **Контейнеризация**: Docker + Docker Compose - **Контейнеризация**: Docker + Docker Compose
## Структура проекта ## Структура проекта
``` ```
hotell777_260507/ hotell777_260507/
├── server.js # Основной файл сервера (784 строки) ├── server.js # Основной файл сервера (633 строки)
├── package.json # Зависимости проекта ├── package.json # Зависимости проекта
├── Dockerfile # Сборка Docker образа ├── Dockerfile # Сборка Docker образа
├── docker-compose.yml # Оркестрация контейнера ├── docker-compose.yml # Оркестрация контейнера
├── .env # Переменные окружения ├── .env # Переменные окружения (только API_KEY)
├── .gitignore ├── .gitignore
├── PROJECT_INFO.md # Описание проекта
├── data/ ├── data/
│ └── bookings.db # База данных SQLite │ └── bookings.db # База данных SQLite
├── public/ ├── public/
│ ├── index.html # Публичная страница (613 строк) │ ├── index.html # Публичная страница
│ ├── admin.html # Админ-панель (949 строк) │ ├── admin.html # Админ-панель
│ ├── css/ # Стили (Bootstrap, шрифты, кастомные стили) │ ├── css/ # Стили (Bootstrap, шрифты, кастомные стили)
│ │ └── files/ # Шрифты Inter и Playfair Display
│ ├── js/ │ ├── js/
│ │ ├── main.js # Логика публичной части │ │ ├── main.js # Логика публичной части
│ │ └── bootstrap.bundle.min.js │ │ └── bootstrap.bundle.min.js
@@ -43,8 +44,9 @@ hotell777_260507/
- Статусы: 'новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена' - Статусы: 'новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'
2. **rooms** — номера отеля 2. **rooms** — номера отеля
- Типы: 'Эконом' (2500₽/чел), 'Стандарт' (4000₽/чел), 'VIP Люкс' (8000₽/чел) - Типы: 'Эконом' (2500₽/ночь), 'Стандарт' (4000₽/ночь), 'VIP Люкс' (8000₽/ночь)
- Поля: type, name, description, rooms_count, single_beds, double_beds, has_sofa, has_ac, has_wifi, has_shower, max_guests, price_per_guest, image_path, is_active - Поля: type, name, description, rooms_count, single_beds, double_beds, has_sofa, has_ac, has_wifi, has_shower, max_guests, price_per_guest, image_path, is_active
- Номера по умолчанию: 'Эконом 1' (3 шт, 2 односпальные кровати), 'Стандарт 1' (2 шт, 1 двуспальная + диван), 'VIP Люкс 1' (1 шт, 1 двуспальная + диван, до 4 гостей)
3. **promocodes** — промокоды 3. **promocodes** — промокоды
- Поля: code, discount_percent (1-99%), valid_from, valid_to, valid_days, is_active - Поля: code, discount_percent (1-99%), valid_from, valid_to, valid_days, is_active
@@ -54,18 +56,24 @@ hotell777_260507/
5. **users** — пользователи системы 5. **users** — пользователи системы
- Роли: 'admin', 'user' - Роли: 'admin', 'user'
- Автоматическая синхронизация суперадмина из .env - Суперадмин создается при первом запуске (пароль задается через .env)
## API Endpoints ## API Endpoints
### Публичные: ### Публичные:
- `GET /` — главная страница (index.html)
- `GET /admin` — админ-панель (admin.html)
- `GET /api/rooms` — список доступных номеров - `GET /api/rooms` — список доступных номеров
- `POST /api/bookings` — создание бронирования - `POST /api/bookings` — создание бронирования
- `POST /api/promocodes/validate` — проверка промокода и расчет скидки - `POST /api/promocodes/validate` — проверка промокода и расчет скидки
- `GET /api/bookings` — получение всех бронирований (требуется API_KEY в заголовке X-API-Key) - `GET /api/bookings` — получение всех бронирований (требуется API_KEY в заголовке X-API-Key)
### Административные (требуется JWT): ### Аутентификация:
- `POST /api/admin/login` — вход в систему - `POST /api/auth/login` — вход в систему (login, password)
- `PUT /api/auth/me` — обновление профиля (full_name, email)
- `POST /api/auth/change-password` — смена пароля (current_password, new_password)
### Административные (требуется JWT + роль admin):
- `GET /api/admin/bookings` — список всех бронирований - `GET /api/admin/bookings` — список всех бронирований
- `GET /api/admin/bookings/:id/history` — история изменений бронирования - `GET /api/admin/bookings/:id/history` — история изменений бронирования
- `PATCH /api/admin/bookings/:id` — изменение статуса бронирования - `PATCH /api/admin/bookings/:id` — изменение статуса бронирования
@@ -80,10 +88,14 @@ hotell777_260507/
- `POST /api/admin/promocodes` — создание промокода - `POST /api/admin/promocodes` — создание промокода
- `PUT /api/admin/promocodes/:id` — обновление промокода - `PUT /api/admin/promocodes/:id` — обновление промокода
- `DELETE /api/admin/promocodes/:id` — удаление промокода - `DELETE /api/admin/promocodes/:id` — удаление промокода
- `GET /api/admin/users` — список пользователей
- `POST /api/admin/users` — создание пользователя
- `PUT /api/admin/users/:id` — обновление пользователя
- `DELETE /api/admin/users/:id` — удаление пользователя
## Основные функции ## Основные функции
1. **Публичная часть**: 1. **Публичная часть**:
- Просмотр номеров и цен - Просмотр номеров и цен (цена за ночь)
- Бронирование номера с указанием дат, количества гостей - Бронирование номера с указанием дат, количества гостей
- Применение промокодов - Применение промокодов
- Галерея изображений - Галерея изображений
@@ -94,26 +106,28 @@ hotell777_260507/
- Изменение статусов бронирований - Изменение статусов бронирований
- Управление номерами (создание, редактирование, удаление) - Управление номерами (создание, редактирование, удаление)
- Управление промокодами - Управление промокодами
- Управление пользователями (создание, редактирование, удаление)
- История изменений по каждому бронированию - История изменений по каждому бронированию
- Статистика и фильтрация
3. **Дополнительно**: 3. **Аутентификация и пользователи**:
- Автоматическая конвертация изображений в WebP - Вход по логину и паролю (JWT токен на 24 часа)
- Смена пароля пользователем
- Обновление профиля (имя, email)
- Разграничение прав: admin и user
4. **Дополнительно**:
- Автоматическая конвертация изображений в WebP (при наличии в папке img)
- Автоматическое создание номеров по умолчанию - Автоматическое создание номеров по умолчанию
- Проверка доступности промокодов по датам - Проверка доступности промокодов по датам
- Расчет стоимости с учетом скидок - Расчет стоимости с учетом скидок (calculateBasePrice)
## Переменные окружения (.env) ## Переменные окружения (.env)
``` ```
PORT=3000
HOTEL777KEY=<секретный_ключ_API> HOTEL777KEY=<секретный_ключ_API>
ADMIN_LOGIN=<логин_администратора>
ADMIN_PASSWORD=<пароль_администратора>
JWT_SECRET=<секрет_JWT>
hotelName="Hotel 777"
hotelAddress="Абхазия, золотой берег"
hotelPhone="+79400000000"
``` ```
- PORT=3000 (по умолчанию)
- JWT_SECRET (опционально, по умолчанию 'fallback-secret-change-in-production')
- Пароль администратора задается при первом запуске через регистрацию
## Docker развертывание ## Docker развертывание
- Образ: `kalugin66/hotell777_260507` - Образ: `kalugin66/hotell777_260507`
@@ -154,8 +168,10 @@ docker network create applications
- GitHub: kalugin66 - GitHub: kalugin66
## Особенности реализации ## Особенности реализации
- Все цены указаны в рублях за человека за ночь - Все цены указаны в рублях за номер за ночь
- Количество ночей рассчитывается как разница между датами заезда и выезда - Количество ночей рассчитывается как разница между датами заезда и выезда
- История изменений сохраняется автоматически при любом изменении бронирования - История изменений сохраняется автоматически при любом изменении бронирования
- Суперадмин автоматически создается/обновляется из переменных окружения при запуске - Администраторы создаются через API (`POST /api/admin/users`)
- Изображения номеров загружаются через форму админ-панели и конвертируются в WebP - Автоматическая конвертация изображений в WebP (функция convertImages)
- CORS настроен через заголовки (не требуется отдельный middleware)
- Multer удален (загрузка изображений через админ-панель отключена)

View File

@@ -70,6 +70,13 @@ if (!API_KEY) {
} }
app.use(express.json()); app.use(express.json());
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
app.use(express.static(path.join(__dirname, 'public'))); app.use(express.static(path.join(__dirname, 'public')));
const dataDir = path.join(__dirname, 'data'); const dataDir = path.join(__dirname, 'data');
@@ -306,6 +313,7 @@ app.get('/api/admin/bookings', authenticateToken, (req, res) => {
}); });
app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res) => { app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res) => {
console.log('PATCH /api/admin/bookings/:id hit', req.params.id, req.body);
const bookingId = parseInt(req.params.id); const bookingId = parseInt(req.params.id);
const { status } = req.body; const { status } = req.body;
const validStatuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена']; const validStatuses = ['новая', 'оплачена', 'зарезервирована', 'заселена', 'выехала', 'отменена'];
@@ -319,7 +327,7 @@ app.patch('/api/admin/bookings/:id', authenticateToken, requireAdmin, (req, res)
db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => { db.run(`UPDATE bookings SET status = ? WHERE id = ?`, [status, bookingId], (err) => {
if (err) return res.status(500).json({ error: 'Database error' }); if (err) return res.status(500).json({ error: 'Database error' });
logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status); logHistory(bookingId, req.user.id, req.user.login, 'status', oldValue, status);
db.get(`SELECT id, name, phone, adults, children, checkin_date, checkout_date, wishes, status, room_type, created_at FROM bookings WHERE id = ?`, [bookingId], (err, row) => { db.get(`SELECT b.*, p.code as promocode_code FROM bookings b LEFT JOIN promocodes p ON b.promocode_id = p.id WHERE b.id = ?`, [bookingId], (err, row) => {
if (err) return res.status(500).json({ error: 'Database error' }); if (err) return res.status(500).json({ error: 'Database error' });
res.json({ message: 'Status updated', booking: row }); res.json({ message: 'Status updated', booking: row });
}); });
@@ -441,6 +449,14 @@ app.post('/api/promocodes/validate', (req, res) => {
}); });
}); });
// Public rooms endpoint
app.get('/api/rooms', (req, res) => {
db.all(`SELECT * FROM rooms WHERE is_active = 1 ORDER BY price_per_guest ASC`, [], (err, rows) => {
if (err) { console.error('Rooms API error:', err); return res.status(500).json({ error: 'Database error' }); }
res.json(rows);
});
});
app.get('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => { app.get('/api/admin/promocodes', authenticateToken, requireAdmin, (req, res) => {
db.all(`SELECT * FROM promocodes ORDER BY created_at DESC`, [], (err, rows) => { db.all(`SELECT * FROM promocodes ORDER BY created_at DESC`, [], (err, rows) => {
if (err) return res.status(500).json({ error: 'Database error' }); if (err) return res.status(500).json({ error: 'Database error' });

12
server.log Normal file
View File

@@ -0,0 +1,12 @@
◇ injected env (3) from .env // tip: ⌘ multiple files { path: ['.env.local', '.env'] }
✅ HOTEL777KEY is HFwy+tfAljHEq8R21BCRt+Ps4SN65bu8zFagA68N24s
✅ Hotel 777 server running on http://localhost:3000
✅ Superadmin "admin" updated from .env
PATCH /api/admin/bookings/:id hit 2 { status: 'оплачена' }
PATCH /api/admin/bookings/:id hit 2 { status: 'заселена' }
PATCH /api/admin/bookings/:id hit 2 { status: 'оплачена' }
PATCH /api/admin/bookings/:id hit 2 { status: 'новая' }
PATCH /api/admin/bookings/:id hit 2 { status: 'оплачена' }
PATCH /api/admin/bookings/:id hit 2 { status: 'выехала' }
PATCH /api/admin/bookings/:id hit 2 { status: 'оплачена' }
PATCH /api/admin/bookings/:id hit 2 { status: 'новая' }

0
server_error.log Normal file
View File