Доработан импорт статей, сделано разделение пользователей на собственно пользователей и авторов.

This commit is contained in:
anibilag 2025-05-21 23:00:36 +03:00
parent 35791b4308
commit 35b470c498
14 changed files with 481 additions and 58 deletions

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "Author" (
"id" TEXT NOT NULL,
"displayName" TEXT NOT NULL,
"bio" TEXT,
"avatarUrl" TEXT,
"order" INTEGER NOT NULL DEFAULT 0,
"okUrl" TEXT,
"vkUrl" TEXT,
"websiteUrl" TEXT,
"email" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Author_pkey" PRIMARY KEY ("id")
);

View File

@ -0,0 +1,8 @@
/*
Warnings:
- Made the column `avatarUrl` on table `Author` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "Author" ALTER COLUMN "avatarUrl" SET NOT NULL;

View File

@ -0,0 +1,5 @@
-- DropForeignKey
ALTER TABLE "Article" DROP CONSTRAINT "Article_authorId_fkey";
-- AddForeignKey
ALTER TABLE "Article" ADD CONSTRAINT "Article_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "Author"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,14 @@
/*
Warnings:
- A unique constraint covering the columns `[userId]` on the table `Author` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "Author" ADD COLUMN "userId" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "Author_userId_key" ON "Author"("userId");
-- AddForeignKey
ALTER TABLE "Author" ADD CONSTRAINT "Author_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Author" ADD COLUMN "isActive" BOOLEAN NOT NULL DEFAULT true;

View File

@ -8,18 +8,44 @@ datasource db {
}
model User {
id String @id @default(uuid())
email String @unique
id String @id @default(uuid())
email String @unique
password String
displayName String
avatarUrl String
bio String?
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isAdmin Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
permissions Json
order Int @default(0)
Author Author?
}
enum AuthorRole {
WRITER
PHOTOGRAPHER
EDITOR
TRANSLATOR
}
model Author {
id String @id @default(uuid())
displayName String
bio String?
avatarUrl String
order Int @default(0)
okUrl String?
vkUrl String?
websiteUrl String?
email String?
userId String? @unique
user User? @relation(fields: [userId], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
articles Article[]
order Int @default(0)
isActive Boolean @default(true)
roles AuthorRole[] @default([])
}
model Article {
@ -35,7 +61,7 @@ model Article {
likes Int @default(0)
dislikes Int @default(0)
publishedAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id])
author Author @relation(fields: [authorId], references: [id])
authorId String
gallery GalleryImage[]
isActive Boolean @default(false)

View File

@ -76,6 +76,25 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
return
}
const userId = req.user?.id; // ← ID текущего пользователя
// 1. Найти автора, связанного с пользователем
let author = await prisma.author.findFirst({
where: { userId },
});
// 2. Если не найден — найти автора с order = 1
if (!author) {
author = await prisma.author.findFirst({
where: { order: 1 },
});
}
if (!author) {
res.status(400).json({ error: 'Автор по умолчанию не найден' });
return
}
const article = await prisma.article.create({
data: {
title,
@ -85,7 +104,7 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
cityId,
coverImage,
readTime,
authorId: req.user.id
authorId: author.id
},
include: {
author: {
@ -296,10 +315,19 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
let importedCount = 0;
// Получение списка всех авторов для находдения автора по имени
const allAuthors = await prisma.author.findMany({
select: { id: true, displayName: true },
});
const authorMap = new Map(allAuthors.map(a => [a.displayName, a.id]));
try {
for (const article of articles) {
try {
// Шаг 1: Создание статьи
const authorId = authorMap.get(article.authorName) || article.author.id;
const newArticle = await prisma.article.create({
data: {
importId: article.importId,
@ -314,7 +342,7 @@ export async function importArticles(req: AuthRequest, res: Response) : Promise<
likes: article.likes || 0,
dislikes: article.dislikes || 0,
author: {
connect: { id: article.author.id },
connect: { id: authorId },
},
},
});

View File

@ -1,14 +1,275 @@
import { Response } from 'express';
import { AuthRequest } from '../../../middleware/auth';
import { userService } from '../../../services/userService';
import { authorService } from '../../../services/authorService';
import { logger } from "../../../config/logger";
import { prisma } from "../../../lib/prisma";
// Список авторов - без permissions и без чистых админов
export async function getAuthors(req: AuthRequest, res: Response): Promise<void> {
try {
const authors = await userService.getAuthors();
const authors = await authorService.getAuthors();
res.json(authors);
} catch {
res.status(500).json({ error: 'Server error' });
}
}
export async function createAuthor(req: AuthRequest, res: Response): Promise<void> {
try {
if (!req.user?.permissions.isAdmin) {
logger.warn(`Не администратор ${req.user?.id} пытается создать автора`);
res.status(403).json({ error: 'Требуются права администратора' });
return
}
const { email, displayName, bio, avatarUrl, order } = req.body;
const lastAuthor = await prisma.author.findFirst({
orderBy: {
order: 'desc',
},
});
const nextOrder = lastAuthor ? lastAuthor.order + 1 : 1;
const author = await prisma.author.create({
data: {
email,
displayName,
bio,
avatarUrl,
order: nextOrder
},
select: {
id: true,
email: true,
displayName: true,
bio: true,
avatarUrl: true,
order: true,
}
});
logger.info(`Успешное создание автора: ${author.id}`);
res.status(201).json(author);
} catch (error) {
logger.error('Ошибка создания автора:', error);
res.status(500).json({ error: 'Ошибка создания автора' });
}
}
export async function updateAuthor(req: AuthRequest, res: Response): Promise<void> {
try {
if (!req.user?.permissions.isAdmin) {
logger.warn(`Не администратор ${req.user?.id} пытается обновить автора`);
res.status(403).json({ error: 'Требуются права администратора' });
return
}
const { id } = req.params;
const { email, displayName, bio, avatarUrl, order, okUrl, vkUrl, websiteUrl } = req.body;
// Подготовка данных для изменения
const updateData: any = {
email,
displayName,
bio,
avatarUrl,
order,
okUrl,
vkUrl,
websiteUrl,
};
// Обновление данных автора
const author = await prisma.author.update({
where: { id },
data: updateData,
select: {
id: true,
email: true,
displayName: true,
bio: true,
avatarUrl: true,
order: true,
okUrl: true,
vkUrl: true,
websiteUrl: true,
}
});
logger.info(`Успешное обновление автора: ${author.id}`);
res.json(author);
} catch (error) {
logger.error('Ошибка обновления автора:', error);
res.status(500).json({ error: 'Ошибка обновления автора' });
}
}
export async function deleteAuthor(req: AuthRequest, res: Response): Promise<void> {
try {
if (!req.user?.permissions.isAdmin) {
logger.warn(`Не администратор ${req.user?.id} пытается удалить пользователя`);
res.status(403).json({ error: 'Требуются права администратора' });
return
}
const { id } = req.params;
const existingUser = await prisma.author.findUnique({
where: { id }
});
if (!existingUser) {
res.status(404).json({ error: 'Автор не найден' });
return
}
await prisma.author.delete({
where: { id }
});
logger.info(`Успешное удаление автора: ${id}`);
res.json({ message: 'Успешное удаление автора' });
} catch (error) {
logger.error('Ошибка удаления автора:', error);
res.status(500).json({ error: 'Ошибка удаления автора' });
}
}
export async function linkAuthorToUser(req: AuthRequest, res: Response): Promise<void> {
try {
if (!req.user?.permissions.isAdmin) {
logger.warn(`Не администратор ${req.user?.id} пытается обновить автора`);
res.status(403).json({ error: 'Требуются права администратора' });
return
}
const { id } = req.params;
const { userId } = req.body;
const author = await prisma.author.update({
where: { id },
data: { userId },
select: {
id: true,
userId: true,
}
});
logger.info(`Успешное связывание автора: ${author.id} с пользователем ${author.userId}`);
res.json(author);
} catch (error) {
logger.error('Ошибка связывания автора:', error);
res.status(500).json({ error: 'Ошибка связывания автора' });
}
}
export async function unlinkAuthorFromUser(req: AuthRequest, res: Response): Promise<void> {
try {
if (!req.user?.permissions.isAdmin) {
logger.warn(`Не администратор ${req.user?.id} пытается обновить автора`);
res.status(403).json({ error: 'Требуются права администратора' });
return
}
const { id } = req.params;
const author = await prisma.author.update({
where: { id },
data: { userId: null },
select: {
id: true,
userId: true,
}
});
logger.info(`Успешное отвязывание автора: ${author.id} от пользователя ${author.userId}`);
res.json(author);
} catch (error) {
logger.error('Ошибка отвязывания автора:', error);
res.status(500).json({ error: 'Ошибка отвязывания автора' });
}
}
export async function toggleActiveAuthor(req: AuthRequest, res: Response) : Promise<void> {
try {
const { isActive } = req.body;
if (!req.user) {
res.status(401).json({ error: 'Пользователь не вошел в систему' });
return
}
const author = await prisma.author.findUnique({
where: { id: req.params.id }
});
if (!author) {
res.status(404).json({ error: 'Автор не найден' });
return
}
const updatedAuthor = await prisma.author.update({
where: { id: req.params.id },
data: { isActive: isActive },
});
res.json(updatedAuthor);
} catch (error) {
logger.error('Ошибка активирования автора:', error);
res.status(500).json({ error: 'Серверная ошибка' });
}
}
export async function reorderAuthor(req: AuthRequest, res: Response) : Promise<void> {
try {
if (!req.user) {
res.status(401).json({ error: 'Пользователь не вошел в систему' });
return
}
const { id } = req.params;
const { direction } = req.body; // 'up' или 'down'
if (!['up', 'down'].includes(direction)) {
res.status(400).json({ error: 'Неправильное направление' });
return
}
const current = await prisma.author.findUnique({ where: { id } });
if (!current) {
res.status(404).json({ error: 'Автор не найден' });
return
}
const neighbor = await prisma.author.findFirst({
where: {
order: direction === 'up' ? { lt: current.order } : { gt: current.order },
},
orderBy: {
order: direction === 'up' ? 'desc' : 'asc',
},
});
if (!neighbor) {
res.status(200).json({ message: 'Уже достигли границы', author: current });
return
}
await prisma.$transaction([
prisma.author.update({
where: { id: current.id },
data: { order: neighbor.order },
}),
prisma.author.update({
where: { id: neighbor.id },
data: { order: current.order },
}),
]);
const updated = await prisma.author.findUnique({ where: { id } });
res.json(updated);
} catch (error) {
console.error('Ошибка при изменении порядка авторов:', error);
res.status(500).json({ error: 'Ошибка при изменении порядка авторов' }); }
}

View File

@ -1,9 +1,26 @@
import express from 'express';
import { getAuthors } from './controllers/authors';
import { auth } from '../../middleware/auth';
import {
getAuthors,
createAuthor,
updateAuthor,
deleteAuthor,
linkAuthorToUser,
unlinkAuthorFromUser,
toggleActiveAuthor,
reorderAuthor
} from './controllers/authors';
const router = express.Router();
router.get('/', getAuthors);
router.post('/', auth, createAuthor);
router.put('/:id', auth, updateAuthor);
router.delete('/:id', auth, deleteAuthor);
router.put('/:id/link-user', auth, linkAuthorToUser);
router.put('/:id/unlink-user', auth, unlinkAuthorFromUser);
router.put('/:id/toggle-active', auth, toggleActiveAuthor);
router.put('/:id/reorder', auth, reorderAuthor);
export default router;

View File

@ -1,12 +1,12 @@
import { Response } from 'express';
import { userService } from '../../../services/userService';
import { authorService } from '../../../services/authorService';
import { AuthRequest } from "../../../middleware/auth";
// Изменить количество лайков
export async function updateLikes(req: AuthRequest, res: Response): Promise<void> {
try {
const authors = await userService.getAuthors();
const authors = await authorService.getAuthors();
res.json(authors);
} catch {
res.status(500).json({ error: 'Серверная ошибка' });

View File

@ -0,0 +1,72 @@
import { PrismaClient } from '@prisma/client';
import { Author } from '../types/auth';
const prisma = new PrismaClient();
export const authorService = {
getAuthors: async (): Promise<Author[]> => {
try {
// 1. Получаем авторов
const authors = await prisma.author.findMany({
select: {
id: true,
displayName: true,
avatarUrl: true,
bio: true,
order: true,
okUrl: true,
vkUrl: true,
websiteUrl: true,
email: true,
userId: true,
isActive: true,
_count: {
select: {articles: true}, // Подсчёт количества статей автора
},
user: {
select: {
displayName: true,
},
}
},
where: {order: {not: 0}},
orderBy: {
order: 'asc',
},
});
// 2. Считаем сумму лайков по авторам
const likesPerAuthor = await prisma.article.groupBy({
by: ['authorId'],
_sum: {
likes: true,
},
});
// 3. Строим map для быстрого доступа
const likesMap = new Map(
likesPerAuthor.map((item) => [item.authorId, item._sum.likes ?? 0])
);
return authors.map(author => ({
id: author.id,
displayName: author.displayName,
avatarUrl: author.avatarUrl,
bio: author.bio,
order: author.order,
okUrl: author.okUrl,
vkUrl: author.vkUrl,
websiteUrl: author.websiteUrl,
email: author.email,
articlesCount: author._count.articles, // Количество статей
userId: author.userId,
userDisplayName: author.user?.displayName || null,
isActive: author.isActive,
totalLikes: likesMap.get(author.id) ?? 0,
}));
} catch (error) {
console.error('Ошибка получения авторов:', error);
throw new Error('Ошибка получения авторов');
}
}
}

View File

@ -14,6 +14,7 @@ export const userService = {
avatarUrl: string;
bio: string | null;
permissions: JsonValue;
Author: { id: string } | null;
}> = await prisma.user.findMany({
select: {
id: true,
@ -22,6 +23,9 @@ export const userService = {
avatarUrl: true,
bio: true,
permissions: true,
Author: {
select: { id: true } // достаточно только id для проверки связи
}
},
});
@ -32,7 +36,8 @@ export const userService = {
return {
...user,
permissions: permissions as UserPermissions,
} as User;
isLinkedToAuthor: !!user.Author,
} as User & { isLinkedToAuthor: boolean };
} else {
throw new Error(`Invalid permissions format for user ${user.id}`);
}
@ -43,39 +48,6 @@ export const userService = {
}
},
getAuthors: async (): Promise<Author[]> => {
try {
const authors = await prisma.user.findMany({
select: {
id: true,
email: true,
displayName: true,
avatarUrl: true,
bio: true,
_count: {
select: { articles: true }, // Подсчёт количества статей автора
},
},
where: { order: { not: 0 } },
orderBy: {
order: 'asc',
},
});
return authors.map(author => ({
id: author.id,
email: author.email,
displayName: author.displayName,
avatarUrl: author.avatarUrl,
bio: author.bio,
articlesCount: author._count.articles, // Количество статей
}));
} catch (error) {
console.error('Ошибка получения авторов:', error);
throw new Error('Ошибка получения авторов');
}
},
updateUserPermissions: async (
userId: string,
permissions: User['permissions']

View File

@ -19,9 +19,14 @@ export interface User {
export interface Author {
id: string;
email: string;
email: string | null;
displayName: string;
avatarUrl: string;
articlesCount: number;
bio: string | null;
order: number;
okUrl: string | null;
vkUrl: string | null;
websiteUrl: string | null;
articlesCount: number;
userId?: string | null;
}

View File

@ -1,3 +1,5 @@
import { Author } from "./auth";
export interface Article {
id: string;
importId: number;
@ -7,6 +9,7 @@ export interface Article {
categoryId: number;
cityId: number;
author: Author;
authorName: string;
coverImage: string;
images?: string[];
imageSubs: string[];
@ -26,9 +29,3 @@ export interface GalleryImage {
alt: string;
}
export interface Author {
id: string;
name: string;
avatar: string;
bio: string;
}