357 lines
10 KiB
TypeScript
357 lines
10 KiB
TypeScript
import { Request, Response } from 'express';
|
||
import axios from "axios";
|
||
import path from "path";
|
||
import { prisma } from '../../../lib/prisma';
|
||
import { AuthRequest } from '../../../middleware/auth';
|
||
import { checkPermission } from '../../../utils/permissions';
|
||
import { logger } from '../../../config/logger';
|
||
import { Article } from "../../../types";
|
||
import { uploadBufferToS3 } from "../../../services/s3Service";
|
||
|
||
const DEFAULT_COVER_IMAGE = '/images/cover-placeholder.webp';
|
||
|
||
export async function getArticle(req: Request, res: Response) : Promise<void> {
|
||
try {
|
||
const article = await prisma.article.findUnique({
|
||
where: { id: req.params.id },
|
||
include: {
|
||
author: {
|
||
select: {
|
||
id: true,
|
||
displayName: true,
|
||
avatarUrl: true,
|
||
email: true
|
||
}
|
||
},
|
||
gallery: {
|
||
orderBy: {order: "asc"}, // Сортировка изображений по порядку
|
||
select: {
|
||
id: true,
|
||
url: true,
|
||
caption: true,
|
||
alt: true,
|
||
width: true,
|
||
height: true,
|
||
size: true,
|
||
format: true,
|
||
order: true, // Добавляем порядок сортировки
|
||
},
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!article) {
|
||
logger.warn(`Статья не найдена: ${req.params.id}`);
|
||
res.status(404).json({ error: 'Статья не найдена' });
|
||
return
|
||
}
|
||
|
||
res.json(article);
|
||
} catch (error) {
|
||
logger.error('Error fetching article:', error);
|
||
res.status(500).json({ error: 'Server error' });
|
||
}
|
||
}
|
||
|
||
export async function createArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||
try {
|
||
const { title, excerpt, content, categoryId, cityId, coverImage, readTime } = req.body;
|
||
|
||
if (!req.user) {
|
||
logger.warn('Пользователь не вошел в систему');
|
||
res.status(401).json({ error: 'Пользователь не аутентифицирован' });
|
||
return
|
||
}
|
||
|
||
if (!categoryId || categoryId < 1 || categoryId > 8) {
|
||
logger.warn(`Недопустимый ID категории: ${categoryId}`);
|
||
res.status(400).json({ error: 'Недопустимая категория' });
|
||
return
|
||
}
|
||
|
||
if (!checkPermission(req.user, categoryId, 'create')) {
|
||
logger.warn(`Не разрешено для пользователя ${req.user.id} создавать статьи в категории ${categoryId}`);
|
||
res.status(403).json({ error: 'В разрешении отказано' });
|
||
return
|
||
}
|
||
|
||
const article = await prisma.article.create({
|
||
data: {
|
||
title,
|
||
excerpt,
|
||
content,
|
||
categoryId,
|
||
cityId,
|
||
coverImage,
|
||
readTime,
|
||
authorId: req.user.id
|
||
},
|
||
include: {
|
||
author: {
|
||
select: {
|
||
id: true,
|
||
displayName: true,
|
||
email: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
logger.info(`Создана статья: ${article.id} пользователем ${req.user.id}`);
|
||
res.status(201).json(article);
|
||
} catch (error) {
|
||
logger.error('Ошибка создания статьи:', error);
|
||
res.status(500).json({ error: 'Серверная ошибка' });
|
||
}
|
||
}
|
||
|
||
export async function updateArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||
try {
|
||
const { title, excerpt, content, categoryId, cityId, coverImage, readTime } = req.body;
|
||
|
||
if (!req.user) {
|
||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||
return
|
||
}
|
||
|
||
const article = await prisma.article.findUnique({
|
||
where: { id: req.params.id }
|
||
});
|
||
|
||
if (!article) {
|
||
res.status(404).json({ error: 'Статья не найдена' });
|
||
return
|
||
}
|
||
|
||
if (!checkPermission(req.user, categoryId, 'edit')) {
|
||
res.status(403).json({ error: 'В разрешении отказано' });
|
||
return
|
||
}
|
||
|
||
const updatedArticle = await prisma.article.update({
|
||
where: { id: req.params.id },
|
||
data: {
|
||
title,
|
||
excerpt,
|
||
content,
|
||
categoryId: Number(categoryId),
|
||
cityId,
|
||
coverImage,
|
||
readTime
|
||
},
|
||
include: {
|
||
author: {
|
||
select: {
|
||
id: true,
|
||
displayName: true,
|
||
email: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
res.json(updatedArticle);
|
||
} catch (error) {
|
||
logger.error('Ошибка редактирования статьи:', error);
|
||
res.status(500).json({ error: 'Серверная ошибка' });
|
||
}
|
||
}
|
||
|
||
export async function activeArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||
try {
|
||
const { isActive } = req.body;
|
||
|
||
if (!req.user) {
|
||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||
return
|
||
}
|
||
|
||
const article = await prisma.article.findUnique({
|
||
where: { id: req.params.id }
|
||
});
|
||
|
||
if (!article) {
|
||
res.status(404).json({ error: 'Статья не найдена' });
|
||
return
|
||
}
|
||
|
||
const updatedArticle = await prisma.article.update({
|
||
where: { id: req.params.id },
|
||
data: {
|
||
isActive: !isActive
|
||
},
|
||
include: {
|
||
author: {
|
||
select: {
|
||
id: true,
|
||
displayName: true,
|
||
email: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
res.json(updatedArticle);
|
||
} catch (error) {
|
||
logger.error('Ошибка активирования статьи:', error);
|
||
res.status(500).json({ error: 'Серверная ошибка' });
|
||
}
|
||
}
|
||
|
||
export async function reactArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||
try {
|
||
const { reaction, likes, dislikes } = req.body;
|
||
|
||
let newLikes:number = Number(likes);
|
||
let newDisLikes:number = Number(dislikes);
|
||
|
||
if (reaction === 'like') newLikes++;
|
||
if (reaction === 'dislike') newDisLikes++;
|
||
|
||
const updatedArticle = await prisma.article.update({
|
||
where: { id: req.params.id },
|
||
data: {
|
||
likes: newLikes,
|
||
dislikes: newDisLikes
|
||
},
|
||
include: {
|
||
author: {
|
||
select: {
|
||
id: true,
|
||
displayName: true,
|
||
email: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
res.json(updatedArticle);
|
||
} catch (error) {
|
||
logger.error('Ошибка установки реакции на статью:', error);
|
||
res.status(500).json({ error: 'Серверная ошибка' });
|
||
}
|
||
}
|
||
|
||
export async function deleteArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||
try {
|
||
if (!req.user) {
|
||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||
return
|
||
}
|
||
|
||
const article = await prisma.article.findUnique({
|
||
where: { id: req.params.id }
|
||
});
|
||
|
||
if (!article) {
|
||
res.status(404).json({ error: 'Статья не найдена' });
|
||
return
|
||
}
|
||
|
||
if (!checkPermission(req.user, article.categoryId.toString(), 'delete')) {
|
||
res.status(403).json({ error: 'В разрешении отказано' });
|
||
return
|
||
}
|
||
|
||
await prisma.article.delete({
|
||
where: { id: req.params.id }
|
||
});
|
||
|
||
res.json({ message: 'Статья успешно удалена' });
|
||
} catch (error) {
|
||
logger.error('Ошибка удаления статьи:', error);
|
||
res.status(500).json({ error: 'Серверная ошибка' });
|
||
}
|
||
}
|
||
|
||
export async function importArticles(req: AuthRequest, res: Response) : Promise<void> {
|
||
const articles: Article[] = req.body;
|
||
|
||
if (!req.user) {
|
||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||
return
|
||
}
|
||
|
||
if (!Array.isArray(articles) || articles.length === 0) {
|
||
res.status(400).json({ message: 'Ожидается непустой массив статей в теле запроса' });
|
||
return
|
||
}
|
||
|
||
let importedCount = 0;
|
||
|
||
try {
|
||
for (const article of articles) {
|
||
try {
|
||
// Шаг 1: Создание статьи
|
||
const newArticle = await prisma.article.create({
|
||
data: {
|
||
title: article.title,
|
||
excerpt: article.excerpt,
|
||
content: article.content,
|
||
categoryId: article.categoryId,
|
||
cityId: article.cityId,
|
||
coverImage: DEFAULT_COVER_IMAGE,
|
||
readTime: article.readTime,
|
||
publishedAt: new Date(article.publishedAt),
|
||
likes: article.likes || 0,
|
||
dislikes: article.dislikes || 0,
|
||
author: {
|
||
connect: { id: article.author.id },
|
||
},
|
||
},
|
||
});
|
||
|
||
// Шаг 2: Обработка coverImage
|
||
if (article.coverImage) {
|
||
const folder = `articles/${newArticle.id}`;
|
||
const imageUrl = article.coverImage;
|
||
|
||
try {
|
||
// Извлечение изображение в виде буфера массива
|
||
const imageResponse = await axios.get(imageUrl, { responseType: 'arraybuffer' });
|
||
|
||
// Создание буфера из данных ответа
|
||
const imageBuffer = Buffer.from(imageResponse.data, 'binary');
|
||
|
||
// Получить тип контента и расширение
|
||
const contentType = imageResponse.headers['content-type'];
|
||
const fileExtension = path.extname(new URL(imageUrl).pathname) ||
|
||
(contentType.includes('image/') ? `.${contentType.split('/')[1]}` : '');
|
||
|
||
// Генерация имени файла
|
||
const filename = `cover-${Date.now()}${fileExtension}`;
|
||
|
||
// Загрузка буфера в S3
|
||
const uploadedUrl = await uploadBufferToS3(
|
||
imageBuffer,
|
||
folder,
|
||
filename,
|
||
contentType,
|
||
'original',
|
||
80
|
||
);
|
||
|
||
} catch (imageError) {
|
||
console.error(`Ошибка загрузки coverImage для статьи ${article.id}:`, imageError);
|
||
}
|
||
}
|
||
|
||
|
||
importedCount++;
|
||
} catch (articleError) {
|
||
console.error(`Ошибка импорта статьи ${article.id}:`, articleError);
|
||
}
|
||
}
|
||
|
||
res.status(200).json({
|
||
message: `Импорт завершен. Успешно импортировано ${importedCount} из ${articles.length} статей.`,
|
||
});
|
||
} catch (err) {
|
||
console.error('Ошибка обработки запроса:', err);
|
||
res.status(500).json({ message: 'Ошибка сервера при импорте' });
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|
||
}
|
||
|