🚀 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:
27
database/apply-image-fix.js
Normal file
27
database/apply-image-fix.js
Normal 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
58
database/check-admins.cjs
Normal 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();
|
||||
40
database/check-article-categories.js
Normal file
40
database/check-article-categories.js
Normal 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();
|
||||
31
database/check-articles-schema.cjs
Normal file
31
database/check-articles-schema.cjs
Normal 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
51
database/check-tables.js
Normal 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();
|
||||
7
database/create-admin.sql
Normal file
7
database/create-admin.sql
Normal 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;
|
||||
25
database/create-test-admin.cjs
Normal file
25
database/create-test-admin.cjs
Normal 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();
|
||||
57
database/image-triggers-fix.sql
Normal file
57
database/image-triggers-fix.sql
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
36
database/update-test-images.js
Normal file
36
database/update-test-images.js
Normal 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();
|
||||
Reference in New Issue
Block a user