1654 lines
44 KiB
JavaScript
1654 lines
44 KiB
JavaScript
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',
|
||
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',
|
||
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',
|
||
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',
|
||
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 = `🤖 <b>Тест Telegram бота</b>\n\n` +
|
||
`✅ Соединение успешно установлено!\n` +
|
||
`🤖 <b>Бот:</b> @${result.bot.username} (${result.bot.first_name})\n` +
|
||
`🆔 <b>ID бота:</b> ${result.bot.id}\n` +
|
||
`⏰ <b>Время тестирования:</b> ${new Date().toLocaleString('ru-RU')}\n` +
|
||
`🌐 <b>Сайт:</b> ${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; |