🚀 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
This commit is contained in:
2025-11-30 00:53:15 +09:00
parent ed871fc4d1
commit b4e513e996
36 changed files with 6894 additions and 239 deletions

View File

@@ -0,0 +1,27 @@
import db from '../src/config/database.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
async function applyImageTriggersFix() {
try {
console.log('🖼️ Applying image triggers fix migration...');
const migrationPath = path.join(__dirname, 'image-triggers-fix.sql');
const sql = fs.readFileSync(migrationPath, 'utf8');
await db.query(sql);
console.log('✅ Image triggers fix applied successfully');
} catch (error) {
console.log(' Some changes may already be applied:', error.message);
}
process.exit(0);
}
applyImageTriggersFix();

58
database/check-admins.cjs Normal file
View File

@@ -0,0 +1,58 @@
require('dotenv').config();
const { Pool } = require('pg');
async function checkAdminsTable() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
try {
console.log('🔍 Проверяем таблицу admins...');
// Проверяем, существует ли таблица
const tableExists = await pool.query(`
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = 'admins'
)
`);
console.log('📋 Таблица admins существует:', tableExists.rows[0].exists);
if (tableExists.rows[0].exists) {
// Получаем структуру таблицы
const structure = await pool.query(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'admins'
ORDER BY ordinal_position
`);
console.log('\n📋 Структура таблицы admins:');
structure.rows.forEach(row => {
console.log(` - ${row.column_name}: ${row.data_type} ${row.is_nullable === 'NO' ? '(NOT NULL)' : '(nullable)'}`);
});
// Проверяем количество записей
const count = await pool.query('SELECT COUNT(*) FROM admins');
console.log(`\n📊 Записей в таблице: ${count.rows[0].count}`);
// Получаем несколько записей для примера
const sample = await pool.query('SELECT id, username, name, role, is_active FROM admins LIMIT 3');
console.log('\n👥 Примеры записей:');
sample.rows.forEach(admin => {
console.log(` - ID: ${admin.id}, Username: ${admin.username}, Name: ${admin.name}, Role: ${admin.role}, Active: ${admin.is_active}`);
});
} else {
console.log('❌ Таблица admins не существует! Нужно создать её.');
}
} catch (error) {
console.error('❌ Ошибка:', error.message);
} finally {
await pool.end();
}
}
checkAdminsTable();

View File

@@ -0,0 +1,40 @@
import db from '../src/config/database.js';
async function checkArticleCategories() {
try {
console.log('🔍 Проверяем ограничения на категории articles...');
// Проверяем constraint (для новых версий PostgreSQL)
const constraints = await db.query(`
SELECT conname, pg_get_constraintdef(oid) as definition
FROM pg_constraint
WHERE conrelid = 'articles'::regclass
AND contype = 'c'
`);
console.log('\n📋 Ограничения articles:');
constraints.rows.forEach(row => {
console.log(` - ${row.conname}: ${row.definition}`);
});
// Проверяем существующие категории
const existingCategories = await db.query(`
SELECT DISTINCT category
FROM articles
WHERE category IS NOT NULL
ORDER BY category
`);
console.log('\n📂 Существующие категории:');
existingCategories.rows.forEach(row => {
console.log(` - ${row.category}`);
});
} catch (error) {
console.error('❌ Ошибка:', error.message);
}
process.exit(0);
}
checkArticleCategories();

View File

@@ -0,0 +1,31 @@
require('dotenv').config();
const { Pool } = require('pg');
async function checkArticlesSchema() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL
});
try {
console.log('🔍 Проверяем структуру таблицы articles...');
const result = await pool.query(`
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = 'articles'
ORDER BY ordinal_position
`);
console.log('\n📋 Структура таблицы articles:');
result.rows.forEach(row => {
console.log(` - ${row.column_name}: ${row.data_type} ${row.is_nullable === 'NO' ? '(NOT NULL)' : '(nullable)'} ${row.column_default ? `default: ${row.column_default}` : ''}`);
});
} catch (error) {
console.error('❌ Ошибка:', error.message);
} finally {
await pool.end();
}
}
checkArticlesSchema();

51
database/check-tables.js Normal file
View File

@@ -0,0 +1,51 @@
import db from '../src/config/database.js';
async function checkTables() {
try {
console.log('🔍 Проверяем структуру таблиц...');
// Проверяем guides
const guidesDesc = await db.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'guides'
ORDER BY ordinal_position
`);
console.log('\n📋 Таблица guides:');
guidesDesc.rows.forEach(row => {
console.log(` - ${row.column_name}: ${row.data_type}`);
});
// Проверяем articles
const articlesDesc = await db.query(`
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_name = 'articles'
ORDER BY ordinal_position
`);
console.log('\n📰 Таблица articles:');
articlesDesc.rows.forEach(row => {
console.log(` - ${row.column_name}: ${row.data_type}`);
});
// Проверяем тип поля languages в guides
const languagesType = await db.query(`
SELECT data_type, column_default
FROM information_schema.columns
WHERE table_name = 'guides' AND column_name = 'languages'
`);
if (languagesType.rows.length > 0) {
console.log(`\n🔤 Поле languages: ${languagesType.rows[0].data_type}`);
}
} catch (error) {
console.error('❌ Ошибка:', error.message);
}
process.exit(0);
}
checkTables();

View File

@@ -0,0 +1,7 @@
-- Создание тестового администратора для проверки
INSERT INTO admins (username, password, name, email, role) VALUES
('admin', '$2a$10$rOjLbFbCqbCQPZdJQWb1gO6WvhzJP1O5VuItXwDJV4tTJYg4oEGoC', 'Главный администратор', 'admin@koreatour.ru', 'admin')
ON CONFLICT (username) DO UPDATE SET
name = EXCLUDED.name,
email = EXCLUDED.email,
role = EXCLUDED.role;

View File

@@ -0,0 +1,25 @@
// Создание тестового администратора для AdminJS
const crypto = require('crypto');
function generateBcryptHash(password) {
// Упрощенная версия - используем готовый хеш
// Пароль: admin123
return '$2a$10$rOjLbFbCqbCQPZdJQWb1gO6WvhzJP1O5VuItXwDJV4tTJYg4oEGoC';
}
function createTestAdmin() {
const hashedPassword = generateBcryptHash('admin123');
console.log('🔐 Создаем тестового администратора...');
console.log('📧 Email: admin@koreatour.ru');
console.log('👤 Username: admin');
console.log('🔑 Password: admin123');
console.log('🔒 Hashed password:', hashedPassword);
const sql = `INSERT INTO admins (username, password, name, email, role, is_active) VALUES ('admin', '${hashedPassword}', 'Главный администратор', 'admin@koreatour.ru', 'admin', true) ON CONFLICT (username) DO UPDATE SET password = EXCLUDED.password, name = EXCLUDED.name, email = EXCLUDED.email, role = EXCLUDED.role, is_active = EXCLUDED.is_active;`;
console.log('\n▶ Запустите эту команду:');
console.log(`docker-compose exec db psql -U tourism_user -d korea_tourism -c "${sql}"`);
}
createTestAdmin();

View File

@@ -0,0 +1,57 @@
-- Исправление триггеров для таблиц с изображениями
-- Запускается автоматически при инициализации базы данных
-- Сначала удаляем существующие проблемные триггеры если они есть
DROP TRIGGER IF EXISTS routes_updated_at_trigger ON routes;
DROP TRIGGER IF EXISTS guides_updated_at_trigger ON guides;
-- Создаем функцию для обновления updated_at (если её нет)
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Создаем триггеры для обновления updated_at при изменении записей
CREATE TRIGGER routes_updated_at_trigger
BEFORE UPDATE ON routes
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER guides_updated_at_trigger
BEFORE UPDATE ON guides
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Добавляем индексы для оптимизации запросов к изображениям
CREATE INDEX IF NOT EXISTS idx_routes_image_url ON routes(image_url) WHERE image_url IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_guides_image_url ON guides(image_url) WHERE image_url IS NOT NULL;
-- Создаем таблицу для метаданных загруженных изображений (опционально)
CREATE TABLE IF NOT EXISTS image_metadata (
id SERIAL PRIMARY KEY,
url VARCHAR(500) NOT NULL UNIQUE,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255),
size_bytes INTEGER,
width INTEGER,
height INTEGER,
mime_type VARCHAR(100),
entity_type VARCHAR(50), -- 'routes', 'guides', 'articles', etc.
entity_id INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Индекс для поиска изображений по типу сущности
CREATE INDEX IF NOT EXISTS idx_image_metadata_entity ON image_metadata(entity_type, entity_id);
-- Триггер для image_metadata
CREATE TRIGGER image_metadata_updated_at_trigger
BEFORE UPDATE ON image_metadata
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
COMMIT;

View File

@@ -79,6 +79,19 @@ export async function initDatabase() {
}
}
// 4. Apply image triggers fix migration
try {
console.log('🖼️ Installing image triggers fix...');
const imageTriggersMigrationPath = path.join(__dirname, 'image-triggers-fix.sql');
if (fs.existsSync(imageTriggersMigrationPath)) {
const imageTriggersMigration = fs.readFileSync(imageTriggersMigrationPath, 'utf8');
await db.query(imageTriggersMigration);
console.log('✅ Image triggers fix applied successfully');
}
} catch (error) {
console.log(' Image triggers fix - some changes may already be applied:', error.message);
}
console.log('✨ Database initialization completed successfully!');
} catch (error) {

View File

@@ -0,0 +1,36 @@
import db from '../src/config/database.js';
async function updateTestImages() {
try {
console.log('🖼️ Обновляем тестовые данные с изображениями...');
// Обновляем первый тур с изображением
await db.query('UPDATE routes SET image_url = $1 WHERE id = 1', ['/uploads/routes/seoul-city-tour.jpg']);
// Обновляем первого гида с изображением
await db.query('UPDATE guides SET image_url = $1 WHERE id = 1', ['/uploads/guides/guide-profile.jpg']);
console.log('✅ Изображения добавлены в базу данных');
// Проверяем результат
const routes = await db.query('SELECT id, title, image_url FROM routes WHERE image_url IS NOT NULL LIMIT 3');
const guides = await db.query('SELECT id, name, image_url FROM guides WHERE image_url IS NOT NULL LIMIT 3');
console.log('📋 Туры с изображениями:');
routes.rows.forEach(row => {
console.log(` - ${row.title}: ${row.image_url}`);
});
console.log('👨‍🏫 Гиды с изображениями:');
guides.rows.forEach(row => {
console.log(` - ${row.name}: ${row.image_url}`);
});
} catch (error) {
console.error('❌ Ошибка:', error);
}
process.exit(0);
}
updateTestImages();