Files
tourrism_site/src/config/adminjs-simple.js
Andrey K. Choi b4e513e996 🚀 Korea Tourism Agency - Complete implementation
 Features:
- Modern tourism website with responsive design
- AdminJS admin panel with image editor integration
- PostgreSQL database with comprehensive schema
- Docker containerization
- Image upload and gallery management

🛠 Tech Stack:
- Backend: Node.js + Express.js
- Database: PostgreSQL 13+
- Frontend: HTML/CSS/JS with responsive design
- Admin: AdminJS with custom components
- Deployment: Docker + Docker Compose
- Image Processing: Sharp with optimization

📱 Admin Features:
- Routes/Tours management (city, mountain, fishing)
- Guides profiles with specializations
- Articles and blog system
- Image editor with upload/gallery/URL options
- User management and authentication
- Responsive admin interface

🎨 Design:
- Korean tourism focused branding
- Mobile-first responsive design
- Custom CSS with modern aesthetics
- Image optimization and gallery
- SEO-friendly structure

🔒 Security:
- Helmet.js security headers
- bcrypt password hashing
- Input validation and sanitization
- CORS protection
- Environment variables
2025-11-30 00:53:15 +09:00

810 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AdminJS from 'adminjs';
import AdminJSExpress from '@adminjs/express';
import AdminJSSequelize from '@adminjs/sequelize';
import uploadFeature from '@adminjs/upload';
import bcrypt from 'bcryptjs';
import pkg from 'pg';
import { Sequelize, DataTypes } from 'sequelize';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
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 },
image_url: { type: DataTypes.STRING },
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 },
image_url: { type: DataTypes.STRING },
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'
});
// Модель настроек сайта
const SiteSettings = sequelize.define('site_settings', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
setting_key: { type: DataTypes.STRING, allowNull: false, unique: true },
setting_value: { type: DataTypes.TEXT },
setting_type: { type: DataTypes.ENUM('text', 'number', 'boolean', 'json', 'color', 'file'), defaultValue: 'text' },
description: { type: DataTypes.TEXT },
category: { type: DataTypes.STRING, defaultValue: 'general' },
updated_at: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }
}, {
timestamps: false,
tableName: 'site_settings'
});
// Определение связей между моделями
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', 'image_url', 'is_featured', 'is_active'],
showProperties: ['id', 'title', 'description', 'content', 'type', 'difficulty_level', 'price', 'duration', 'max_group_size', 'image_url', '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,
},
image_url: {
type: 'string',
description: 'Изображение тура. Кнопка "Выбрать" будет добавлена автоматически'
},
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', 'image_url', 'hourly_rate', 'is_active'],
showProperties: ['id', 'name', 'email', 'phone', 'languages', 'specialization', 'bio', 'experience', 'image_url', '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: 'Опыт работы в годах',
},
image_url: {
type: 'string',
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', 'image_url', '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: 'История' }
],
},
image_url: {
type: 'string',
description: 'Изображение статьи. Кнопка "Выбрать" будет добавлена автоматически'
},
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 },
}
},
}
},
{
resource: SiteSettings,
options: {
parent: { name: 'Администрирование', icon: 'Settings' },
listProperties: ['id', 'setting_key', 'setting_value', 'category', 'updated_at'],
editProperties: ['setting_key', 'setting_value', 'setting_type', 'description', 'category'],
showProperties: ['id', 'setting_key', 'setting_value', 'setting_type', 'description', 'category', 'updated_at'],
filterProperties: ['setting_key', 'category', 'setting_type'],
properties: {
setting_key: {
isTitle: true,
isRequired: true,
description: 'Уникальный ключ настройки (например: primary_color, hero_background_url)'
},
setting_value: {
type: 'textarea',
isRequired: true,
description: 'Значение настройки (цвет в HEX, URL изображения, текст и т.д.)'
},
setting_type: {
availableValues: [
{ value: 'text', label: 'Текст' },
{ value: 'number', label: 'Число' },
{ value: 'boolean', label: 'Да/Нет' },
{ value: 'json', label: 'JSON' },
{ value: 'color', label: 'Цвет (HEX)' },
{ value: 'file', label: 'Файл/URL' }
],
isRequired: true
},
description: {
type: 'textarea',
description: 'Описание назначения этой настройки'
},
category: {
availableValues: [
{ value: 'general', label: 'Общие' },
{ value: 'theme', label: 'Тема и стили' },
{ value: 'colors', label: 'Цвета' },
{ value: 'typography', label: 'Типографика' },
{ value: 'images', label: 'Изображения' },
{ value: 'effects', label: 'Эффекты' },
{ value: 'layout', label: 'Макет' }
],
defaultValue: 'general'
},
updated_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
},
assets: {
styles: ['/css/admin-custom.css'],
scripts: ['/js/admin-image-selector-fixed.js']
}
};
// Создаем экземпляр AdminJS с componentLoader
// Создание 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 };