🎨 UI улучшения: - Уменьшена высота синих панелей с 100vh до 70vh на главной - Добавлен класс .compact (25vh) для всех остальных страниц - Улучшена адаптивность для мобильных устройств - Обновлены все шаблоны с hero секциями 🚀 Инфраструктура: - Автоматическая инициализация базы данных при деплое - Улучшены мокапные данные (больше отзывов, бронирований, сообщений) - Добавлены настройки сайта в базу данных - Создан скрипт автоматического деплоя deploy.sh 📦 Система сборки: - Обновлен .gitignore с полным покрытием файлов - Добавлена папка для загрузок с .gitkeep - Улучшен README с инструкциями по запуску - ES модули для инициализации базы данных 🐛 Исправления: - Совместимость с ES модулями в Node.js - Правильная обработка ошибок инициализации БД - Корректные SQL запросы для PostgreSQL
718 lines
26 KiB
JavaScript
718 lines
26 KiB
JavaScript
import AdminJS from 'adminjs';
|
||
import AdminJSExpress from '@adminjs/express';
|
||
import AdminJSSequelize from '@adminjs/sequelize';
|
||
import bcrypt from 'bcryptjs';
|
||
import pkg from 'pg';
|
||
import { Sequelize, DataTypes } from 'sequelize';
|
||
const { Pool } = pkg;
|
||
|
||
// Регистрируем адаптер Sequelize
|
||
AdminJS.registerAdapter(AdminJSSequelize);
|
||
|
||
// Создаем подключение Sequelize
|
||
const sequelize = new Sequelize(
|
||
process.env.DB_NAME || 'korea_tourism',
|
||
process.env.DB_USER || 'tourism_user',
|
||
process.env.DB_PASSWORD || 'tourism_password',
|
||
{
|
||
host: process.env.DB_HOST || 'postgres',
|
||
port: process.env.DB_PORT || 5432,
|
||
dialect: 'postgres',
|
||
logging: false,
|
||
}
|
||
);
|
||
|
||
// Создаем пул подключений для аутентификации (отдельно от Sequelize)
|
||
const authPool = new Pool({
|
||
host: process.env.DB_HOST || 'postgres',
|
||
port: process.env.DB_PORT || 5432,
|
||
database: process.env.DB_NAME || 'korea_tourism',
|
||
user: process.env.DB_USER || 'tourism_user',
|
||
password: process.env.DB_PASSWORD || 'tourism_password',
|
||
});
|
||
|
||
// Определяем модели Sequelize
|
||
const Routes = sequelize.define('routes', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
title: { type: DataTypes.STRING, allowNull: false },
|
||
description: { type: DataTypes.TEXT },
|
||
content: { type: DataTypes.TEXT },
|
||
type: { type: DataTypes.ENUM('city', 'mountain', 'fishing') },
|
||
difficulty_level: { type: DataTypes.ENUM('easy', 'moderate', 'hard') },
|
||
price: { type: DataTypes.DECIMAL(10, 2) },
|
||
duration: { type: DataTypes.INTEGER },
|
||
max_group_size: { type: DataTypes.INTEGER },
|
||
is_featured: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
|
||
updated_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'routes'
|
||
});
|
||
|
||
const Guides = sequelize.define('guides', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
name: { type: DataTypes.STRING, allowNull: false },
|
||
email: { type: DataTypes.STRING },
|
||
phone: { type: DataTypes.STRING },
|
||
languages: { type: DataTypes.TEXT },
|
||
specialization: { type: DataTypes.ENUM('city', 'mountain', 'fishing', 'general') },
|
||
bio: { type: DataTypes.TEXT },
|
||
experience: { type: DataTypes.INTEGER },
|
||
hourly_rate: { type: DataTypes.DECIMAL(10, 2) },
|
||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'guides'
|
||
});
|
||
|
||
const Articles = sequelize.define('articles', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
title: { type: DataTypes.STRING, allowNull: false },
|
||
excerpt: { type: DataTypes.TEXT },
|
||
content: { type: DataTypes.TEXT, allowNull: false },
|
||
category: { type: DataTypes.ENUM('travel-tips', 'culture', 'food', 'nature', 'history') },
|
||
is_published: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||
views: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
|
||
updated_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'articles'
|
||
});
|
||
|
||
const Bookings = sequelize.define('bookings', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
customer_name: { type: DataTypes.STRING, allowNull: false },
|
||
customer_email: { type: DataTypes.STRING, allowNull: false },
|
||
customer_phone: { type: DataTypes.STRING },
|
||
preferred_date: { type: DataTypes.DATE, allowNull: false },
|
||
group_size: { type: DataTypes.INTEGER, allowNull: false },
|
||
status: { type: DataTypes.ENUM('pending', 'confirmed', 'cancelled', 'completed'), defaultValue: 'pending' },
|
||
total_price: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
|
||
notes: { type: DataTypes.TEXT },
|
||
guide_id: { type: DataTypes.INTEGER },
|
||
route_id: { type: DataTypes.INTEGER },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'bookings'
|
||
});
|
||
|
||
const Reviews = sequelize.define('reviews', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
customer_name: { type: DataTypes.STRING, allowNull: false },
|
||
customer_email: { type: DataTypes.STRING },
|
||
rating: { type: DataTypes.INTEGER, validate: { min: 1, max: 5 } },
|
||
comment: { type: DataTypes.TEXT },
|
||
is_approved: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'reviews'
|
||
});
|
||
|
||
const ContactMessages = sequelize.define('contact_messages', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
name: { type: DataTypes.STRING, allowNull: false },
|
||
email: { type: DataTypes.STRING, allowNull: false },
|
||
phone: { type: DataTypes.STRING },
|
||
subject: { type: DataTypes.STRING, allowNull: false },
|
||
message: { type: DataTypes.TEXT, allowNull: false },
|
||
status: { type: DataTypes.ENUM('unread', 'read', 'replied'), defaultValue: 'unread' },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'contact_messages'
|
||
});
|
||
|
||
const Admins = sequelize.define('admins', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
username: { type: DataTypes.STRING, allowNull: false, unique: true },
|
||
name: { type: DataTypes.STRING, allowNull: false },
|
||
email: { type: DataTypes.STRING, allowNull: false },
|
||
password: { type: DataTypes.STRING, allowNull: false },
|
||
role: { type: DataTypes.ENUM('admin', 'manager', 'editor'), defaultValue: 'admin' },
|
||
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
last_login: { type: DataTypes.DATE },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'admins'
|
||
});
|
||
|
||
// Новые модели для системы рейтинга и расписания
|
||
const Ratings = sequelize.define('ratings', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
user_ip: { type: DataTypes.STRING(45), allowNull: false },
|
||
target_id: { type: DataTypes.INTEGER, allowNull: false },
|
||
target_type: { type: DataTypes.ENUM('route', 'guide', 'article'), allowNull: false },
|
||
rating: { type: DataTypes.INTEGER, allowNull: false, validate: { isIn: [[1, -1]] } },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'ratings'
|
||
});
|
||
|
||
const GuideSchedules = sequelize.define('guide_schedules', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
guide_id: { type: DataTypes.INTEGER, allowNull: false },
|
||
monday: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
tuesday: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
wednesday: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
thursday: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
friday: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||
saturday: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||
sunday: { type: DataTypes.BOOLEAN, defaultValue: false },
|
||
start_time: { type: DataTypes.TIME, defaultValue: '09:00' },
|
||
end_time: { type: DataTypes.TIME, defaultValue: '18:00' },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
|
||
updated_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'guide_schedules'
|
||
});
|
||
|
||
const Holidays = sequelize.define('holidays', {
|
||
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
|
||
date: { type: DataTypes.DATEONLY, allowNull: false },
|
||
title: { type: DataTypes.STRING, allowNull: false },
|
||
type: { type: DataTypes.ENUM('public', 'guide_personal'), allowNull: false },
|
||
guide_id: { type: DataTypes.INTEGER },
|
||
created_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
|
||
}, {
|
||
timestamps: false,
|
||
tableName: 'holidays'
|
||
});
|
||
|
||
// Определение связей между моделями
|
||
Guides.hasOne(GuideSchedules, { foreignKey: 'guide_id' });
|
||
GuideSchedules.belongsTo(Guides, { foreignKey: 'guide_id' });
|
||
|
||
Guides.hasMany(Holidays, { foreignKey: 'guide_id' });
|
||
Holidays.belongsTo(Guides, { foreignKey: 'guide_id' });
|
||
|
||
Guides.hasMany(Bookings, { foreignKey: 'guide_id' });
|
||
Bookings.belongsTo(Guides, { foreignKey: 'guide_id' });
|
||
|
||
Routes.hasMany(Bookings, { foreignKey: 'route_id' });
|
||
Bookings.belongsTo(Routes, { foreignKey: 'route_id' });
|
||
|
||
// Методы для получения рейтингов
|
||
const getRatingStats = async (targetType, targetId) => {
|
||
const result = await sequelize.query(
|
||
'SELECT * FROM calculate_rating(:targetType, :targetId)',
|
||
{
|
||
replacements: { targetType, targetId },
|
||
type: sequelize.QueryTypes.SELECT
|
||
}
|
||
);
|
||
return result[0] || { likes_count: 0, dislikes_count: 0, total_votes: 0, rating_percentage: 0 };
|
||
};
|
||
|
||
|
||
// Конфигурация AdminJS с ресурсами базы данных
|
||
// Конфигурация AdminJS с ресурсами Sequelize
|
||
const adminJsOptions = {
|
||
resources: [
|
||
{
|
||
resource: Routes,
|
||
options: {
|
||
parent: { name: 'Контент', icon: 'DocumentText' },
|
||
listProperties: ['id', 'title', 'type', 'price', 'duration', 'is_active', 'created_at'],
|
||
editProperties: ['title', 'description', 'content', 'type', 'difficulty_level', 'price', 'duration', 'max_group_size', 'is_featured', 'is_active'],
|
||
showProperties: ['id', 'title', 'description', 'content', 'type', 'difficulty_level', 'price', 'duration', 'max_group_size', 'is_featured', 'is_active', 'created_at', 'updated_at'],
|
||
filterProperties: ['title', 'type', 'is_active'],
|
||
properties: {
|
||
title: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
description: {
|
||
type: 'textarea',
|
||
isRequired: true,
|
||
},
|
||
content: {
|
||
type: 'textarea',
|
||
},
|
||
type: {
|
||
availableValues: [
|
||
{ value: 'city', label: 'Городской тур' },
|
||
{ value: 'mountain', label: 'Горный поход' },
|
||
{ value: 'fishing', label: 'Рыбалка' }
|
||
],
|
||
},
|
||
difficulty_level: {
|
||
availableValues: [
|
||
{ value: 'easy', label: 'Легкий' },
|
||
{ value: 'moderate', label: 'Средний' },
|
||
{ value: 'hard', label: 'Сложный' }
|
||
],
|
||
},
|
||
price: {
|
||
type: 'number',
|
||
isRequired: true,
|
||
},
|
||
duration: {
|
||
type: 'number',
|
||
isRequired: true,
|
||
},
|
||
max_group_size: {
|
||
type: 'number',
|
||
isRequired: true,
|
||
},
|
||
is_featured: { type: 'boolean' },
|
||
is_active: { type: 'boolean' },
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
},
|
||
updated_at: {
|
||
isVisible: { list: false, filter: false, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Guides,
|
||
options: {
|
||
parent: { name: 'Персонал', icon: 'Users' },
|
||
listProperties: ['id', 'name', 'email', 'specialization', 'experience', 'hourly_rate', 'is_active'],
|
||
editProperties: ['name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active'],
|
||
showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'hourly_rate', 'is_active', 'created_at'],
|
||
filterProperties: ['name', 'specialization', 'is_active'],
|
||
properties: {
|
||
name: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
email: {
|
||
type: 'email',
|
||
isRequired: true,
|
||
},
|
||
phone: { type: 'string' },
|
||
languages: {
|
||
type: 'textarea',
|
||
description: 'Языки через запятую',
|
||
},
|
||
specialization: {
|
||
availableValues: [
|
||
{ value: 'city', label: 'Городские туры' },
|
||
{ value: 'mountain', label: 'Горные походы' },
|
||
{ value: 'fishing', label: 'Рыбалка' },
|
||
{ value: 'general', label: 'Универсальный' }
|
||
],
|
||
},
|
||
bio: { type: 'textarea' },
|
||
experience: {
|
||
type: 'number',
|
||
description: 'Опыт работы в годах',
|
||
},
|
||
hourly_rate: {
|
||
type: 'number',
|
||
description: 'Ставка за час в вонах',
|
||
},
|
||
is_active: { type: 'boolean' },
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Articles,
|
||
options: {
|
||
parent: { name: 'Контент', icon: 'DocumentText' },
|
||
listProperties: ['id', 'title', 'category', 'is_published', 'views', 'created_at'],
|
||
editProperties: ['title', 'excerpt', 'content', 'category', 'is_published'],
|
||
showProperties: ['id', 'title', 'excerpt', 'content', 'category', 'is_published', 'views', 'created_at', 'updated_at'],
|
||
filterProperties: ['title', 'category', 'is_published'],
|
||
properties: {
|
||
title: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
excerpt: {
|
||
type: 'textarea',
|
||
description: 'Краткое описание статьи',
|
||
},
|
||
content: {
|
||
type: 'textarea',
|
||
isRequired: true,
|
||
},
|
||
category: {
|
||
availableValues: [
|
||
{ value: 'travel-tips', label: 'Советы путешественникам' },
|
||
{ value: 'culture', label: 'Культура' },
|
||
{ value: 'food', label: 'Еда' },
|
||
{ value: 'nature', label: 'Природа' },
|
||
{ value: 'history', label: 'История' }
|
||
],
|
||
},
|
||
is_published: { type: 'boolean' },
|
||
views: {
|
||
type: 'number',
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
},
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
},
|
||
updated_at: {
|
||
isVisible: { list: false, filter: false, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Bookings,
|
||
options: {
|
||
parent: { name: 'Заказы', icon: 'ShoppingCart' },
|
||
listProperties: ['id', 'customer_name', 'customer_email', 'preferred_date', 'status', 'total_price', 'created_at'],
|
||
editProperties: ['customer_name', 'customer_email', 'customer_phone', 'preferred_date', 'group_size', 'status', 'total_price', 'notes'],
|
||
showProperties: ['id', 'customer_name', 'customer_email', 'customer_phone', 'preferred_date', 'group_size', 'status', 'total_price', 'notes', 'created_at'],
|
||
filterProperties: ['customer_name', 'customer_email', 'status', 'preferred_date'],
|
||
properties: {
|
||
customer_name: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
customer_email: {
|
||
type: 'email',
|
||
isRequired: true,
|
||
},
|
||
customer_phone: { type: 'string' },
|
||
preferred_date: {
|
||
type: 'date',
|
||
isRequired: true,
|
||
},
|
||
group_size: {
|
||
type: 'number',
|
||
isRequired: true,
|
||
},
|
||
status: {
|
||
availableValues: [
|
||
{ value: 'pending', label: 'В ожидании' },
|
||
{ value: 'confirmed', label: 'Подтверждено' },
|
||
{ value: 'cancelled', label: 'Отменено' },
|
||
{ value: 'completed', label: 'Завершено' }
|
||
],
|
||
},
|
||
total_price: {
|
||
type: 'number',
|
||
isRequired: true,
|
||
},
|
||
notes: { type: 'textarea' },
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Reviews,
|
||
options: {
|
||
parent: { name: 'Отзывы', icon: 'Star' },
|
||
listProperties: ['id', 'customer_name', 'rating', 'is_approved', 'created_at'],
|
||
editProperties: ['customer_name', 'customer_email', 'rating', 'comment', 'is_approved'],
|
||
showProperties: ['id', 'customer_name', 'customer_email', 'rating', 'comment', 'is_approved', 'created_at'],
|
||
filterProperties: ['customer_name', 'rating', 'is_approved'],
|
||
properties: {
|
||
customer_name: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
customer_email: { type: 'email' },
|
||
rating: {
|
||
type: 'number',
|
||
availableValues: [
|
||
{ value: 1, label: '1 звезда' },
|
||
{ value: 2, label: '2 звезды' },
|
||
{ value: 3, label: '3 звезды' },
|
||
{ value: 4, label: '4 звезды' },
|
||
{ value: 5, label: '5 звезд' }
|
||
]
|
||
},
|
||
comment: { type: 'textarea' },
|
||
is_approved: { type: 'boolean' },
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: ContactMessages,
|
||
options: {
|
||
parent: { name: 'Сообщения', icon: 'Email' },
|
||
listProperties: ['id', 'name', 'email', 'subject', 'status', 'created_at'],
|
||
editProperties: ['name', 'email', 'phone', 'subject', 'message', 'status'],
|
||
showProperties: ['id', 'name', 'email', 'phone', 'subject', 'message', 'status', 'created_at'],
|
||
filterProperties: ['name', 'email', 'status'],
|
||
properties: {
|
||
name: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
email: {
|
||
type: 'email',
|
||
isRequired: true,
|
||
},
|
||
phone: { type: 'string' },
|
||
subject: {
|
||
type: 'string',
|
||
isRequired: true,
|
||
},
|
||
message: {
|
||
type: 'textarea',
|
||
isRequired: true,
|
||
},
|
||
status: {
|
||
availableValues: [
|
||
{ value: 'unread', label: 'Не прочитано' },
|
||
{ value: 'read', label: 'Прочитано' },
|
||
{ value: 'replied', label: 'Отвечено' }
|
||
],
|
||
},
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
actions: {
|
||
new: { isAccessible: false },
|
||
edit: { isAccessible: true },
|
||
delete: { isAccessible: true },
|
||
list: { isAccessible: true },
|
||
show: { isAccessible: true }
|
||
}
|
||
}
|
||
},
|
||
{
|
||
resource: Admins,
|
||
options: {
|
||
parent: { name: 'Администрирование', icon: 'Settings' },
|
||
listProperties: ['id', 'username', 'name', 'email', 'role', 'is_active', 'created_at'],
|
||
editProperties: ['username', 'name', 'email', 'role', 'is_active'],
|
||
showProperties: ['id', 'username', 'name', 'email', 'role', 'is_active', 'last_login', 'created_at'],
|
||
filterProperties: ['username', 'name', 'role', 'is_active'],
|
||
properties: {
|
||
username: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
name: {
|
||
type: 'string',
|
||
isRequired: true,
|
||
},
|
||
email: {
|
||
type: 'email',
|
||
isRequired: true,
|
||
},
|
||
password: {
|
||
type: 'password',
|
||
isVisible: { list: false, filter: false, show: false, edit: true }
|
||
},
|
||
role: {
|
||
availableValues: [
|
||
{ value: 'admin', label: 'Администратор' },
|
||
{ value: 'manager', label: 'Менеджер' },
|
||
{ value: 'editor', label: 'Редактор' }
|
||
],
|
||
},
|
||
is_active: { type: 'boolean' },
|
||
last_login: {
|
||
isVisible: { list: false, filter: false, show: true, edit: false },
|
||
},
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Ratings,
|
||
options: {
|
||
parent: { name: 'Система рейтингов', icon: 'Star' },
|
||
listProperties: ['id', 'target_type', 'target_id', 'rating', 'user_ip', 'created_at'],
|
||
showProperties: ['id', 'target_type', 'target_id', 'rating', 'user_ip', 'created_at'],
|
||
filterProperties: ['target_type', 'target_id', 'rating'],
|
||
properties: {
|
||
target_type: {
|
||
availableValues: [
|
||
{ value: 'route', label: 'Маршрут' },
|
||
{ value: 'guide', label: 'Гид' },
|
||
{ value: 'article', label: 'Статья' }
|
||
],
|
||
},
|
||
rating: {
|
||
availableValues: [
|
||
{ value: 1, label: '👍 Лайк' },
|
||
{ value: -1, label: '👎 Дизлайк' }
|
||
],
|
||
},
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
actions: {
|
||
new: { isAccessible: false },
|
||
edit: { isAccessible: false },
|
||
delete: { isAccessible: true },
|
||
list: { isAccessible: true },
|
||
show: { isAccessible: true }
|
||
}
|
||
}
|
||
},
|
||
{
|
||
resource: GuideSchedules,
|
||
options: {
|
||
parent: { name: 'Управление гидами', icon: 'Calendar' },
|
||
listProperties: ['id', 'guide_id', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_time', 'end_time'],
|
||
editProperties: ['guide_id', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_time', 'end_time'],
|
||
showProperties: ['id', 'guide_id', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'start_time', 'end_time', 'created_at', 'updated_at'],
|
||
filterProperties: ['guide_id'],
|
||
properties: {
|
||
guide_id: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
},
|
||
monday: { type: 'boolean' },
|
||
tuesday: { type: 'boolean' },
|
||
wednesday: { type: 'boolean' },
|
||
thursday: { type: 'boolean' },
|
||
friday: { type: 'boolean' },
|
||
saturday: { type: 'boolean' },
|
||
sunday: { type: 'boolean' },
|
||
start_time: {
|
||
type: 'string',
|
||
description: 'Время начала работы (формат HH:MM)'
|
||
},
|
||
end_time: {
|
||
type: 'string',
|
||
description: 'Время окончания работы (формат HH:MM)'
|
||
},
|
||
created_at: {
|
||
isVisible: { list: false, filter: false, show: true, edit: false },
|
||
},
|
||
updated_at: {
|
||
isVisible: { list: false, filter: false, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
},
|
||
{
|
||
resource: Holidays,
|
||
options: {
|
||
parent: { name: 'Управление гидами', icon: 'Calendar' },
|
||
listProperties: ['id', 'date', 'title', 'type', 'guide_id', 'created_at'],
|
||
editProperties: ['date', 'title', 'type', 'guide_id'],
|
||
showProperties: ['id', 'date', 'title', 'type', 'guide_id', 'created_at'],
|
||
filterProperties: ['date', 'type', 'guide_id'],
|
||
properties: {
|
||
date: {
|
||
isTitle: true,
|
||
isRequired: true,
|
||
type: 'date'
|
||
},
|
||
title: {
|
||
isRequired: true,
|
||
description: 'Название выходного дня'
|
||
},
|
||
type: {
|
||
availableValues: [
|
||
{ value: 'public', label: 'Общий выходной' },
|
||
{ value: 'guide_personal', label: 'Личный выходной гида' }
|
||
],
|
||
isRequired: true
|
||
},
|
||
guide_id: {
|
||
description: 'Оставить пустым для общих выходных'
|
||
},
|
||
created_at: {
|
||
isVisible: { list: true, filter: true, show: true, edit: false },
|
||
}
|
||
},
|
||
}
|
||
}
|
||
],
|
||
rootPath: '/admin',
|
||
branding: {
|
||
companyName: 'Korea Tourism Agency',
|
||
softwareBrothers: false,
|
||
theme: {
|
||
colors: {
|
||
primary100: '#ff6b6b',
|
||
primary80: '#ff5252',
|
||
primary60: '#ff3d3d',
|
||
primary40: '#ff2828',
|
||
primary20: '#ff1313',
|
||
grey100: '#151515',
|
||
grey80: '#333333',
|
||
grey60: '#666666',
|
||
grey40: '#999999',
|
||
grey20: '#cccccc',
|
||
filterBg: '#333333',
|
||
accent: '#38C172',
|
||
hoverBg: '#f0f0f0',
|
||
},
|
||
},
|
||
},
|
||
dashboard: {
|
||
component: false
|
||
}
|
||
};
|
||
|
||
// Создаем экземпляр AdminJS
|
||
const adminJs = new AdminJS(adminJsOptions);
|
||
|
||
// Настраиваем аутентификацию
|
||
const router = AdminJSExpress.buildAuthenticatedRouter(adminJs, {
|
||
authenticate: async (email, password) => {
|
||
try {
|
||
console.log('Attempting login for:', email);
|
||
|
||
const result = await authPool.query(
|
||
'SELECT * FROM admins WHERE username = $1 AND is_active = true',
|
||
[email]
|
||
);
|
||
|
||
if (result.rows.length === 0) {
|
||
console.log('No admin found with username:', email);
|
||
return null;
|
||
}
|
||
|
||
const admin = result.rows[0];
|
||
console.log('Admin found:', admin.name);
|
||
|
||
const isValid = await bcrypt.compare(password, admin.password);
|
||
|
||
if (isValid) {
|
||
console.log('Authentication successful for:', email);
|
||
return {
|
||
id: admin.id,
|
||
email: admin.username,
|
||
title: admin.name,
|
||
role: admin.role
|
||
};
|
||
}
|
||
|
||
console.log('Invalid password for:', email);
|
||
return null;
|
||
} catch (error) {
|
||
console.error('Auth error:', error);
|
||
return null;
|
||
}
|
||
},
|
||
cookiePassword: process.env.SESSION_SECRET || 'korea-tourism-secret-key-2024'
|
||
}, null, {
|
||
resave: false,
|
||
saveUninitialized: false,
|
||
secret: process.env.SESSION_SECRET || 'korea-tourism-secret-key-2024',
|
||
cookie: {
|
||
httpOnly: true,
|
||
secure: process.env.NODE_ENV === 'production',
|
||
maxAge: 24 * 60 * 60 * 1000 // 24 часа
|
||
}
|
||
});
|
||
|
||
export { adminJs, router }; |