✨ 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
236 lines
7.5 KiB
JavaScript
236 lines
7.5 KiB
JavaScript
import express from 'express';
|
||
import path from 'path';
|
||
import session from 'express-session';
|
||
import cors from 'cors';
|
||
import helmet from 'helmet';
|
||
import compression from 'compression';
|
||
import morgan from 'morgan';
|
||
import methodOverride from 'method-override';
|
||
import formatters from './helpers/formatters.js';
|
||
import SiteSettingsHelper from './helpers/site-settings.js';
|
||
import { adminJs, router as adminRouter } from './config/adminjs-simple.js';
|
||
import { fileURLToPath } from 'url';
|
||
import { dirname } from 'path';
|
||
import { createRequire } from 'module';
|
||
import dotenv from 'dotenv';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
const require = createRequire(import.meta.url);
|
||
|
||
dotenv.config();
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3000;
|
||
|
||
async function setupApp() {
|
||
|
||
// Initialize database on startup
|
||
try {
|
||
console.log('🚀 Initializing database...');
|
||
const { initDatabase } = await import('../database/init-database.js');
|
||
await initDatabase();
|
||
console.log('✅ Database initialized successfully');
|
||
} catch (error) {
|
||
console.error('❌ Database initialization failed:', error);
|
||
console.log('⚠️ Continuing without database initialization...');
|
||
}
|
||
|
||
// Security middleware
|
||
app.use(helmet({
|
||
contentSecurityPolicy: {
|
||
directives: {
|
||
defaultSrc: ["'self'"],
|
||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com"],
|
||
fontSrc: ["'self'", "https://fonts.gstatic.com", "https://cdnjs.cloudflare.com"],
|
||
scriptSrc: ["'self'", "'unsafe-inline'", "'unsafe-eval'", "https://cdn.jsdelivr.net", "https://cdnjs.cloudflare.com", "https://code.jquery.com"],
|
||
imgSrc: ["'self'", "data:", "https:", "blob:"],
|
||
connectSrc: ["'self'"],
|
||
},
|
||
},
|
||
}));
|
||
app.use(compression());
|
||
app.use(morgan('combined'));
|
||
app.use(cors());
|
||
|
||
// Method override for PUT/DELETE requests
|
||
app.use(methodOverride('_method'));
|
||
|
||
// Static files
|
||
app.use(express.static(path.join(__dirname, '../public')));
|
||
|
||
// Serve node_modules for AdminLTE assets
|
||
app.use('/node_modules', express.static(path.join(__dirname, '../node_modules')));
|
||
|
||
// Session configuration
|
||
app.use(session({
|
||
secret: process.env.SESSION_SECRET || 'korea-tourism-secret',
|
||
resave: false,
|
||
saveUninitialized: false,
|
||
cookie: {
|
||
secure: process.env.NODE_ENV === 'production',
|
||
maxAge: 24 * 60 * 60 * 1000 // 24 hours
|
||
}
|
||
}));
|
||
|
||
// View engine setup
|
||
app.set('view engine', 'ejs');
|
||
app.set('views', path.join(__dirname, '../views'));
|
||
|
||
// Global template variables with site settings
|
||
app.use(async (req, res, next) => {
|
||
res.locals.siteName = process.env.SITE_NAME || 'Корея Тур Агентство';
|
||
res.locals.siteDescription = process.env.SITE_DESCRIPTION || 'Откройте для себя красоту Кореи';
|
||
res.locals.user = req.session.user || null;
|
||
res.locals.admin = req.session.admin || null;
|
||
res.locals.currentPath = req.path;
|
||
res.locals.page = 'home'; // default page
|
||
|
||
// Load site settings for templates
|
||
try {
|
||
res.locals.siteSettings = await SiteSettingsHelper.getAllSettings();
|
||
} catch (error) {
|
||
console.error('Error loading site settings for templates:', error);
|
||
res.locals.siteSettings = {};
|
||
}
|
||
|
||
// Add all helper functions to template globals
|
||
Object.assign(res.locals, formatters);
|
||
|
||
next();
|
||
});
|
||
|
||
// Layout middleware
|
||
app.use((req, res, next) => {
|
||
const originalRender = res.render;
|
||
|
||
res.render = function(view, locals, callback) {
|
||
if (typeof locals === 'function') {
|
||
callback = locals;
|
||
locals = {};
|
||
}
|
||
locals = locals || {};
|
||
|
||
// Check if it's an admin route
|
||
if (req.path.startsWith('/admin')) {
|
||
// Check if a custom layout is specified
|
||
if (locals.layout) {
|
||
const customLayout = locals.layout;
|
||
delete locals.layout;
|
||
|
||
// Render the view content first
|
||
originalRender.call(this, view, locals, (err, html) => {
|
||
if (err) return callback ? callback(err) : next(err);
|
||
|
||
// Then render the custom layout with the content
|
||
locals.body = html;
|
||
originalRender.call(res, customLayout, locals, callback);
|
||
});
|
||
} else {
|
||
return originalRender.call(this, view, locals, callback);
|
||
}
|
||
} else {
|
||
// Render the view content first
|
||
originalRender.call(this, view, locals, (err, html) => {
|
||
if (err) return callback ? callback(err) : next(err);
|
||
|
||
// Then render the layout with the content
|
||
locals.body = html;
|
||
originalRender.call(res, 'layout', locals, callback);
|
||
});
|
||
}
|
||
};
|
||
|
||
next();
|
||
});
|
||
|
||
// Routes
|
||
app.use(adminJs.options.rootPath, adminRouter); // AdminJS routes
|
||
|
||
// Body parser middleware (after AdminJS)
|
||
app.use(express.json({ limit: '10mb' }));
|
||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||
|
||
// Dynamic imports for CommonJS routes
|
||
const indexRouter = (await import('./routes/index.js')).default;
|
||
const toursRouter = (await import('./routes/tours.js')).default;
|
||
const guidesRouter = (await import('./routes/guides.js')).default;
|
||
const articlesRouter = (await import('./routes/articles.js')).default;
|
||
const apiRouter = (await import('./routes/api.js')).default;
|
||
const settingsRouter = (await import('./routes/settings.js')).default;
|
||
const ratingsRouter = (await import('./routes/ratings.js')).default;
|
||
const imagesRouter = (await import('./routes/images.js')).default;
|
||
const crudRouter = (await import('./routes/crud.js')).default;
|
||
|
||
app.use('/', indexRouter);
|
||
app.use('/routes', toursRouter);
|
||
app.use('/guides', guidesRouter);
|
||
app.use('/articles', articlesRouter);
|
||
app.use('/api', apiRouter);
|
||
app.use('/api', ratingsRouter);
|
||
app.use('/', settingsRouter); // Settings routes (CSS and API)
|
||
app.use('/api/images', imagesRouter); // Image management routes
|
||
app.use('/api/crud', crudRouter); // CRUD API routes
|
||
|
||
// Health check endpoint
|
||
app.get('/health', (req, res) => {
|
||
res.json({
|
||
status: 'OK',
|
||
timestamp: new Date().toISOString(),
|
||
uptime: process.uptime()
|
||
});
|
||
});
|
||
|
||
// Test image editor endpoint
|
||
app.get('/test-editor', (req, res) => {
|
||
res.sendFile(path.join(__dirname, '../public/test-editor.html'));
|
||
});
|
||
|
||
// Image system documentation
|
||
app.get('/image-docs', (req, res) => {
|
||
res.sendFile(path.join(__dirname, '../public/image-system-docs.html'));
|
||
});
|
||
|
||
// Error handling
|
||
app.use((req, res) => {
|
||
res.status(404).render('error', {
|
||
title: '404 - Page Not Found',
|
||
message: 'The page you are looking for does not exist.',
|
||
error: { status: 404 },
|
||
layout: 'layout'
|
||
});
|
||
});
|
||
|
||
app.use((err, req, res, next) => {
|
||
console.error('Error:', err.stack);
|
||
|
||
// Don't expose stack trace in production
|
||
const isDev = process.env.NODE_ENV === 'development';
|
||
|
||
res.status(err.status || 500).render('error', {
|
||
title: `${err.status || 500} - Server Error`,
|
||
message: isDev ? err.message : 'Something went wrong on our server.',
|
||
error: isDev ? err : { status: err.status || 500 },
|
||
layout: 'layout'
|
||
});
|
||
});
|
||
|
||
// Graceful shutdown
|
||
process.on('SIGTERM', () => {
|
||
console.log('SIGTERM received, shutting down gracefully');
|
||
server.close(() => {
|
||
console.log('Process terminated');
|
||
});
|
||
});
|
||
|
||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||
console.log(`🚀 Korea Tourism Agency server running on port ${PORT}`);
|
||
console.log(`📍 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||
console.log(`🔧 Admin panel: http://localhost:${PORT}${adminJs.options.rootPath}`);
|
||
console.log(`🏠 Website: http://localhost:${PORT}`);
|
||
});
|
||
|
||
}
|
||
|
||
// Start the application
|
||
setupApp().catch(console.error); |