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 };