const express = require('express'); const router = express.Router(); const multer = require('multer'); const sharp = require('sharp'); const path = require('path'); const fs = require('fs').promises; const { body, validationResult } = require('express-validator'); const { User, Portfolio, Service, Contact, SiteSettings, Banner } = require('../models'); // Configure multer for file uploads const storage = multer.memoryStorage(); // Use memory storage to process with sharp const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit fileFilter: (req, file, cb) => { if (file.mimetype.startsWith('image/')) { cb(null, true); } else { cb(new Error('Only image files are allowed'), false); } } }); // Authentication middleware const requireAuth = (req, res, next) => { if (!req.session.user) { return res.redirect('/admin/login'); } next(); }; // Add stats middleware const addStats = async (req, res, next) => { try { if (req.session.user) { const [portfolioCount, servicesCount, contactsCount] = await Promise.all([ Portfolio.count({ where: { isPublished: true } }), Service.count({ where: { isActive: true } }), Contact.count() ]); res.locals.stats = { portfolioCount, servicesCount, contactsCount }; } next(); } catch (error) { console.error('Stats middleware error:', error); next(); } }; // Admin login page router.get('/login', (req, res) => { if (req.session.user) { return res.redirect('/admin/dashboard'); } res.render('admin/login', { title: 'Admin Login', error: null }); }); // Admin login POST router.post('/login', async (req, res) => { try { const { email, password } = req.body; const user = await User.findOne({ where: { email: email, isActive: true } }); if (!user || !(await user.comparePassword(password))) { return res.render('admin/login', { title: 'Admin Login', error: 'Invalid credentials' }); } await user.updateLastLogin(); req.session.user = { id: user.id, email: user.email, name: user.name, role: user.role }; res.redirect('/admin/dashboard'); } catch (error) { console.error('Admin login error:', error); res.render('admin/login', { title: 'Admin Login', error: 'Server error' }); } }); // Admin logout router.post('/logout', (req, res) => { req.session.destroy(err => { if (err) { console.error('Logout error:', err); } res.redirect('/admin/login'); }); }); // Dashboard (default route) router.get('/', requireAuth, async (req, res) => { res.redirect('/admin/dashboard'); }); // Dashboard router.get('/dashboard', requireAuth, async (req, res) => { try { const [ portfolioCount, servicesCount, contactsCount, recentContacts, recentPortfolio ] = await Promise.all([ Portfolio.count({ where: { isPublished: true } }), Service.count({ where: { isActive: true } }), Contact.count(), Contact.findAll({ order: [['createdAt', 'DESC']], limit: 5 }), Portfolio.findAll({ where: { isPublished: true }, order: [['createdAt', 'DESC']], limit: 5 }) ]); const stats = { portfolioCount: portfolioCount, servicesCount: servicesCount, contactsCount: contactsCount, usersCount: await User.count() }; res.render('admin/dashboard', { title: 'Admin Dashboard', layout: 'admin/layout', user: req.session.user, stats, recentContacts, recentPortfolio, currentPage: 'dashboard' }); } catch (error) { console.error('Dashboard error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading dashboard' }); } }); // Banner management router.get('/banners', requireAuth, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = 20; const skip = (page - 1) * limit; const [banners, total] = await Promise.all([ Banner.findAll({ order: [['order', 'ASC'], ['createdAt', 'DESC']], offset: skip, limit: limit }), Banner.count() ]); const totalPages = Math.ceil(total / limit); res.render('admin/banners/list', { title: 'Banner Management - Admin Panel', layout: 'admin/layout', user: req.session.user, banners, pagination: { current: page, total: totalPages, hasNext: page < totalPages, hasPrev: page > 1 } }); } catch (error) { console.error('Banner list error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading banners' }); } }); // Add banner router.get('/banners/add', requireAuth, (req, res) => { res.render('admin/banners/add', { title: 'Add Banner - Admin Panel', layout: 'admin/layout', user: req.session.user, positions: [ { value: 'hero', label: '메인 히어로' }, { value: 'secondary', label: '보조 배너' }, { value: 'footer', label: '푸터 배너' } ], animations: [ { value: 'none', label: '없음' }, { value: 'fade', label: '페이드' }, { value: 'slide', label: '슬라이드' }, { value: 'zoom', label: '줌' } ] }); }); // Create banner router.post('/banners/add', requireAuth, [ body('title').notEmpty().withMessage('제목을 입력해주세요'), body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'), body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: '입력 데이터를 확인해주세요', errors: errors.array() }); } const { title, subtitle, description, buttonText, buttonUrl, image, mobileImage, position, order, isActive = true, startDate, endDate, targetAudience = 'all', backgroundColor, textColor, animation = 'none' } = req.body; const banner = await Banner.create({ title, subtitle: subtitle || null, description: description || null, buttonText: buttonText || null, buttonUrl: buttonUrl || null, image: image || null, mobileImage: mobileImage || null, position, order: parseInt(order), isActive: Boolean(isActive), startDate: startDate || null, endDate: endDate || null, targetAudience, backgroundColor: backgroundColor || null, textColor: textColor || null, animation }); res.json({ success: true, message: '배너가 성공적으로 생성되었습니다', banner: { id: banner.id, title: banner.title, position: banner.position } }); } catch (error) { console.error('Banner creation error:', error); res.status(500).json({ success: false, message: '배너 생성 중 오류가 발생했습니다' }); } }); // Edit banner router.get('/banners/edit/:id', requireAuth, async (req, res) => { try { const banner = await Banner.findByPk(req.params.id); if (!banner) { return res.status(404).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Banner not found' }); } res.render('admin/banners/edit', { title: 'Edit Banner - Admin Panel', layout: 'admin/layout', user: req.session.user, banner, positions: [ { value: 'hero', label: '메인 히어로' }, { value: 'secondary', label: '보조 배너' }, { value: 'footer', label: '푸터 배너' } ], animations: [ { value: 'none', label: '없음' }, { value: 'fade', label: '페이드' }, { value: 'slide', label: '슬라이드' }, { value: 'zoom', label: '줌' } ] }); } catch (error) { console.error('Banner edit error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading banner' }); } }); // Update banner router.put('/banners/:id', requireAuth, [ body('title').notEmpty().withMessage('제목을 입력해주세요'), body('position').isIn(['hero', 'secondary', 'footer']).withMessage('유효한 위치를 선택해주세요'), body('order').isInt({ min: 0 }).withMessage('유효한 순서를 입력해주세요') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: '입력 데이터를 확인해주세요', errors: errors.array() }); } const banner = await Banner.findByPk(req.params.id); if (!banner) { return res.status(404).json({ success: false, message: '배너를 찾을 수 없습니다' }); } const { title, subtitle, description, buttonText, buttonUrl, image, mobileImage, position, order, isActive, startDate, endDate, targetAudience, backgroundColor, textColor, animation } = req.body; await banner.update({ title, subtitle: subtitle || null, description: description || null, buttonText: buttonText || null, buttonUrl: buttonUrl || null, image: image || null, mobileImage: mobileImage || null, position, order: parseInt(order), isActive: Boolean(isActive), startDate: startDate || null, endDate: endDate || null, targetAudience: targetAudience || 'all', backgroundColor: backgroundColor || null, textColor: textColor || null, animation: animation || 'none' }); res.json({ success: true, message: '배너가 성공적으로 업데이트되었습니다', banner: { id: banner.id, title: banner.title, position: banner.position } }); } catch (error) { console.error('Banner update error:', error); res.status(500).json({ success: false, message: '배너 업데이트 중 오류가 발생했습니다' }); } }); // Delete banner router.delete('/banners/:id', requireAuth, async (req, res) => { try { const banner = await Banner.findByPk(req.params.id); if (!banner) { return res.status(404).json({ success: false, message: '배너를 찾을 수 없습니다' }); } await banner.destroy(); res.json({ success: true, message: '배너가 성공적으로 삭제되었습니다' }); } catch (error) { console.error('Banner deletion error:', error); res.status(500).json({ success: false, message: '배너 삭제 중 오류가 발생했습니다' }); } }); // Toggle banner active status router.patch('/banners/:id/toggle-active', requireAuth, async (req, res) => { try { const banner = await Banner.findByPk(req.params.id); if (!banner) { return res.status(404).json({ success: false, message: '배너를 찾을 수 없습니다' }); } const newStatus = !banner.isActive; await banner.update({ isActive: newStatus }); res.json({ success: true, message: `배너가 ${newStatus ? '활성화' : '비활성화'}되었습니다`, isActive: newStatus }); } catch (error) { console.error('Banner toggle active error:', error); res.status(500).json({ success: false, message: '상태 변경 중 오류가 발생했습니다' }); } }); // Record banner click router.post('/banners/:id/click', async (req, res) => { try { const banner = await Banner.findByPk(req.params.id); if (!banner) { return res.status(404).json({ success: false, message: '배너를 찾을 수 없습니다' }); } await banner.recordClick(); res.json({ success: true, clickCount: banner.clickCount }); } catch (error) { console.error('Banner click record error:', error); res.status(500).json({ success: false, message: '클릭 기록 중 오류가 발생했습니다' }); } }); // Banner Editor (legacy route) router.get('/banner-editor', requireAuth, addStats, async (req, res) => { res.redirect('/admin/banners'); }); // Portfolio management router.get('/portfolio', requireAuth, addStats, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = 20; const skip = (page - 1) * limit; const [portfolio, total] = await Promise.all([ Portfolio.findAll({ order: [['createdAt', 'DESC']], offset: skip, limit: limit }), Portfolio.count() ]); const totalPages = Math.ceil(total / limit); res.render('admin/portfolio/list', { title: 'Portfolio Management - Admin Panel', layout: 'admin/layout', currentPage: 'portfolio', user: req.session.user, portfolio, pagination: { current: page, total: totalPages, hasNext: page < totalPages, hasPrev: page > 1 } }); } catch (error) { console.error('Portfolio list error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading portfolio' }); } }); // Utility function for category names const getCategoryName = (category) => { const categoryNames = { 'web-development': 'Веб-разработка', 'mobile-app': 'Мобильные приложения', 'ui-ux-design': 'UI/UX дизайн', 'e-commerce': 'Электронная коммерция', 'enterprise': 'Корпоративное ПО', 'other': 'Другое' }; return categoryNames[category] || category; }; // Add portfolio item router.get('/portfolio/add', requireAuth, (req, res) => { res.render('admin/portfolio/add', { title: 'Add Portfolio Item - Admin Panel', layout: 'admin/layout', user: req.session.user, categories: [ 'web-development', 'mobile-app', 'ui-ux-design', 'e-commerce', 'enterprise', 'other' ], getCategoryName: getCategoryName }); }); // Create portfolio item router.post('/portfolio/add', requireAuth, upload.array('images', 10), async (req, res) => { try { const { title, shortDescription, description, category, technologies, demoUrl, githubUrl, clientName, duration, seoTitle, seoDescription, isPublished, featured } = req.body; // Validate required fields if (!title || !shortDescription || !description || !category) { return res.status(400).json({ success: false, message: 'Заполните все обязательные поля' }); } // Parse technologies from JSON string let parsedTechnologies = []; try { parsedTechnologies = JSON.parse(technologies || '[]'); } catch (e) { parsedTechnologies = typeof technologies === 'string' ? [technologies] : []; } if (parsedTechnologies.length === 0) { return res.status(400).json({ success: false, message: 'Добавьте хотя бы одну технологию' }); } // Process uploaded images const processedImages = []; if (req.files && req.files.length > 0) { const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio'); // Ensure directory exists try { await fs.access(uploadDir); } catch { await fs.mkdir(uploadDir, { recursive: true }); } for (const file of req.files) { const timestamp = Date.now(); const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`; const filepath = path.join(uploadDir, filename); // Process image with Sharp await sharp(file.buffer) .webp({ quality: 85 }) .resize(1200, 800, { fit: 'inside', withoutEnlargement: true }) .toFile(filepath); processedImages.push(`/uploads/portfolio/${filename}`); } } // Create SEO data const seo = {}; if (seoTitle) seo.title = seoTitle; if (seoDescription) seo.description = seoDescription; // Create portfolio item const portfolio = await Portfolio.create({ title, shortDescription, description, category, technologies: parsedTechnologies, images: processedImages, projectUrl: demoUrl || null, githubUrl: githubUrl || null, clientName: clientName || null, duration: duration ? parseInt(duration) : null, isPublished: isPublished === 'true' || isPublished === true, featured: featured === 'true' || featured === true, publishedAt: (isPublished === 'true' || isPublished === true) ? new Date() : null, status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft', seo: Object.keys(seo).length > 0 ? seo : null }); res.json({ success: true, message: 'Проект успешно создан!', portfolio: { id: portfolio.id, title: portfolio.title, category: portfolio.category, isPublished: portfolio.isPublished } }); } catch (error) { console.error('Portfolio creation error:', error); res.status(500).json({ success: false, message: 'Ошибка при создании проекта: ' + error.message }); } }); // Edit portfolio item router.get('/portfolio/edit/:id', requireAuth, async (req, res) => { try { const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio) { return res.status(404).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Portfolio item not found' }); } res.render('admin/portfolio/edit', { title: 'Edit Portfolio Item - Admin Panel', layout: 'admin/layout', user: req.session.user, portfolio, categories: [ 'web-development', 'mobile-app', 'ui-ux-design', 'e-commerce', 'enterprise', 'other' ] }); } catch (error) { console.error('Portfolio edit error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading portfolio item' }); } }); // Update portfolio item router.put('/portfolio/:id', requireAuth, upload.array('images', 10), async (req, res) => { try { const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio) { return res.status(404).json({ success: false, message: 'Проект не найден' }); } const { title, shortDescription, description, category, technologies, demoUrl, githubUrl, clientName, duration, seoTitle, seoDescription, isPublished, featured, existingImages } = req.body; // Validate required fields if (!title || !shortDescription || !description || !category) { return res.status(400).json({ success: false, message: 'Заполните все обязательные поля' }); } // Parse technologies from JSON string let parsedTechnologies = []; try { parsedTechnologies = JSON.parse(technologies || '[]'); } catch (e) { parsedTechnologies = typeof technologies === 'string' ? [technologies] : []; } // Handle existing images let finalImages = []; try { const existing = JSON.parse(existingImages || '[]'); finalImages = Array.isArray(existing) ? existing : []; } catch (e) { finalImages = portfolio.images || []; } // Process new uploaded images if (req.files && req.files.length > 0) { const uploadDir = path.join(process.cwd(), 'public', 'uploads', 'portfolio'); // Ensure directory exists try { await fs.access(uploadDir); } catch { await fs.mkdir(uploadDir, { recursive: true }); } for (const file of req.files) { const timestamp = Date.now(); const filename = `${timestamp}-${Math.random().toString(36).substr(2, 9)}.webp`; const filepath = path.join(uploadDir, filename); // Process image with Sharp await sharp(file.buffer) .webp({ quality: 85 }) .resize(1200, 800, { fit: 'inside', withoutEnlargement: true }) .toFile(filepath); finalImages.push(`/uploads/portfolio/${filename}`); } } // Create SEO data const seo = portfolio.seo || {}; if (seoTitle) seo.title = seoTitle; if (seoDescription) seo.description = seoDescription; // Update portfolio await portfolio.update({ title, shortDescription, description, category, technologies: parsedTechnologies, images: finalImages, projectUrl: demoUrl || null, githubUrl: githubUrl || null, clientName: clientName || null, duration: duration ? parseInt(duration) : null, isPublished: isPublished === 'true' || isPublished === true, featured: featured === 'true' || featured === true, publishedAt: (isPublished === 'true' || isPublished === true) && !portfolio.publishedAt ? new Date() : portfolio.publishedAt, status: (isPublished === 'true' || isPublished === true) ? 'published' : 'draft', seo: Object.keys(seo).length > 0 ? seo : null }); res.json({ success: true, message: 'Проект успешно обновлен!', portfolio: { id: portfolio.id, title: portfolio.title, category: portfolio.category, isPublished: portfolio.isPublished } }); } catch (error) { console.error('Portfolio update error:', error); res.status(500).json({ success: false, message: 'Ошибка при обновлении проекта: ' + error.message }); } }); // Delete portfolio item router.delete('/portfolio/:id', requireAuth, async (req, res) => { try { const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio) { return res.status(404).json({ success: false, message: '포트폴리오를 찾을 수 없습니다' }); } await portfolio.destroy(); res.json({ success: true, message: '포트폴리오가 성공적으로 삭제되었습니다' }); } catch (error) { console.error('Portfolio deletion error:', error); res.status(500).json({ success: false, message: '포트폴리오 삭제 중 오류가 발생했습니다' }); } }); // Toggle portfolio publish status router.patch('/portfolio/:id/toggle-publish', requireAuth, async (req, res) => { try { const portfolio = await Portfolio.findByPk(req.params.id); if (!portfolio) { return res.status(404).json({ success: false, message: '포트폴리오를 찾을 수 없습니다' }); } const newStatus = !portfolio.isPublished; await portfolio.update({ isPublished: newStatus, publishedAt: newStatus && !portfolio.publishedAt ? new Date() : portfolio.publishedAt, status: newStatus ? 'published' : 'draft' }); res.json({ success: true, message: `포트폴리오가 ${newStatus ? '게시' : '비공개'}되었습니다`, isPublished: newStatus }); } catch (error) { console.error('Portfolio toggle publish error:', error); res.status(500).json({ success: false, message: '상태 변경 중 오류가 발생했습니다' }); } }); // Portfolio preview router.post('/portfolio/preview', requireAuth, upload.array('images', 10), async (req, res) => { try { const { title, shortDescription, description, category, technologies, demoUrl, githubUrl, clientName } = req.body; // Parse technologies from JSON string let parsedTechnologies = []; try { parsedTechnologies = JSON.parse(technologies || '[]'); } catch (e) { parsedTechnologies = typeof technologies === 'string' ? [technologies] : []; } // Process uploaded images for preview const previewImages = []; if (req.files && req.files.length > 0) { for (const file of req.files) { // Convert to base64 for preview const base64 = `data:${file.mimetype};base64,${file.buffer.toString('base64')}`; previewImages.push(base64); } } const previewData = { title, shortDescription, description, category, technologies: parsedTechnologies, images: previewImages, projectUrl: demoUrl || null, githubUrl: githubUrl || null, clientName: clientName || null, createdAt: new Date() }; res.json({ success: true, preview: previewData }); } catch (error) { console.error('Portfolio preview error:', error); res.status(500).json({ success: false, message: 'Ошибка при создании предпросмотра: ' + error.message }); } }); // Services management router.get('/services', requireAuth, addStats, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = 20; const skip = (page - 1) * limit; const [services, total] = await Promise.all([ Service.findAll({ order: [['createdAt', 'DESC']], offset: skip, limit: limit }), Service.count() ]); const totalPages = Math.ceil(total / limit); res.render('admin/services/list', { title: 'Services Management - Admin Panel', layout: 'admin/layout', currentPage: 'services', user: req.session.user, services, pagination: { current: page, total: totalPages, hasNext: page < totalPages, hasPrev: page > 1 } }); } catch (error) { console.error('Services list error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading services' }); } }); // Add service router.get('/services/add', requireAuth, (req, res) => { res.render('admin/services/add', { title: 'Add Service - Admin Panel', layout: 'admin/layout', user: req.session.user, serviceTypes: [ 'web-development', 'mobile-app', 'ui-ux-design', 'e-commerce', 'seo', 'maintenance', 'consultation', 'other' ] }); }); // Create service router.post('/services/add', requireAuth, [ body('name').notEmpty().withMessage('서비스명을 입력해주세요'), body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), body('category').notEmpty().withMessage('카테고리를 선택해주세요'), body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: '입력 데이터를 확인해주세요', errors: errors.array() }); } const { name, shortDescription, description, category, basePrice, features, duration, isActive = true, featured = false } = req.body; const service = await Service.create({ name, shortDescription, description, category, basePrice: parseFloat(basePrice), features: features || [], duration: duration ? parseInt(duration) : null, isActive: Boolean(isActive), featured: Boolean(featured) }); res.json({ success: true, message: '서비스가 성공적으로 생성되었습니다', service: { id: service.id, name: service.name, category: service.category } }); } catch (error) { console.error('Service creation error:', error); res.status(500).json({ success: false, message: '서비스 생성 중 오류가 발생했습니다' }); } }); // Edit service router.get('/services/edit/:id', requireAuth, async (req, res) => { try { const service = await Service.findByPk(req.params.id); if (!service) { return res.status(404).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Service not found' }); } const availablePortfolio = await Portfolio.findAll({ where: { isPublished: true }, attributes: ['id', 'title', 'category'] }); res.render('admin/services/edit', { title: 'Edit Service - Admin Panel', layout: 'admin/layout', user: req.session.user, service, availablePortfolio, serviceTypes: [ 'web-development', 'mobile-app', 'ui-ux-design', 'e-commerce', 'seo', 'maintenance', 'consultation', 'other' ] }); } catch (error) { console.error('Service edit error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading service' }); } }); // Update service router.put('/services/:id', requireAuth, [ body('name').notEmpty().withMessage('서비스명을 입력해주세요'), body('shortDescription').notEmpty().withMessage('간단한 설명을 입력해주세요'), body('description').notEmpty().withMessage('자세한 설명을 입력해주세요'), body('category').notEmpty().withMessage('카테고리를 선택해주세요'), body('basePrice').isNumeric().withMessage('기본 가격을 입력해주세요') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: '입력 데이터를 확인해주세요', errors: errors.array() }); } const service = await Service.findByPk(req.params.id); if (!service) { return res.status(404).json({ success: false, message: '서비스를 찾을 수 없습니다' }); } const { name, shortDescription, description, category, basePrice, features, duration, isActive, featured } = req.body; await service.update({ name, shortDescription, description, category, basePrice: parseFloat(basePrice), features: features || [], duration: duration ? parseInt(duration) : null, isActive: Boolean(isActive), featured: Boolean(featured) }); res.json({ success: true, message: '서비스가 성공적으로 업데이트되었습니다', service: { id: service.id, name: service.name, category: service.category } }); } catch (error) { console.error('Service update error:', error); res.status(500).json({ success: false, message: '서비스 업데이트 중 오류가 발생했습니다' }); } }); // Delete service router.delete('/services/:id', requireAuth, async (req, res) => { try { const service = await Service.findByPk(req.params.id); if (!service) { return res.status(404).json({ success: false, message: '서비스를 찾을 수 없습니다' }); } await service.destroy(); res.json({ success: true, message: '서비스가 성공적으로 삭제되었습니다' }); } catch (error) { console.error('Service deletion error:', error); res.status(500).json({ success: false, message: '서비스 삭제 중 오류가 발생했습니다' }); } }); // Toggle service active status router.patch('/services/:id/toggle-active', requireAuth, async (req, res) => { try { const service = await Service.findByPk(req.params.id); if (!service) { return res.status(404).json({ success: false, message: '서비스를 찾을 수 없습니다' }); } const newStatus = !service.isActive; await service.update({ isActive: newStatus }); res.json({ success: true, message: `서비스가 ${newStatus ? '활성화' : '비활성화'}되었습니다`, isActive: newStatus }); } catch (error) { console.error('Service toggle active error:', error); res.status(500).json({ success: false, message: '상태 변경 중 오류가 발생했습니다' }); } }); // Contacts management router.get('/contacts', requireAuth, addStats, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = 20; const skip = (page - 1) * limit; const status = req.query.status; let whereClause = {}; if (status && status !== 'all') { whereClause.status = status; } const [contacts, total] = await Promise.all([ Contact.findAll({ where: whereClause, order: [['createdAt', 'DESC']], offset: skip, limit: limit }), Contact.count({ where: whereClause }) ]); const totalPages = Math.ceil(total / limit); res.render('admin/contacts/list', { title: 'Contacts Management - Admin Panel', layout: 'admin/layout', currentPage: 'contacts', user: req.session.user, contacts, currentStatus: status || 'all', pagination: { current: page, total: totalPages, hasNext: page < totalPages, hasPrev: page > 1 } }); } catch (error) { console.error('Contacts list error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading contacts' }); } }); // View contact details router.get('/contacts/:id', requireAuth, async (req, res) => { try { const contact = await Contact.findByPk(req.params.id); if (!contact) { return res.status(404).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Contact not found' }); } // Mark as read if (!contact.isRead) { contact.isRead = true; await contact.save(); } res.render('admin/contacts/view', { title: 'Contact Details - Admin Panel', layout: 'admin/layout', user: req.session.user, contact }); } catch (error) { console.error('Contact view error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading contact' }); } }); // Settings router.get('/settings', requireAuth, addStats, async (req, res) => { try { const settings = await SiteSettings.findOne() || await SiteSettings.create({}); res.render('admin/settings', { title: 'Site Settings - Admin Panel', layout: 'admin/layout', currentPage: 'settings', user: req.session.user, settings }); } catch (error) { console.error('Settings error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading settings' }); } }); // Media gallery router.get('/media', requireAuth, addStats, (req, res) => { res.render('admin/media', { title: 'Media Gallery - Admin Panel', layout: 'admin/layout', currentPage: 'media', user: req.session.user }); }); // Telegram bot configuration and testing router.get('/telegram', requireAuth, addStats, async (req, res) => { try { const telegramService = require('../services/telegram'); // Get bot info and available chats if token is configured let botInfo = null; let availableChats = []; if (telegramService.botToken) { const result = await telegramService.getBotInfo(); if (result.success) { botInfo = result.bot; availableChats = telegramService.getAvailableChats(); } } res.render('admin/telegram', { title: 'Telegram Bot - Admin Panel', layout: 'admin/layout', currentPage: 'telegram', user: req.session.user, botConfigured: telegramService.isEnabled, botToken: telegramService.botToken || '', chatId: telegramService.chatId || '', botInfo, availableChats }); } catch (error) { console.error('Telegram page error:', error); res.status(500).render('admin/error', { title: 'Error - Admin Panel', layout: 'admin/layout', message: 'Error loading Telegram settings' }); } }); // Update bot token router.post('/telegram/configure', requireAuth, [ body('botToken').notEmpty().withMessage('Bot token is required'), body('chatId').optional().isNumeric().withMessage('Chat ID must be numeric') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Validation failed', errors: errors.array() }); } const { botToken, chatId } = req.body; const telegramService = require('../services/telegram'); // Update bot token const result = await telegramService.updateBotToken(botToken); if (result.success) { // Update chat ID if provided if (chatId) { telegramService.updateChatId(parseInt(chatId)); } // Update environment variables (in production, this should update a config file) process.env.TELEGRAM_BOT_TOKEN = botToken; if (chatId) { process.env.TELEGRAM_CHAT_ID = chatId; } res.json({ success: true, message: 'Telegram bot configured successfully', botInfo: result.bot, availableChats: result.availableChats || [] }); } else { res.status(400).json({ success: false, message: result.error || 'Failed to configure bot' }); } } catch (error) { console.error('Configure Telegram bot error:', error); res.status(500).json({ success: false, message: 'Error configuring Telegram bot' }); } }); // Get bot info and discover chats router.get('/telegram/info', requireAuth, async (req, res) => { try { const telegramService = require('../services/telegram'); const result = await telegramService.testConnection(); if (result.success) { res.json({ success: true, botInfo: result.bot, availableChats: result.availableChats || [], isConfigured: telegramService.isEnabled }); } else { res.status(400).json({ success: false, message: result.error || result.message || 'Failed to get bot info' }); } } catch (error) { console.error('Get Telegram info error:', error); res.status(500).json({ success: false, message: 'Error getting bot information' }); } }); // Get chat information router.get('/telegram/chat/:chatId', requireAuth, async (req, res) => { try { const telegramService = require('../services/telegram'); const result = await telegramService.getChat(req.params.chatId); if (result.success) { res.json({ success: true, chat: result.chat }); } else { res.status(400).json({ success: false, message: result.error || 'Failed to get chat info' }); } } catch (error) { console.error('Get chat info error:', error); res.status(500).json({ success: false, message: 'Error getting chat information' }); } }); // Test connection router.post('/telegram/test', requireAuth, async (req, res) => { try { const telegramService = require('../services/telegram'); const result = await telegramService.testConnection(); if (result.success) { const testMessage = `🤖 Тест Telegram бота\n\n` + `✅ Соединение успешно установлено!\n` + `🤖 Бот: @${result.bot.username} (${result.bot.first_name})\n` + `🆔 ID бота: ${result.bot.id}\n` + `⏰ Время тестирования: ${new Date().toLocaleString('ru-RU')}\n` + `🌐 Сайт: ${process.env.BASE_URL || 'http://localhost:3000'}\n\n` + `Бот готов к отправке уведомлений! 🚀`; const sendResult = await telegramService.sendMessage(testMessage); if (sendResult.success) { res.json({ success: true, message: 'Test message sent successfully!', botInfo: result.bot, availableChats: result.availableChats || [] }); } else { res.status(400).json({ success: false, message: 'Bot connection successful, but failed to send test message: ' + (sendResult.error || sendResult.message) }); } } else { res.status(400).json({ success: false, message: result.message || result.error || 'Failed to connect to Telegram bot' }); } } catch (error) { console.error('Telegram test error:', error); res.status(500).json({ success: false, message: 'Error testing Telegram bot' }); } }); // Send custom message router.post('/telegram/send', requireAuth, [ body('message').notEmpty().withMessage('Message is required'), body('chatIds').optional().isArray().withMessage('Chat IDs must be an array'), body('parseMode').optional().isIn(['HTML', 'Markdown', 'MarkdownV2']).withMessage('Invalid parse mode') ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, message: 'Validation failed', errors: errors.array() }); } const { message, chatIds = [], parseMode = 'HTML', disableWebPagePreview = false, disableNotification = false } = req.body; const telegramService = require('../services/telegram'); let result; if (chatIds.length > 0) { // Send to multiple chats result = await telegramService.sendCustomMessage({ text: message, chatIds: chatIds.map(id => parseInt(id)), parseMode, disableWebPagePreview, disableNotification }); res.json({ success: result.success, message: result.success ? `Message sent to ${result.totalSent} chat(s). ${result.totalFailed} failed.` : 'Failed to send message', results: result.results || [], errors: result.errors || [] }); } else { // Send to default chat result = await telegramService.sendMessage(message, { parse_mode: parseMode, disable_web_page_preview: disableWebPagePreview, disable_notification: disableNotification }); if (result.success) { res.json({ success: true, message: 'Message sent successfully!' }); } else { res.status(400).json({ success: false, message: result.error || result.message || 'Failed to send message' }); } } } catch (error) { console.error('Send Telegram message error:', error); res.status(500).json({ success: false, message: 'Error sending message' }); } }); // Settings page router.get('/settings', requireAuth, addStats, (req, res) => { res.render('admin/settings', { title: 'Site Settings - Admin Panel', layout: 'admin/layout', user: req.session.user }); }); // Banner editor page router.get('/banner-editor', requireAuth, addStats, (req, res) => { res.render('admin/banner-editor', { title: 'Banner Editor - Admin Panel', layout: 'admin/layout', user: req.session.user }); }); module.exports = router;