Add complete CKE Project implementation with news management system

This commit is contained in:
albivkt
2025-07-13 01:34:11 +03:00
parent c9317555ca
commit a84810c6b9
32 changed files with 8901 additions and 811 deletions

455
lib/graphql/resolvers.ts Normal file
View File

@ -0,0 +1,455 @@
import { PrismaClient } from '@prisma/client';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
const prisma = new PrismaClient();
export const resolvers = {
Query: {
// News queries
news: async (_: any, { id }: { id: string }) => {
return await prisma.news.findUnique({
where: { id },
include: { author: true }
});
},
newsList: async (_: any, args: {
page?: number;
limit?: number;
category?: string;
search?: string;
featured?: boolean;
published?: boolean;
sortBy?: string;
sortOrder?: string;
}) => {
const {
page = 1,
limit = 10,
category,
search,
featured,
published = true,
sortBy = 'publishedAt',
sortOrder = 'desc'
} = args;
const skip = (page - 1) * limit;
const where: any = {};
if (published !== undefined) {
where.published = published;
}
if (category) {
where.category = category;
}
if (featured !== undefined) {
where.featured = featured;
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ summary: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
];
}
const orderBy: any = {};
orderBy[sortBy] = sortOrder;
const [news, total] = await Promise.all([
prisma.news.findMany({
where,
skip,
take: limit,
orderBy,
include: { author: true }
}),
prisma.news.count({ where })
]);
const totalPages = Math.ceil(total / limit);
return {
news,
total,
page,
limit,
totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1
};
},
newsCount: async (_: any, args: {
category?: string;
search?: string;
published?: boolean;
}) => {
const { category, search, published = true } = args;
const where: any = {};
if (published !== undefined) {
where.published = published;
}
if (category) {
where.category = category;
}
if (search) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },
{ summary: { contains: search, mode: 'insensitive' } },
{ content: { contains: search, mode: 'insensitive' } }
];
}
return await prisma.news.count({ where });
},
// Category queries
categories: async () => {
return await prisma.category.findMany({
orderBy: { name: 'asc' }
});
},
category: async (_: any, { id }: { id: string }) => {
return await prisma.category.findUnique({
where: { id }
});
},
// User queries
user: async (_: any, { id }: { id: string }) => {
return await prisma.user.findUnique({
where: { id },
include: { news: true }
});
},
users: async () => {
return await prisma.user.findMany({
include: { news: true }
});
},
me: async (_: any, __: any, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
return await prisma.user.findUnique({
where: { id: context.user.id },
include: { news: true }
});
}
},
Mutation: {
// News mutations
createNews: async (_: any, { input }: { input: any }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
const news = await prisma.news.create({
data: {
...input,
authorId: context.user.id,
publishedAt: input.publishedAt ? new Date(input.publishedAt) : new Date()
},
include: { author: true }
});
return news;
},
updateNews: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
const existingNews = await prisma.news.findUnique({
where: { id }
});
if (!existingNews) {
throw new Error('News not found');
}
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const updateData: any = { ...input };
if (input.publishedAt) {
updateData.publishedAt = new Date(input.publishedAt);
}
const news = await prisma.news.update({
where: { id },
data: updateData,
include: { author: true }
});
return news;
},
deleteNews: async (_: any, { id }: { id: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
const existingNews = await prisma.news.findUnique({
where: { id }
});
if (!existingNews) {
throw new Error('News not found');
}
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
await prisma.news.delete({
where: { id }
});
return true;
},
toggleNewsPublished: async (_: any, { id }: { id: string }, context: any) => {
if (!context.user) {
throw new Error('Not authenticated');
}
const existingNews = await prisma.news.findUnique({
where: { id }
});
if (!existingNews) {
throw new Error('News not found');
}
if (existingNews.authorId !== context.user.id && context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const news = await prisma.news.update({
where: { id },
data: { published: !existingNews.published },
include: { author: true }
});
return news;
},
toggleNewsFeatured: async (_: any, { id }: { id: string }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const existingNews = await prisma.news.findUnique({
where: { id }
});
if (!existingNews) {
throw new Error('News not found');
}
const news = await prisma.news.update({
where: { id },
data: { featured: !existingNews.featured },
include: { author: true }
});
return news;
},
incrementNewsViews: async (_: any, { id }: { id: string }) => {
const news = await prisma.news.update({
where: { id },
data: { views: { increment: 1 } },
include: { author: true }
});
return news;
},
likeNews: async (_: any, { id }: { id: string }) => {
const news = await prisma.news.update({
where: { id },
data: { likes: { increment: 1 } },
include: { author: true }
});
return news;
},
// Category mutations
createCategory: async (_: any, { input }: { input: any }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const category = await prisma.category.create({
data: input
});
return category;
},
updateCategory: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const category = await prisma.category.update({
where: { id },
data: input
});
return category;
},
deleteCategory: async (_: any, { id }: { id: string }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
await prisma.category.delete({
where: { id }
});
return true;
},
// User mutations
createUser: async (_: any, { input }: { input: any }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
const hashedPassword = await bcrypt.hash(input.password, 12);
const user = await prisma.user.create({
data: {
...input,
password: hashedPassword
},
include: { news: true }
});
return user;
},
updateUser: async (_: any, { id, input }: { id: string; input: any }, context: any) => {
if (!context.user || (context.user.id !== id && context.user.role !== 'ADMIN')) {
throw new Error('Not authorized');
}
const updateData: any = { ...input };
if (input.password) {
updateData.password = await bcrypt.hash(input.password, 12);
}
const user = await prisma.user.update({
where: { id },
data: updateData,
include: { news: true }
});
return user;
},
deleteUser: async (_: any, { id }: { id: string }, context: any) => {
if (!context.user || context.user.role !== 'ADMIN') {
throw new Error('Not authorized');
}
await prisma.user.delete({
where: { id }
});
return true;
},
// Auth mutations
login: async (_: any, { email, password }: { email: string; password: string }) => {
const user = await prisma.user.findUnique({
where: { email }
});
if (!user) {
throw new Error('Invalid credentials');
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
throw new Error('Invalid credentials');
}
const token = jwt.sign(
{ userId: user.id },
process.env.NEXTAUTH_SECRET || 'secret',
{ expiresIn: '7d' }
);
return {
token,
user
};
},
register: async (_: any, { input }: { input: any }) => {
const existingUser = await prisma.user.findFirst({
where: {
OR: [
{ email: input.email },
{ username: input.username }
]
}
});
if (existingUser) {
throw new Error('User already exists');
}
const hashedPassword = await bcrypt.hash(input.password, 12);
const user = await prisma.user.create({
data: {
...input,
password: hashedPassword
}
});
const token = jwt.sign(
{ userId: user.id },
process.env.NEXTAUTH_SECRET || 'secret',
{ expiresIn: '7d' }
);
return {
token,
user
};
},
logout: async () => {
// В GraphQL logout обычно обрабатывается на клиенте
// путем удаления токена из localStorage/cookies
return true;
}
}
};

182
lib/graphql/schema.ts Normal file
View File

@ -0,0 +1,182 @@
import { gql } from 'graphql-tag';
export const typeDefs = gql`
type News {
id: ID!
title: String!
slug: String!
summary: String!
content: String!
category: String!
imageUrl: String
featured: Boolean!
published: Boolean!
publishedAt: String!
createdAt: String!
updatedAt: String!
authorId: String
author: User
views: Int!
likes: Int!
tags: [String!]!
}
type User {
id: ID!
email: String!
username: String!
role: UserRole!
name: String
avatar: String
createdAt: String!
updatedAt: String!
news: [News!]!
}
type Category {
id: ID!
name: String!
slug: String!
description: String
color: String!
createdAt: String!
updatedAt: String!
}
enum UserRole {
USER
ADMIN
EDITOR
}
type Query {
# News queries
news(id: ID!): News
newsList(
page: Int = 1
limit: Int = 10
category: String
search: String
featured: Boolean
published: Boolean = true
sortBy: String = "publishedAt"
sortOrder: String = "desc"
): NewsListResponse!
newsCount(category: String, search: String, published: Boolean = true): Int!
# Category queries
categories: [Category!]!
category(id: ID!): Category
# User queries
user(id: ID!): User
users: [User!]!
me: User
}
type Mutation {
# News mutations
createNews(input: CreateNewsInput!): News!
updateNews(id: ID!, input: UpdateNewsInput!): News!
deleteNews(id: ID!): Boolean!
toggleNewsPublished(id: ID!): News!
toggleNewsFeatured(id: ID!): News!
incrementNewsViews(id: ID!): News!
likeNews(id: ID!): News!
# Category mutations
createCategory(input: CreateCategoryInput!): Category!
updateCategory(id: ID!, input: UpdateCategoryInput!): Category!
deleteCategory(id: ID!): Boolean!
# User mutations
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
# Auth mutations
login(email: String!, password: String!): AuthResponse!
register(input: RegisterInput!): AuthResponse!
logout: Boolean!
}
type NewsListResponse {
news: [News!]!
total: Int!
page: Int!
limit: Int!
totalPages: Int!
hasNextPage: Boolean!
hasPrevPage: Boolean!
}
type AuthResponse {
token: String!
user: User!
}
input CreateNewsInput {
title: String!
slug: String!
summary: String!
content: String!
category: String!
imageUrl: String
featured: Boolean = false
published: Boolean = true
publishedAt: String
tags: [String!] = []
}
input UpdateNewsInput {
title: String
slug: String
summary: String
content: String
category: String
imageUrl: String
featured: Boolean
published: Boolean
publishedAt: String
tags: [String!]
}
input CreateCategoryInput {
name: String!
slug: String!
description: String
color: String = "#3B82F6"
}
input UpdateCategoryInput {
name: String
slug: String
description: String
color: String
}
input CreateUserInput {
email: String!
username: String!
password: String!
role: UserRole = USER
name: String
avatar: String
}
input UpdateUserInput {
email: String
username: String
password: String
role: UserRole
name: String
avatar: String
}
input RegisterInput {
email: String!
username: String!
password: String!
name: String
}
`;