Реализована работа с статьями - создание, редактирование.
This commit is contained in:
parent
c5cb7c2984
commit
9e6276ace9
@ -0,0 +1,10 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `city` on the `Article` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `cityId` to the `Article` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Article" DROP COLUMN "city",
|
||||||
|
ADD COLUMN "cityId" INTEGER NOT NULL;
|
@ -0,0 +1,11 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `Category` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Article" DROP CONSTRAINT "Article_categoryId_fkey";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "Category";
|
@ -25,9 +25,8 @@ model Article {
|
|||||||
title String
|
title String
|
||||||
excerpt String
|
excerpt String
|
||||||
content String
|
content String
|
||||||
category Category @relation(fields: [categoryId], references: [id])
|
|
||||||
categoryId Int
|
categoryId Int
|
||||||
city String
|
cityId Int
|
||||||
coverImage String
|
coverImage String
|
||||||
readTime Int
|
readTime Int
|
||||||
likes Int @default(0)
|
likes Int @default(0)
|
||||||
@ -38,12 +37,6 @@ model Article {
|
|||||||
gallery GalleryImage[]
|
gallery GalleryImage[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Category {
|
|
||||||
id Int @id
|
|
||||||
name String @unique
|
|
||||||
articles Article[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model GalleryImage {
|
model GalleryImage {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
url String
|
url String
|
||||||
|
@ -9,11 +9,11 @@ export async function getArticle(req: Request, res: Response) : Promise<void> {
|
|||||||
const article = await prisma.article.findUnique({
|
const article = await prisma.article.findUnique({
|
||||||
where: { id: req.params.id },
|
where: { id: req.params.id },
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
|
||||||
author: {
|
author: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
displayName: true,
|
displayName: true,
|
||||||
|
avatarUrl: true,
|
||||||
email: true
|
email: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ export async function getArticle(req: Request, res: Response) : Promise<void> {
|
|||||||
|
|
||||||
export async function createArticle(req: AuthRequest, res: Response) : Promise<void> {
|
export async function createArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { title, excerpt, content, categoryId, city, coverImage, readTime } = req.body;
|
const { title, excerpt, content, categoryId, cityId, coverImage, readTime } = req.body;
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
logger.warn('Unauthorized article creation attempt');
|
logger.warn('Unauthorized article creation attempt');
|
||||||
@ -43,18 +43,14 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = await prisma.category.findUnique({
|
if (!categoryId || categoryId < 1 || categoryId > 8) {
|
||||||
where: { id: categoryId }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!category) {
|
|
||||||
logger.warn(`Invalid category ID: ${categoryId}`);
|
logger.warn(`Invalid category ID: ${categoryId}`);
|
||||||
res.status(400).json({ error: 'Invalid category' });
|
res.status(400).json({ error: 'Invalid category' });
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkPermission(req.user, categoryId, 'create')) {
|
if (!checkPermission(req.user, categoryId, 'create')) {
|
||||||
logger.warn(`Permission denied for user ${req.user.id} to create article in category ${category.name}`);
|
logger.warn(`Permission denied for user ${req.user.id} to create article in category ${categoryId}`);
|
||||||
res.status(403).json({ error: 'Permission denied' });
|
res.status(403).json({ error: 'Permission denied' });
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -65,13 +61,12 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
excerpt,
|
excerpt,
|
||||||
content,
|
content,
|
||||||
categoryId,
|
categoryId,
|
||||||
city,
|
cityId,
|
||||||
coverImage,
|
coverImage,
|
||||||
readTime,
|
readTime,
|
||||||
authorId: req.user.id
|
authorId: req.user.id
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
|
||||||
author: {
|
author: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -92,7 +87,7 @@ export async function createArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
|
|
||||||
export async function updateArticle(req: AuthRequest, res: Response) : Promise<void> {
|
export async function updateArticle(req: AuthRequest, res: Response) : Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { title, excerpt, content, category, city, coverImage, readTime } = req.body;
|
const { title, excerpt, content, categoryId, cityId, coverImage, readTime } = req.body;
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
res.status(401).json({ error: 'Пользователь не вошел в систему' });
|
||||||
@ -100,8 +95,7 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
}
|
}
|
||||||
|
|
||||||
const article = await prisma.article.findUnique({
|
const article = await prisma.article.findUnique({
|
||||||
where: { id: req.params.id },
|
where: { id: req.params.id }
|
||||||
include: { category: true }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
@ -109,7 +103,7 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!checkPermission(req.user, category, 'edit')) {
|
if (!checkPermission(req.user, categoryId, 'edit')) {
|
||||||
res.status(403).json({ error: 'Нет прав на выполнение этой операции' });
|
res.status(403).json({ error: 'Нет прав на выполнение этой операции' });
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -120,13 +114,12 @@ export async function updateArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
title,
|
title,
|
||||||
excerpt,
|
excerpt,
|
||||||
content,
|
content,
|
||||||
categoryId: parseInt(category),
|
categoryId: Number(categoryId),
|
||||||
city,
|
cityId,
|
||||||
coverImage,
|
coverImage,
|
||||||
readTime
|
readTime
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
category: true,
|
|
||||||
author: {
|
author: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -152,8 +145,7 @@ export async function deleteArticle(req: AuthRequest, res: Response) : Promise<v
|
|||||||
}
|
}
|
||||||
|
|
||||||
const article = await prisma.article.findUnique({
|
const article = await prisma.article.findUnique({
|
||||||
where: { id: req.params.id },
|
where: { id: req.params.id }
|
||||||
include: { category: true }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
|
@ -1,19 +1,20 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { prisma } from '../../../lib/prisma';
|
import { prisma } from '../../../lib/prisma';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { CategoryMap } from '../../../types';
|
|
||||||
|
|
||||||
export async function listArticles(req: Request, res: Response) {
|
export async function listArticles(req: Request, res: Response) {
|
||||||
try {
|
try {
|
||||||
const { page = 1, category, city } = req.query;
|
const { page = 1, categoryId, cityId } = req.query;
|
||||||
const perPage = 6;
|
const perPage = 6;
|
||||||
|
|
||||||
const categoryId = CategoryMap[category as string];
|
// Преобразование и проверка параметров
|
||||||
|
const catId = Number(categoryId);
|
||||||
|
const citId = Number(cityId);
|
||||||
|
|
||||||
// Проверка и преобразование параметров в строковые значения
|
// Проверка и преобразование параметров
|
||||||
const where: Prisma.ArticleWhereInput = {
|
const where: Prisma.ArticleWhereInput = {
|
||||||
...(categoryId && { categoryId: categoryId }),
|
...(categoryId && !Number.isNaN(catId) && catId !== 0 && { categoryId: catId }),
|
||||||
...(city && { city: city as string }),
|
...(cityId && !Number.isNaN(citId) && citId !== 0 && { cityId: citId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Рассчитываем пропуск записей для пагинации
|
// Рассчитываем пропуск записей для пагинации
|
||||||
@ -28,7 +29,8 @@ export async function listArticles(req: Request, res: Response) {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
displayName: true,
|
displayName: true,
|
||||||
email: true,
|
avatarUrl: true,
|
||||||
|
email: true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -28,6 +28,7 @@ export async function searchArticles(req: Request, res: Response) {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
displayName: true,
|
displayName: true,
|
||||||
|
avatarUrl: true,
|
||||||
email: true,
|
email: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,16 +1,28 @@
|
|||||||
import { CategoryName, City } from './index';
|
import {CategoryName, CategoryIds} from './index';
|
||||||
|
|
||||||
|
/*
|
||||||
export interface UserPermissions {
|
export interface UserPermissions {
|
||||||
categories: {
|
categories: {
|
||||||
[key in CategoryName]?: {
|
[key in CategoryName]: {
|
||||||
create: boolean;
|
create: boolean;
|
||||||
edit: boolean;
|
edit: boolean;
|
||||||
delete: boolean;
|
delete: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
cities: City[];
|
cities: number[];
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface UserPermissions {
|
||||||
|
categories: {
|
||||||
|
[key: string]: { edit: boolean; create: boolean; delete: boolean };
|
||||||
|
};
|
||||||
|
cities: number[];
|
||||||
|
isAdmin: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PermissionAction = 'edit' | 'create' | 'delete';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -3,9 +3,8 @@ export interface Article {
|
|||||||
title: string;
|
title: string;
|
||||||
excerpt: string;
|
excerpt: string;
|
||||||
content: string;
|
content: string;
|
||||||
category: Category;
|
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
city: City;
|
cityId: number;
|
||||||
author: Author;
|
author: Author;
|
||||||
coverImage: string;
|
coverImage: string;
|
||||||
gallery?: GalleryImage[];
|
gallery?: GalleryImage[];
|
||||||
@ -30,14 +29,17 @@ export interface Author {
|
|||||||
bio: string;
|
bio: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Category {
|
// export interface Category {
|
||||||
id: number;
|
// id: number;
|
||||||
name: CategoryName;
|
// name: CategoryName;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export type CategoryName = 'Film' | 'Theater' | 'Music' | 'Sports' | 'Art' | 'Legends' | 'Anniversaries' | 'Memory';
|
export type CategoryName = 'Film' | 'Theater' | 'Music' | 'Sports' | 'Art' | 'Legends' | 'Anniversaries' | 'Memory';
|
||||||
|
export type CategoryIds = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8';
|
||||||
|
|
||||||
export type City = 'New York' | 'London';
|
export type City = 'New York' | 'London';
|
||||||
|
|
||||||
|
/*
|
||||||
export const CategoryMap: Record<string, number> = {
|
export const CategoryMap: Record<string, number> = {
|
||||||
'Film': 1,
|
'Film': 1,
|
||||||
'Theater': 2,
|
'Theater': 2,
|
||||||
@ -48,3 +50,4 @@ export const CategoryMap: Record<string, number> = {
|
|||||||
'Anniversaries': 7,
|
'Anniversaries': 7,
|
||||||
'Memory': 8,
|
'Memory': 8,
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
@ -1,30 +1,46 @@
|
|||||||
import { Category, City } from '../types';
|
import {PermissionAction, User} from '../types/auth';
|
||||||
import { User } from '../types/auth';
|
|
||||||
|
|
||||||
|
/*
|
||||||
export const checkPermission = (
|
export const checkPermission = (
|
||||||
user: User,
|
user: User,
|
||||||
category: Category,
|
categoryId: string,
|
||||||
action: 'create' | 'edit' | 'delete'
|
action: PermissionAction
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (user.permissions.isAdmin) return true;
|
if (user.permissions.isAdmin) return true;
|
||||||
return !!user.permissions.categories[category.name]?.[action];
|
return !!user.permissions.categories?[categoryId]?.[action];
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const checkPermission = (
|
||||||
|
user: User,
|
||||||
|
categoryId: string,
|
||||||
|
action: PermissionAction
|
||||||
|
): boolean => {
|
||||||
|
// Если пользователь админ — ему разрешено всё
|
||||||
|
if (user.permissions.isAdmin) return true;
|
||||||
|
|
||||||
|
// Проверяем, есть ли такая категория в permissions
|
||||||
|
const categoryPermissions = user.permissions.categories[categoryId];
|
||||||
|
|
||||||
|
// Если категория отсутствует или в ней нет действия — запрет
|
||||||
|
return categoryPermissions ? categoryPermissions[action] : false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkCityAccess = (user: User, city: City): boolean => {
|
export const checkCityAccess = (user: User, cityId: number): boolean => {
|
||||||
if (user.permissions.isAdmin) return true;
|
if (user.permissions.isAdmin) return true;
|
||||||
return user.permissions.cities.includes(city);
|
return user.permissions.cities.includes(cityId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getDefaultPermissions = () => ({
|
export const getDefaultPermissions = () => ({
|
||||||
categories: {
|
categories: {
|
||||||
Film: { create: false, edit: false, delete: false },
|
1: { create: false, edit: false, delete: false },
|
||||||
Theater: { create: false, edit: false, delete: false },
|
2: { create: false, edit: false, delete: false },
|
||||||
Music: { create: false, edit: false, delete: false },
|
3: { create: false, edit: false, delete: false },
|
||||||
Sports: { create: false, edit: false, delete: false },
|
4: { create: false, edit: false, delete: false },
|
||||||
Art: { create: false, edit: false, delete: false },
|
5: { create: false, edit: false, delete: false },
|
||||||
Legends: { create: false, edit: false, delete: false },
|
6: { create: false, edit: false, delete: false },
|
||||||
Anniversaries: { create: false, edit: false, delete: false },
|
7: { create: false, edit: false, delete: false },
|
||||||
Memory: { create: false, edit: false, delete: false }
|
8: { create: false, edit: false, delete: false }
|
||||||
},
|
},
|
||||||
cities: [],
|
cities: [],
|
||||||
isAdmin: false
|
isAdmin: false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user