Files
sst_site/routes/admin.js
2025-10-26 14:44:10 +09:00

1611 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
};
// 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, async (req, res) => {
res.redirect('/admin/banners');
});
// Portfolio management
router.get('/portfolio', requireAuth, 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, 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, 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, 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, (req, res) => {
res.render('admin/media', {
title: 'Media Gallery - Admin Panel',
layout: 'admin/layout',
user: req.session.user
});
});
// Telegram bot configuration and testing
router.get('/telegram', requireAuth, 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',
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'
});
}
});
module.exports = router;