diff --git a/ADMIN_DESIGN_IMPROVEMENTS.md b/ADMIN_DESIGN_IMPROVEMENTS.md deleted file mode 100644 index a988c16..0000000 --- a/ADMIN_DESIGN_IMPROVEMENTS.md +++ /dev/null @@ -1,169 +0,0 @@ -# Улучшения дизайна админ панели - -## Обзор изменений - -Админ панель была полностью переработана с современным дизайном и улучшенной функциональностью. - -## 🎨 Дизайн-система - -### Цветовая палитра -- **Основной**: Градиенты от синего к индиго -- **Sidebar**: Темная тема (gray-900 to gray-800) -- **Акценты**: Цветные индикаторы для разных статусов -- **Состояния**: Зеленый (успех), красный (ошибка), желтый (важное) - -### Типографика -- **Заголовки**: Градиентный текст с clip-path -- **Основной текст**: Улучшенная читаемость -- **Иконки**: Lucide React с консистентными размерами - -## 🚀 Улучшения интерфейса - -### 1. Главная страница (Dashboard) -**Было**: Статичные данные из файла -**Стало**: Динамические данные из API - -#### Статистические карточки -- 5 карточек вместо 4 (добавлены пользователи) -- Градиентные иконки с цветными фонами -- Улучшенная анимация при наведении -- Реальные данные из базы - -#### Список новостей -- Улучшенное отображение метаданных -- Показ просмотров и лайков -- Эмодзи для лучшей визуализации -- Кнопки с hover-эффектами - -#### Быстрые действия -- Карточки с цветными иконками -- Группированные hover-эффекты -- Добавлена ссылка на настройки - -#### Системная информация -- Новая карточка с градиентным фоном -- Статус подключений в реальном времени -- Индикаторы состояния - -### 2. Sidebar (Боковая панель) -**Было**: Светлая тема -**Стало**: Темная тема с градиентом - -#### Улучшения -- Градиентный фон (gray-900 to gray-800) -- Обновленный логотип "CKE Admin" -- Активные состояния с синим фоном -- Улучшенная анимация переходов -- Интеграция S3Status в темном стиле - -### 3. Top Bar (Верхняя панель) -**Было**: Простой текст приветствия -**Стало**: Аватар пользователя с градиентом - -#### Новые элементы -- Круглый аватар с инициалами -- Градиентный фон аватара -- Улучшенная типографика - -### 4. Форма входа -**Было**: Простая белая форма -**Стало**: Современная форма с градиентами - -#### Улучшения -- Градиентный фон страницы -- Круглая иконка с градиентом -- Улучшенные тени и скругления -- Градиентный текст заголовка - -## 🔧 Функциональные улучшения - -### 1. Интеграция с API -- Загрузка реальных данных вместо статичных -- Поддержка параметра `published=all` -- Обработка состояний загрузки -- Улучшенная обработка ошибок - -### 2. S3 Status интеграция -- Адаптация под темную тему -- Размещение в отдельном блоке -- Улучшенные цвета для темного фона - -### 3. Респонсивность -- Улучшенная сетка для статистики (5 колонок) -- Адаптивные карточки -- Мобильная оптимизация - -## 📊 Новые компоненты - -### 1. Расширенная статистика -```typescript -interface DashboardStats { - totalNews: number; - publishedNews: number; - featuredNews: number; - recentNews: number; - totalUsers: number; -} -``` - -### 2. Улучшенные карточки новостей -- Метаданные с эмодзи -- Цветные бейджи статусов -- Hover-эффекты для кнопок - -### 3. Системная информация -- Индикаторы состояния -- Версия системы -- Статус подключений - -## 🎯 Результаты - -### Визуальные улучшения -- ✅ Современный дизайн с градиентами -- ✅ Консистентная цветовая схема -- ✅ Улучшенная типографика -- ✅ Анимации и переходы - -### Функциональные улучшения -- ✅ Реальные данные из API -- ✅ Улучшенная обработка состояний -- ✅ Интеграция S3 статуса -- ✅ Респонсивный дизайн - -### UX улучшения -- ✅ Интуитивная навигация -- ✅ Информативная статистика -- ✅ Быстрые действия -- ✅ Статус системы - -## 🔮 Дальнейшие улучшения - -### Планируемые функции -- [ ] Темная/светлая тема переключатель -- [ ] Уведомления в реальном времени -- [ ] Расширенная аналитика -- [ ] Пользовательские настройки -- [ ] Экспорт данных - -### Оптимизации -- [ ] Кэширование данных -- [ ] Lazy loading компонентов -- [ ] Оптимизация изображений -- [ ] PWA функции - -## 🛠 Технические детали - -### Используемые технологии -- **React 19** с hooks -- **TypeScript** для типизации -- **Tailwind CSS** для стилизации -- **Lucide React** для иконок -- **Framer Motion** для анимаций - -### Архитектурные решения -- Разделение компонентов по функциональности -- Типизированные интерфейсы -- Консистентная обработка ошибок -- Оптимизированные API запросы - -Админ панель теперь имеет современный, профессиональный вид с улучшенной функциональностью и удобством использования. \ No newline at end of file diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md deleted file mode 100644 index 7185e11..0000000 --- a/DATABASE_SETUP.md +++ /dev/null @@ -1,269 +0,0 @@ -# Настройка базы данных и системы управления новостями - -## Обзор системы - -Система управления новостями для ckeproekt.ru включает: - -- **База данных**: PostgreSQL с Prisma ORM -- **API**: REST API и GraphQL endpoints -- **Админ-панель**: Полнофункциональная панель управления -- **Безопасность**: JWT аутентификация и авторизация -- **Функциональность**: CRUD операции, пагинация, поиск, фильтрация - -## Требования - -- Node.js 18+ -- PostgreSQL 12+ -- npm или yarn - -## Установка и настройка - -### 1. Установка зависимостей - -```bash -npm install -``` - -### 2. Настройка базы данных - -1. Создайте PostgreSQL базу данных: -```sql -CREATE DATABASE ckeproject; -``` - -2. Обновите файл `.env` с вашими данными: -```env -DATABASE_URL="postgresql://username:password@localhost:5432/ckeproject?schema=public" -NEXTAUTH_SECRET="your-secret-key-here" -NEXTAUTH_URL="http://localhost:3000" -``` - -### 3. Инициализация Prisma - -```bash -# Генерация Prisma Client -npm run db:generate - -# Применение схемы к базе данных -npm run db:push - -# Заполнение базы данных начальными данными -npm run db:seed -``` - -### 4. Запуск приложения - -```bash -npm run dev -``` - -## Структура базы данных - -### Таблица `users` -- `id` - Уникальный идентификатор -- `email` - Email (уникальный) -- `username` - Имя пользователя (уникальное) -- `password` - Хешированный пароль -- `role` - Роль (USER, ADMIN, EDITOR) -- `name` - Полное имя -- `avatar` - URL аватара -- `createdAt` - Дата создания -- `updatedAt` - Дата обновления - -### Таблица `news` -- `id` - Уникальный идентификатор -- `title` - Заголовок новости -- `slug` - URL-слаг (уникальный) -- `summary` - Краткое описание -- `content` - Полное содержание -- `category` - Категория новости -- `imageUrl` - URL изображения -- `featured` - Рекомендуемая новость -- `published` - Статус публикации -- `publishedAt` - Дата публикации -- `authorId` - ID автора -- `views` - Количество просмотров -- `likes` - Количество лайков -- `tags` - Массив тегов -- `createdAt` - Дата создания -- `updatedAt` - Дата обновления - -### Таблица `categories` -- `id` - Уникальный идентификатор -- `name` - Название категории -- `slug` - URL-слаг (уникальный) -- `description` - Описание -- `color` - Цвет категории -- `createdAt` - Дата создания -- `updatedAt` - Дата обновления - -## API Endpoints - -### REST API - -#### Новости -- `GET /api/news` - Получить список новостей -- `POST /api/news` - Создать новость -- `GET /api/news/[id]` - Получить новость по ID -- `PUT /api/news/[id]` - Обновить новость -- `DELETE /api/news/[id]` - Удалить новость - -#### Категории -- `GET /api/categories` - Получить список категорий -- `POST /api/categories` - Создать категорию -- `PUT /api/categories/[id]` - Обновить категорию -- `DELETE /api/categories/[id]` - Удалить категорию - -#### Аутентификация -- `POST /api/auth/login` - Вход в систему -- `POST /api/auth/register` - Регистрация -- `POST /api/auth/logout` - Выход из системы - -### GraphQL API - -GraphQL endpoint доступен по адресу `/api/graphql` - -#### Примеры запросов - -```graphql -# Получить список новостей -query { - newsList(page: 1, limit: 10, category: "company") { - news { - id - title - summary - publishedAt - author { - name - } - } - total - totalPages - } -} - -# Создать новость -mutation { - createNews(input: { - title: "Новая новость" - slug: "novaya-novost" - summary: "Краткое описание" - content: "Полное содержание" - category: "company" - featured: true - }) { - id - title - slug - } -} -``` - -## Админ-панель - -Админ-панель доступна по адресу `/admin` - -### Пользователи по умолчанию - -После выполнения `npm run db:seed` будут созданы: - -1. **Администратор** - - Email: `admin@ckeproekt.ru` - - Пароль: `admin123` - - Роль: ADMIN - -2. **Редактор** - - Email: `editor@ckeproekt.ru` - - Пароль: `editor123` - - Роль: EDITOR - -### Функциональность админ-панели - -- ✅ Создание, редактирование и удаление новостей -- ✅ Управление категориями -- ✅ Загрузка изображений -- ✅ Визуальный редактор содержимого -- ✅ Система тегов -- ✅ Управление публикацией -- ✅ Поиск и фильтрация -- ✅ Пагинация -- ✅ Статистика - -## Безопасность - -### Аутентификация -- JWT токены для авторизации -- Хеширование паролей с bcrypt -- Защищенные API endpoints - -### Авторизация -- Роли пользователей (USER, ADMIN, EDITOR) -- Проверка прав доступа на уровне API -- Middleware для защиты маршрутов - -### Валидация -- Проверка входных данных -- Санитизация контента -- Защита от XSS и SQL инъекций - -## Развертывание в продакшн - -### 1. Настройка переменных окружения - -```env -DATABASE_URL="postgresql://user:password@host:5432/database" -NEXTAUTH_SECRET="strong-secret-key" -NEXTAUTH_URL="https://your-domain.com" -``` - -### 2. Сборка приложения - -```bash -npm run build -``` - -### 3. Миграция базы данных - -```bash -npm run db:migrate -npm run db:seed -``` - -### 4. Запуск с PM2 - -```bash -npm run pm2:start -``` - -## Мониторинг и обслуживание - -### Prisma Studio -Для просмотра и редактирования данных: -```bash -npm run db:studio -``` - -### Логи -Логи доступны через PM2: -```bash -pm2 logs -``` - -### Резервное копирование -Регулярно создавайте резервные копии базы данных: -```bash -pg_dump ckeproject > backup.sql -``` - -## Миграция с существующей системы - -Скрипт `scripts/init-database.ts` автоматически мигрирует данные из `lib/news-data.ts` в базу данных. - -## Поддержка - -Для получения поддержки или сообщения об ошибках обращайтесь к разработчикам системы. - -## Лицензия - -Система разработана специально для ckeproekt.ru и является собственностью компании. \ No newline at end of file diff --git a/FIX_REPORT.md b/FIX_REPORT.md deleted file mode 100644 index 042b5a6..0000000 --- a/FIX_REPORT.md +++ /dev/null @@ -1,67 +0,0 @@ -# Отчет о решении проблемы "Failed to fetch news" - -## Проблема -Пользователи получали ошибку "Failed to fetch news" при загрузке новостей на главной странице. - -## Причины -1. **Неверная конфигурация базы данных**: В схеме Prisma был указан провайдер `sqlite`, но в `.env` была настроена PostgreSQL база данных -2. **Отсутствие данных**: База данных не была инициализирована с тестовыми данными -3. **Недостаточная обработка ошибок**: Компонент не показывал информативные сообщения об ошибках - -## Решение - -### 1. Исправление конфигурации базы данных -**Файл**: `prisma/schema.prisma` -```diff -datasource db { -- provider = "sqlite" -+ provider = "postgresql" - url = env("DATABASE_URL") -} -``` - -### 2. Обновление клиента Prisma -```bash -npm run db:generate -npm run db:push -``` - -### 3. Инициализация базы данных -```bash -npm run db:seed -``` - -### 4. Улучшение обработки ошибок -**Файл**: `app/components/NewsBlock.tsx` -- Добавлено состояние для отслеживания ошибок -- Улучшена обработка HTTP ошибок -- Добавлено отображение ошибок в интерфейсе с кнопкой обновления - -### 5. Добавление health check -**Файл**: `app/api/health/route.ts` -- Создан эндпоинт для проверки состояния системы -- Проверка подключения к базе данных -- Отображение статистики (количество новостей и пользователей) - -## Результат -✅ **База данных**: Подключена и работает корректно -✅ **API**: Возвращает данные успешно -✅ **Новости**: Загружаются и отображаются на сайте -✅ **Обработка ошибок**: Улучшена для лучшего пользовательского опыта - -## Тестирование -- Проверена работа API: `GET /api/news` возвращает корректные данные -- Проверено состояние системы: `GET /api/health` показывает healthy status -- В базе данных создано 5 новостей и 2 пользователя для тестирования - -## Дополнительные улучшения -- Добавлен компонент для отображения ошибок с возможностью обновления -- Улучшена обработка состояния "нет новостей" -- Добавлена проверка HTTP статусов ответов - -## Доступ к админ-панели -- **URL**: `/admin` -- **Логин**: `admin` -- **Пароль**: `admin123` - -Проблема полностью решена. Новости теперь загружаются корректно, а пользователи получают информативные сообщения в случае ошибок. \ No newline at end of file diff --git a/IMPLEMENTATION_SUMMARY.md b/IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index 56acb83..0000000 --- a/IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,194 +0,0 @@ -# Итоговый отчет: Система управления новостями для ckeproekt.ru - -## ✅ Выполненные задачи - -### 1. Создание модели News в базе данных ✅ - -**Файлы:** -- `prisma/schema.prisma` - Схема базы данных -- `lib/database.ts` - Утилиты для работы с базой данных - -**Реализованная модель News:** -```prisma -model News { - id String @id @default(cuid()) - title String - slug String @unique - summary String - content String @db.Text - category String - imageUrl String? - featured Boolean @default(false) - published Boolean @default(true) - publishedAt DateTime @default(now()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - authorId String? - author User? @relation(fields: [authorId], references: [id]) - views Int @default(0) - likes Int @default(0) - tags String[] -} -``` - -**Дополнительные модели:** -- `User` - Пользователи системы с ролями (USER, ADMIN, EDITOR) -- `Category` - Категории новостей -- Связи между моделями через foreign keys - -### 2. Разработка GraphQL API ✅ - -**Файлы:** -- `lib/graphql/schema.ts` - GraphQL схема -- `lib/graphql/resolvers.ts` - Резолверы для GraphQL - -**Реализованный функционал:** -- Queries: получение новостей, списка новостей, категорий, пользователей -- Mutations: создание, обновление, удаление новостей и категорий -- Аутентификация и авторизация в GraphQL -- Пагинация и фильтрация в GraphQL запросах -- Поиск по содержимому новостей - -### 3. Интеграция в существующую MS ✅ - -**Файлы:** -- `app/api/news/route.ts` - REST API для новостей -- `app/api/news/[id]/route.ts` - API для отдельной новости -- `lib/database.ts` - Сервисы для работы с базой данных -- `scripts/init-database.ts` - Скрипт миграции данных - -**Реализованная интеграция:** -- REST API endpoints для всех операций с новостями -- Миграция существующих данных из статических файлов -- Совместимость с существующими компонентами -- Обратная совместимость с текущим интерфейсом - -### 4. Обеспечение безопасности ✅ - -**Файлы:** -- `lib/auth.ts` - Система аутентификации и авторизации -- Middleware для защиты API endpoints - -**Реализованные меры безопасности:** -- JWT токены для аутентификации -- Хеширование паролей с bcrypt -- Роли пользователей (USER, ADMIN, EDITOR) -- Middleware для проверки прав доступа -- Защита API endpoints от несанкционированного доступа -- Валидация входных данных - -### 5. Создание всех необходимых страниц и компонентов ✅ - -**Обновленные файлы:** -- `app/admin/news/page.tsx` - Админ-панель управления новостями -- `app/admin/news/create/page.tsx` - Создание новостей -- `app/admin/news/[id]/edit/page.tsx` - Редактирование новостей -- `app/news/page.tsx` - Публичная страница новостей -- `app/news/[slug]/page.tsx` - Страница отдельной новости -- `app/components/NewsBlock.tsx` - Блок новостей на главной - -**Функциональность админ-панели:** -- Создание, редактирование и удаление новостей -- Управление статусом публикации -- Система тегов -- Загрузка изображений -- Визуальный редактор -- Поиск и фильтрация -- Пагинация -- Статистика - -## 🔧 Техническая архитектура - -### База данных -- **PostgreSQL** с **Prisma ORM** -- Миграции и схемы версионируются -- Индексы для оптимизации запросов -- Связи между таблицами - -### API -- **REST API** для основных операций -- **GraphQL API** для сложных запросов -- Единообразная обработка ошибок -- Валидация данных - -### Безопасность -- **JWT** аутентификация -- **bcrypt** для хеширования паролей -- **RBAC** (Role-Based Access Control) -- Защита от XSS и SQL инъекций - -### Frontend -- **Next.js 15** с **TypeScript** -- **React Server Components** -- **Tailwind CSS** для стилизации -- Адаптивный дизайн - -## 📊 Функциональность системы - -### Для администраторов: -- ✅ Полное управление новостями -- ✅ Управление пользователями -- ✅ Статистика и аналитика -- ✅ Настройка категорий -- ✅ Модерация контента - -### Для редакторов: -- ✅ Создание и редактирование новостей -- ✅ Управление своими публикациями -- ✅ Работа с черновиками -- ✅ Загрузка медиа-файлов - -### Для посетителей: -- ✅ Просмотр опубликованных новостей -- ✅ Поиск по содержимому -- ✅ Фильтрация по категориям -- ✅ Пагинация результатов -- ✅ Адаптивный дизайн - -## 🚀 Развертывание - -### Файлы конфигурации: -- `package.json` - Обновлен с необходимыми зависимостями -- `.env` - Переменные окружения -- `DATABASE_SETUP.md` - Подробная инструкция по развертыванию - -### Скрипты: -- `npm run db:generate` - Генерация Prisma Client -- `npm run db:push` - Применение схемы к БД -- `npm run db:seed` - Заполнение начальными данными -- `npm run db:studio` - Prisma Studio для управления данными - -### Пользователи по умолчанию: -- **Администратор**: admin@ckeproekt.ru / admin123 -- **Редактор**: editor@ckeproekt.ru / editor123 - -## 📈 Результаты - -### Полностью функционирующая система: -1. ✅ **База данных** настроена и готова к работе -2. ✅ **API** реализован и протестирован -3. ✅ **Админ-панель** полностью функциональна -4. ✅ **Безопасность** обеспечена на всех уровнях -5. ✅ **Интеграция** с существующим сайтом выполнена -6. ✅ **Миграция данных** из статических файлов завершена - -### Преимущества новой системы: -- **Масштабируемость**: Легко добавлять новые функции -- **Производительность**: Оптимизированные запросы к БД -- **Безопасность**: Многоуровневая защита -- **Удобство**: Интуитивная админ-панель -- **SEO**: Оптимизированные URL и метаданные - -## 🔄 Миграция с существующей системы - -Система автоматически мигрирует данные из `lib/news-data.ts` в базу данных при выполнении команды `npm run db:seed`. - -## 📞 Поддержка - -Система полностью интегрирована в ckeproekt.ru и готова к использованию. Все компоненты протестированы и оптимизированы для производственной среды. - ---- - -**Статус проекта**: ✅ ЗАВЕРШЕН -**Дата завершения**: $(date) -**Версия**: 1.0.0 \ No newline at end of file diff --git a/S3_SETUP.md b/S3_SETUP.md deleted file mode 100644 index 0d55c48..0000000 --- a/S3_SETUP.md +++ /dev/null @@ -1,212 +0,0 @@ -# Настройка S3 хранилища - -## Обзор - -В проекте настроено подключение к S3-совместимому хранилищу для загрузки и хранения файлов. Используется сервис TWC Storage. - -## Конфигурация - -### Переменные окружения - -В файле `.env` добавлены следующие переменные: - -```env -# S3 Configuration -S3_ENDPOINT="https://s3.twcstorage.ru" -S3_BUCKET_NAME="617774af-ckeproekt" -S3_ACCESS_KEY_ID="I6XD2OR7YO2ZN6L6Z629" -S3_SECRET_ACCESS_KEY="9xCOoafisG0aB9lJNvdLO1UuK73fBvMcpHMdijrJ" -S3_REGION="ru-1" - -# Swift Configuration (альтернативный доступ) -SWIFT_URL="https://swift.twcstorage.ru" -SWIFT_ACCESS_KEY="wu14330:swift" -SWIFT_SECRET_ACCESS_KEY="Zh6NYPbgp4IYmzKeMAUgwZFi8uLY4VpS6SIYMDge" -``` - -## Использование - -### API для загрузки файлов - -**Эндпоинт:** `/api/upload` - -**Методы:** -- `POST` - загрузка файла -- `DELETE` - удаление файла - -#### Загрузка файла - -```javascript -const formData = new FormData(); -formData.append('file', file); -formData.append('folder', 'images'); // опционально, по умолчанию 'uploads' -formData.append('oldUrl', oldFileUrl); // опционально, для замены существующего файла - -const response = await fetch('/api/upload', { - method: 'POST', - body: formData, -}); - -const result = await response.json(); -// result.data содержит: { key, url, publicUrl } -``` - -#### Удаление файла - -```javascript -const response = await fetch(`/api/upload?url=${encodeURIComponent(fileUrl)}`, { - method: 'DELETE', -}); -``` - -### Хук useFileUpload - -Для упрощения работы с файлами создан хук `useFileUpload`: - -```javascript -import { useFileUpload } from '@/lib/hooks/useFileUpload'; - -function MyComponent() { - const { uploadFile, deleteFile, isUploading, error, clearError } = useFileUpload(); - - const handleUpload = async (file) => { - try { - const result = await uploadFile(file, { - folder: 'images', - maxSize: 5, // MB - allowedTypes: ['image/jpeg', 'image/png'] - }); - console.log('Файл загружен:', result.publicUrl); - } catch (err) { - console.error('Ошибка загрузки:', err); - } - }; - - return ( -
- {isUploading &&

Загрузка...

} - {error &&

{error}

} - handleUpload(e.target.files[0])} /> -
- ); -} -``` - -### Утилиты для работы с S3 - -В файле `lib/s3.ts` доступны следующие функции: - -```javascript -import { - uploadFileToS3, - deleteFileFromS3, - getSignedUrlFromS3, - getPublicUrlFromS3, - extractKeyFromUrl, - isS3Url -} from '@/lib/s3'; - -// Загрузка файла -const result = await uploadFileToS3(buffer, contentType, folder, fileName); - -// Удаление файла -await deleteFileFromS3(key); - -// Получение подписанного URL (для приватных файлов) -const signedUrl = await getSignedUrlFromS3(key, 3600); // 1 час - -// Получение публичного URL -const publicUrl = getPublicUrlFromS3(key); - -// Извлечение ключа из URL -const key = extractKeyFromUrl(url); - -// Проверка, является ли URL ссылкой на S3 -const isS3File = isS3Url(url); -``` - -## Компоненты - -### ImageUpload - -Компонент `ImageUpload` обновлен для работы с S3: - -```javascript -import ImageUpload from '@/app/admin/components/ImageUpload'; - - setImageUrl('')} - maxSize={5} // MB - acceptedTypes={['image/jpeg', 'image/png']} -/> -``` - -### S3Status - -Компонент для отображения статуса подключения к S3: - -```javascript -import S3Status from '@/app/admin/components/S3Status'; - - -``` - -## Миграция существующих изображений - -Для миграции существующих изображений в S3 используйте команду: - -```bash -npm run migrate:images -``` - -Скрипт: -1. Найдет все изображения в папках `public/images` -2. Загрузит их в S3 -3. Создаст файл `image-mapping.json` с соответствием старых и новых URL - -## Ограничения - -- Максимальный размер файла: 10MB -- Поддерживаемые типы файлов: - - Изображения: JPEG, PNG, GIF, WebP, SVG - - Документы: PDF, DOC, DOCX - -## Структура папок в S3 - -``` -bucket/ -├── images/ # Изображения -├── documents/ # Документы -├── uploads/ # Общие загрузки -├── certificates/ # Сертификаты -├── placeholders/ # Плейсхолдеры -└── test/ # Тестовые файлы -``` - -## Мониторинг - -Статус подключения к S3 отображается в админ-панели в левом сайдбаре. Компонент автоматически проверяет подключение при загрузке и позволяет повторить проверку в случае ошибки. - -## Безопасность - -- Все файлы загружаются как публично доступные -- Для приватных файлов используйте подписанные URL -- Валидация типов и размеров файлов происходит на клиенте и сервере -- Старые файлы автоматически удаляются при замене - -## Troubleshooting - -### Ошибка "Файл не найден" -- Проверьте, что файл выбран корректно -- Убедитесь, что размер файла не превышает лимит - -### Ошибка подключения к S3 -- Проверьте переменные окружения -- Убедитесь, что S3 сервис доступен -- Проверьте права доступа к бакету - -### Ошибка "Неподдерживаемый тип файла" -- Проверьте список разрешенных типов файлов -- Убедитесь, что MIME-тип файла корректен \ No newline at end of file diff --git a/S3_TROUBLESHOOTING.md b/S3_TROUBLESHOOTING.md deleted file mode 100644 index 77b00e6..0000000 --- a/S3_TROUBLESHOOTING.md +++ /dev/null @@ -1,79 +0,0 @@ -# Устранение неполадок S3 - -## Возможные причины ошибки S3 - -### 1. Проблемы с переменными окружения -- **Проверка**: Убедитесь, что все переменные S3 настроены в `.env` -- **Команда**: `Get-Content .env | Select-String "S3"` -- **Тест**: `curl http://localhost:3000/api/test-s3` - -### 2. Проблемы с сетевым подключением -- **Симптомы**: Таймаут подключения, ошибки сети -- **Проверка**: Попробуйте получить доступ к `https://s3.twcstorage.ru` напрямую -- **Решение**: Проверьте файрвол, прокси-настройки - -### 3. Проблемы с учетными данными -- **Симптомы**: Ошибки авторизации, 403 Forbidden -- **Проверка**: Убедитесь, что Access Key и Secret Key корректны -- **Решение**: Обновите учетные данные в `.env` - -### 4. Проблемы с бакетом -- **Симптомы**: Ошибки "bucket not found", 404 -- **Проверка**: Убедитесь, что имя бакета `617774af-ckeproekt` корректно -- **Решение**: Проверьте настройки бакета в панели управления - -### 5. Проблемы с CORS -- **Симптомы**: Ошибки в браузере, но работает в API -- **Проверка**: Посмотрите на Network tab в браузере -- **Решение**: Настройте CORS для бакета - -## Диагностика - -### Шаг 1: Проверьте API на сервере -```bash -curl http://localhost:3000/api/test-s3 -``` - -### Шаг 2: Проверьте в браузере -1. Откройте `http://localhost:3000/admin/test-s3` -2. Откройте консоль разработчика (F12) -3. Посмотрите на логи S3Status - -### Шаг 3: Проверьте сетевые запросы -1. Откройте вкладку Network в DevTools -2. Обновите страницу -3. Найдите запрос к `/api/test-s3` -4. Посмотрите на статус и ответ - -## Возможные решения - -### Если S3 работает в API, но не в браузере: -1. Проверьте консоль браузера на ошибки JavaScript -2. Убедитесь, что компонент S3Status монтируется -3. Проверьте, нет ли блокировки запросов браузером - -### Если S3 не работает вообще: -1. Проверьте подключение к интернету -2. Убедитесь, что сервис s3.twcstorage.ru доступен -3. Проверьте учетные данные - -### Если проблема с таймаутом: -1. Увеличьте таймаут в S3Status компоненте -2. Проверьте скорость соединения -3. Попробуйте использовать другой регион S3 - -## Логи для отладки - -Компонент S3Status выводит подробные логи в консоль: -- `S3Status: Проверяю подключение к S3...` -- `S3Status: Результат проверки:` - результат API -- `S3Status: Подключение успешно` - при успехе -- `S3Status: Ошибка подключения:` - при ошибке -- `S3Status: Исключение при проверке:` - при исключении - -## Контакты для поддержки - -Если проблема не решается: -1. Соберите логи из консоли браузера -2. Проверьте результат `curl http://localhost:3000/api/test-s3` -3. Опишите точные симптомы и шаги воспроизведения \ No newline at end of file diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx index bb9ba02..1fb716b 100644 --- a/app/admin/layout.tsx +++ b/app/admin/layout.tsx @@ -18,28 +18,43 @@ export default function AdminLayout({ children }: AdminLayoutProps) { const router = useRouter(); useEffect(() => { - // Проверяем авторизацию - const adminAuth = localStorage.getItem('adminAuth'); - if (adminAuth) { - setIsAuthenticated(true); - } - setIsLoading(false); + const checkAuth = async () => { + try { + const res = await fetch('/api/admin/me', { cache: 'no-store' }); + setIsAuthenticated(res.ok); + } catch (e) { + setIsAuthenticated(false); + } finally { + setIsLoading(false); + } + }; + checkAuth(); }, []); - const handleLogin = (username: string, password: string) => { - // Простая проверка (в реальном проекте должна быть серверная авторизация) - if (username === 'admin' && password === 'admin123') { - localStorage.setItem('adminAuth', JSON.stringify({ username, role: 'admin' })); - setIsAuthenticated(true); - return true; + const handleLogin = async (email: string, password: string) => { + try { + const res = await fetch('/api/admin/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ identifier: email, password }) + }); + if (res.ok) { + setIsAuthenticated(true); + return true; + } + return false; + } catch (e) { + return false; } - return false; }; - const handleLogout = () => { - localStorage.removeItem('adminAuth'); - setIsAuthenticated(false); - router.push('/admin'); + const handleLogout = async () => { + try { + await fetch('/api/admin/logout', { method: 'POST' }); + } finally { + setIsAuthenticated(false); + router.push('/admin'); + } }; const navigation = [ @@ -154,8 +169,8 @@ export default function AdminLayout({ children }: AdminLayoutProps) { } // Компонент формы входа -function LoginForm({ onLogin }: { onLogin: (username: string, password: string) => boolean }) { - const [username, setUsername] = useState(''); +function LoginForm({ onLogin }: { onLogin: (email: string, password: string) => Promise }) { + const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); @@ -163,15 +178,14 @@ function LoginForm({ onLogin }: { onLogin: (username: string, password: string) e.preventDefault(); setError(''); - if (!username || !password) { + if (!email || !password) { setError('Заполните все поля'); return; } - const success = onLogin(username, password); - if (!success) { - setError('Неверный логин или пароль'); - } + onLogin(email, password).then((success) => { + if (!success) setError('Неверный логин или пароль'); + }); }; return ( @@ -189,16 +203,16 @@ function LoginForm({ onLogin }: { onLogin: (username: string, password: string)
-
diff --git a/app/admin/news/[id]/edit/page.tsx b/app/admin/news/[id]/edit/page.tsx index 7bc28ee..ad00b45 100644 --- a/app/admin/news/[id]/edit/page.tsx +++ b/app/admin/news/[id]/edit/page.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import { ArrowLeft, Save, Eye, Upload, X, Trash2 } from 'lucide-react'; -import { NEWS_CATEGORIES, NewsFormData, NewsCategory } from '@/lib/types'; +import { NewsFormData, NewsCategory } from '@/lib/types'; // import { getNewsById } from '@/lib/news-data'; interface EditNewsPageProps { @@ -35,8 +35,24 @@ export default function EditNewsPage({ params }: EditNewsPageProps) { const [errors, setErrors] = useState>({}); const [tagInput, setTagInput] = useState(''); + const [categories, setCategories] = useState<{ id: string; name: string }[]>([ + { id: 'company', name: 'Новости компании' }, + { id: 'promotions', name: 'Акции' }, + { id: 'other', name: 'Другое' } + ]); useEffect(() => { + // подгружаем категории + (async () => { + try { + const res = await fetch('/api/categories', { cache: 'no-store' }); + const data = await res.json(); + if (res.ok && data?.data?.length) { + setCategories(data.data.map((c: any) => ({ id: c.slug, name: c.name }))); + } + } catch {} + })(); + const loadNews = async () => { const resolvedParams = await params; const id = resolvedParams.id; @@ -300,7 +316,7 @@ export default function EditNewsPage({ params }: EditNewsPageProps) { onChange={(e) => setFormData(prev => ({ ...prev, category: e.target.value as NewsCategory }))} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" > - {NEWS_CATEGORIES.map((category) => ( + {categories.map((category) => ( @@ -506,10 +522,8 @@ export default function EditNewsPage({ params }: EditNewsPageProps) {
- cat.id === formData.category)?.color || 'bg-gray-500' - }`}> - {NEWS_CATEGORIES.find(cat => cat.id === formData.category)?.name} + + {categories.find(cat => cat.id === formData.category)?.name || 'Категория'} {formData.featured && ( diff --git a/app/admin/news/create/page.tsx b/app/admin/news/create/page.tsx index 76f8ddf..c857adb 100644 --- a/app/admin/news/create/page.tsx +++ b/app/admin/news/create/page.tsx @@ -1,17 +1,13 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { ArrowLeft, Save, X } from 'lucide-react'; import Link from 'next/link'; import TextEditor from '@/app/admin/components/TextEditor'; import ImageUpload from '@/app/admin/components/ImageUpload'; -const NEWS_CATEGORIES = [ - { id: 'company', name: 'Новости компании' }, - { id: 'promotions', name: 'Акции' }, - { id: 'other', name: 'Другое' } -]; +interface UiCategory { id: string; name: string } export default function CreateNewsPage() { const router = useRouter(); @@ -29,6 +25,25 @@ export default function CreateNewsPage() { tags: [] as string[] }); const [tagInput, setTagInput] = useState(''); + const [categories, setCategories] = useState([ + { id: 'company', name: 'Новости компании' }, + { id: 'promotions', name: 'Акции' }, + { id: 'other', name: 'Другое' } + ]); + + useEffect(() => { + (async () => { + try { + const res = await fetch('/api/categories', { cache: 'no-store' }); + const data = await res.json(); + if (res.ok && data?.data?.length) { + setCategories(data.data.map((c: any) => ({ id: c.slug, name: c.name }))); + // если выбранная категория отсутствует — выставим первую + setFormData(prev => ({ ...prev, category: data.data[0]?.slug || prev.category })); + } + } catch {} + })(); + }, []); const generateSlug = (title: string) => { return title @@ -238,7 +253,7 @@ export default function CreateNewsPage() { className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" required > - {NEWS_CATEGORIES.map((category) => ( + {categories.map((category) => ( diff --git a/app/admin/news/page.tsx b/app/admin/news/page.tsx index 842e768..f67b4dc 100644 --- a/app/admin/news/page.tsx +++ b/app/admin/news/page.tsx @@ -33,13 +33,14 @@ interface NewsCategory { color: string; } -const NEWS_CATEGORIES: NewsCategory[] = [ +const DEFAULT_CATEGORIES: NewsCategory[] = [ { id: 'company', name: 'Новости компании', slug: 'company', color: 'bg-blue-500' }, { id: 'promotions', name: 'Акции', slug: 'promotions', color: 'bg-green-500' }, { id: 'other', name: 'Другое', slug: 'other', color: 'bg-purple-500' } ]; export default function AdminNewsPage() { + const [categories, setCategories] = useState(DEFAULT_CATEGORIES); const [news, setNews] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); @@ -52,6 +53,18 @@ export default function AdminNewsPage() { loadNews(); }, [searchQuery, selectedCategory, selectedStatus, sortBy, sortOrder]); + useEffect(() => { + (async () => { + try { + const res = await fetch('/api/categories', { cache: 'no-store' }); + const data = await res.json(); + if (res.ok && data?.data?.length) { + setCategories(data.data.map((c: any) => ({ id: c.slug, name: c.name, slug: c.slug, color: c.color || 'bg-blue-500' }))); + } + } catch {} + })(); + }, []); + const loadNews = async () => { try { setLoading(true); @@ -102,9 +115,7 @@ export default function AdminNewsPage() { }); }; - const getCategoryInfo = (categoryId: string) => { - return NEWS_CATEGORIES.find(cat => cat.id === categoryId); - }; + const getCategoryInfo = (categoryId: string) => categories.find(cat => cat.id === categoryId); const handleDelete = async (id: string) => { if (!confirm('Вы уверены, что хотите удалить эту новость?')) { @@ -230,7 +241,7 @@ export default function AdminNewsPage() { className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" > - {NEWS_CATEGORIES.map((category) => ( + {categories.map((category) => ( diff --git a/app/admin/settings/page.tsx b/app/admin/settings/page.tsx index 2613163..67dba49 100644 --- a/app/admin/settings/page.tsx +++ b/app/admin/settings/page.tsx @@ -1,14 +1,14 @@ 'use client'; -import React, { useState } from 'react'; -import { Save, Plus, Edit, Trash2, Settings as SettingsIcon, Palette, Globe } from 'lucide-react'; -import { NEWS_CATEGORIES, NewsCategory, NewsCategoryInfo } from '@/lib/types'; +import React, { useEffect, useState } from 'react'; +import { Save, Plus, Edit, Trash2, Settings as SettingsIcon, Palette, Globe, Lock, Mail } from 'lucide-react'; +import { NewsCategory, NewsCategoryInfo } from '@/lib/types'; export default function SettingsPage() { const [activeTab, setActiveTab] = useState('categories'); const [isSubmitting, setIsSubmitting] = useState(false); - const [categories, setCategories] = useState(NEWS_CATEGORIES); + const [categories, setCategories] = useState([]); const [newCategory, setNewCategory] = useState({ name: '', description: '', @@ -38,27 +38,91 @@ export default function SettingsPage() { const tabs = [ { id: 'categories', name: 'Категории', icon: Palette }, { id: 'general', name: 'Общие', icon: SettingsIcon }, + { id: 'security', name: 'Безопасность', icon: Lock }, { id: 'seo', name: 'SEO', icon: Globe } ]; - const handleAddCategory = () => { + const [security, setSecurity] = useState({ + email: 'admin@ckeproekt.ru', + username: 'admin', + currentPassword: '', + newPassword: '', + isSaving: false, + message: '' as string | null, + error: '' as string | null, + }); + + useEffect(() => { + (async () => { + try { + const res = await fetch('/api/admin/me', { cache: 'no-store' }); + if (!res.ok) return; + const data = await res.json(); + if (data?.user) { + setSecurity(prev => ({ ...prev, email: data.user.email, username: data.user.username })); + } + } catch {} + })(); + }, []); + + // загрузка категорий из БД + useEffect(() => { + (async () => { + try { + const res = await fetch('/api/categories', { cache: 'no-store' }); + if (!res.ok) return; + const data = await res.json(); + const mapped: NewsCategoryInfo[] = (data.data || []).map((c: any) => ({ + id: c.slug as NewsCategory, + name: c.name, + description: c.description || '', + color: c.color || 'bg-blue-500' + })); + setCategories(mapped); + } catch {} + })(); + }, []); + + const handleAddCategory = async () => { if (!newCategory.name.trim()) return; - - const categoryId = newCategory.name.toLowerCase().replace(/\s+/g, '-') as NewsCategory; - const category: NewsCategoryInfo = { - id: categoryId, - name: newCategory.name, - description: newCategory.description, - color: newCategory.color - }; - - setCategories([...categories, category]); - setNewCategory({ name: '', description: '', color: 'bg-blue-500' }); + try { + const res = await fetch('/api/categories', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: newCategory.name, description: newCategory.description, color: newCategory.color }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Ошибка создания категории'); + // Преобразуем к локальному типу + const created: NewsCategoryInfo = { + id: data.data.slug as NewsCategory, + name: data.data.name, + description: data.data.description || '', + color: data.data.color || 'bg-blue-500' + }; + setCategories(prev => [...prev, created]); + setNewCategory({ name: '', description: '', color: 'bg-blue-500' }); + } catch (e) { + alert(e instanceof Error ? e.message : 'Ошибка'); + } }; - const handleDeleteCategory = (id: string) => { - if (confirm('Вы уверены, что хотите удалить эту категорию?')) { - setCategories(categories.filter(cat => cat.id !== id)); + const handleDeleteCategory = async (slug: string) => { + if (!confirm('Вы уверены, что хотите удалить эту категорию?')) return; + try { + // нужно найти id категории по slug через /api/categories (у нас в списке нет db id), + // поэтому запрашиваем полный список и ищем совпадение + const resList = await fetch('/api/categories'); + const list = await resList.json(); + const match = (list.data || []).find((c: any) => c.slug === slug); + if (!match) throw new Error('Категория не найдена'); + + const res = await fetch(`/api/categories/${match.id}`, { method: 'DELETE' }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || 'Ошибка удаления'); + setCategories(prev => prev.filter(cat => cat.id !== slug)); + } catch (e) { + alert(e instanceof Error ? e.message : 'Ошибка'); } }; @@ -66,13 +130,10 @@ export default function SettingsPage() { setIsSubmitting(true); try { - // В реальном приложении здесь будет API вызов - console.log('Saving settings:', { categories, generalSettings }); - - // Имитация задержки - await new Promise(resolve => setTimeout(resolve, 1000)); - - alert('Настройки сохранены успешно!'); + // пока сохраняем только generalSettings (к примеру локально) + console.log('Saving settings:', { generalSettings }); + await new Promise(r => setTimeout(r, 500)); + alert('Настройки сохранены'); } catch (error) { console.error('Error saving settings:', error); alert('Ошибка при сохранении настроек'); @@ -131,6 +192,7 @@ export default function SettingsPage() {

Управление категориями

+

Категории синхронизируются с базой данных

{/* Add New Category */}
@@ -172,7 +234,7 @@ export default function SettingsPage() {
- {/* Categories List */} + {/* Categories List */}
{categories.map((category) => (
@@ -184,11 +246,9 @@ export default function SettingsPage() {
- + {/* Редактирование можно добавить при необходимости */}
)} + + {/* Security Tab */} + {activeTab === 'security' && ( +
+
+

Учетные данные администратора

+
+
+ + setSecurity(prev => ({ ...prev, email: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="admin@ckeproekt.ru" + /> +
+
+ + setSecurity(prev => ({ ...prev, username: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="admin" + /> +
+
+ + setSecurity(prev => ({ ...prev, currentPassword: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Введите текущий пароль" + /> +
+
+ + setSecurity(prev => ({ ...prev, newPassword: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Оставьте пустым, чтобы не менять" + /> +
+
+
+ +
+ {security.message &&

{security.message}

} + {security.error &&

{security.error}

} +
+
+ )}
diff --git a/app/api/admin/credentials/route.ts b/app/api/admin/credentials/route.ts new file mode 100644 index 0000000..a4f00cf --- /dev/null +++ b/app/api/admin/credentials/route.ts @@ -0,0 +1,54 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuthContext, hashPassword, verifyPassword } from '@/lib/auth'; +import prisma from '@/lib/database'; + +export async function PUT(request: NextRequest) { + const context = await getAuthContext(request); + if (!context.user || context.user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + try { + const { email, username, currentPassword, newPassword } = await request.json(); + + if (!currentPassword) { + return NextResponse.json({ error: 'Текущий пароль обязателен' }, { status: 400 }); + } + + const user = await prisma.user.findUnique({ where: { id: context.user.id } }); + if (!user) { + return NextResponse.json({ error: 'Пользователь не найден' }, { status: 404 }); + } + + // Проверка текущего пароля + const isValid = await verifyPassword(currentPassword, user.password); + if (!isValid) { + return NextResponse.json({ error: 'Неверный текущий пароль' }, { status: 400 }); + } + + const data: any = {}; + if (email && email !== user.email) data.email = email; + if (username && username !== user.username) data.username = username; + if (newPassword && newPassword.length >= 6) { + data.password = await hashPassword(newPassword); + } + + if (Object.keys(data).length === 0) { + return NextResponse.json({ success: true }); + } + + const updated = await prisma.user.update({ + where: { id: user.id }, + data, + select: { id: true, email: true, username: true, role: true, name: true, avatar: true } + }); + + return NextResponse.json({ user: updated }); + } catch (error: any) { + if (error.code === 'P2002') { + return NextResponse.json({ error: 'Email или логин уже заняты' }, { status: 409 }); + } + return NextResponse.json({ error: 'Внутренняя ошибка сервера' }, { status: 500 }); + } +} + diff --git a/app/api/admin/login/route.ts b/app/api/admin/login/route.ts new file mode 100644 index 0000000..9fa52b5 --- /dev/null +++ b/app/api/admin/login/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from 'next/server'; +import prisma from '@/lib/database'; +import { verifyPassword, generateToken } from '@/lib/auth'; + +export async function POST(request: NextRequest) { + try { + const { identifier, password } = await request.json(); + + if (!identifier || !password) { + return NextResponse.json({ error: 'Логин/Email и пароль обязательны' }, { status: 400 }); + } + + const user = await prisma.user.findFirst({ + where: { + OR: [ + { email: identifier }, + { username: identifier } + ] + } + }); + + if (!user) { + return NextResponse.json({ error: 'Неверные учетные данные' }, { status: 401 }); + } + + if (user.role !== 'ADMIN') { + return NextResponse.json({ error: 'Доступ запрещен' }, { status: 403 }); + } + + const isValid = await verifyPassword(password, user.password); + if (!isValid) { + return NextResponse.json({ error: 'Неверные учетные данные' }, { status: 401 }); + } + + const token = generateToken(user.id); + const { password: _pwd, ...safeUser } = user as any; + + const response = NextResponse.json({ user: safeUser }); + response.cookies.set('auth-token', token, { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + path: '/', + maxAge: 60 * 60 * 24 * 7, // 7 дней + }); + return response; + } catch (error) { + return NextResponse.json({ error: 'Внутренняя ошибка сервера' }, { status: 500 }); + } +} + diff --git a/app/api/admin/logout/route.ts b/app/api/admin/logout/route.ts new file mode 100644 index 0000000..b850b51 --- /dev/null +++ b/app/api/admin/logout/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from 'next/server'; + +export async function POST() { + const response = NextResponse.json({ success: true }); + response.cookies.set('auth-token', '', { + httpOnly: true, + sameSite: 'lax', + secure: process.env.NODE_ENV === 'production', + path: '/', + maxAge: 0, + }); + return response; +} + diff --git a/app/api/admin/me/route.ts b/app/api/admin/me/route.ts new file mode 100644 index 0000000..8cd77e4 --- /dev/null +++ b/app/api/admin/me/route.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getAuthContext } from '@/lib/auth'; + +export async function GET(request: NextRequest) { + const context = await getAuthContext(request); + if (!context.user || context.user.role !== 'ADMIN') { + return NextResponse.json({ user: null }, { status: 401 }); + } + return NextResponse.json({ user: context.user }); +} + diff --git a/app/api/categories/[id]/route.ts b/app/api/categories/[id]/route.ts new file mode 100644 index 0000000..263a9b8 --- /dev/null +++ b/app/api/categories/[id]/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server'; +import prisma from '@/lib/database'; + +export async function PUT( + request: Request, + { params }: { params: { id: string } } +) { + try { + const body = await request.json(); + const { name, description, color, slug } = body; + const id = params.id; + + const updated = await prisma.category.update({ + where: { id }, + data: { + ...(name ? { name } : {}), + ...(description !== undefined ? { description } : {}), + ...(color ? { color } : {}), + ...(slug ? { slug } : {}), + }, + select: { id: true, name: true, slug: true, description: true, color: true } + }); + + return NextResponse.json({ success: true, data: updated }); + } catch (error: any) { + if (error.code === 'P2025') { + return NextResponse.json({ success: false, error: 'Категория не найдена' }, { status: 404 }); + } + if (error.code === 'P2002') { + return NextResponse.json({ success: false, error: 'Имя или слаг уже используются' }, { status: 409 }); + } + return NextResponse.json({ success: false, error: 'Не удалось обновить категорию' }, { status: 500 }); + } +} + +export async function DELETE( + _request: Request, + { params }: { params: { id: string } } +) { + try { + const id = params.id; + await prisma.category.delete({ where: { id } }); + return NextResponse.json({ success: true }); + } catch (error: any) { + if (error.code === 'P2025') { + return NextResponse.json({ success: false, error: 'Категория не найдена' }, { status: 404 }); + } + return NextResponse.json({ success: false, error: 'Не удалось удалить категорию' }, { status: 500 }); + } +} + diff --git a/app/api/categories/route.ts b/app/api/categories/route.ts new file mode 100644 index 0000000..8105661 --- /dev/null +++ b/app/api/categories/route.ts @@ -0,0 +1,56 @@ +import { NextResponse } from 'next/server'; +import prisma from '@/lib/database'; + +export async function GET() { + try { + const categories = await prisma.category.findMany({ + orderBy: { name: 'asc' }, + select: { id: true, name: true, slug: true, description: true, color: true } + }); + return NextResponse.json({ success: true, data: categories }); + } catch { + return NextResponse.json({ success: false, error: 'Не удалось получить категории' }, { status: 500 }); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const name: string = body.name; + const description: string | undefined = body.description; + const color: string | undefined = body.color; + let slug: string | undefined = body.slug; + + if (!name || !name.trim()) { + return NextResponse.json({ success: false, error: 'Название обязательно' }, { status: 400 }); + } + + if (!slug) { + slug = name + .toLowerCase() + .replace(/[^a-z0-9а-яё\s-]/g, '') + .replace(/[\s_]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + } + + const created = await prisma.category.create({ + data: { + name: name.trim(), + slug, + description, + color: color || 'bg-blue-500' + }, + select: { id: true, name: true, slug: true, description: true, color: true } + }); + + return NextResponse.json({ success: true, data: created }, { status: 201 }); + } catch (e: unknown) { + const code = typeof e === 'object' && e !== null && 'code' in e ? (e as { code?: string }).code : undefined; + if (code === 'P2002') { + return NextResponse.json({ success: false, error: 'Категория с таким именем или слагом уже существует' }, { status: 409 }); + } + return NextResponse.json({ success: false, error: 'Не удалось создать категорию' }, { status: 500 }); + } +} + diff --git a/app/news/NewsPageComponent.tsx b/app/news/NewsPageComponent.tsx index 4812792..fd316d1 100644 --- a/app/news/NewsPageComponent.tsx +++ b/app/news/NewsPageComponent.tsx @@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react'; import { useSearchParams, useRouter, usePathname } from 'next/navigation'; import Link from 'next/link'; import Image from 'next/image'; -import { NEWS_CATEGORIES, NewsItem } from '@/lib/types'; +import { NewsItem } from '@/lib/types'; import { Search, Eye, ArrowRight } from 'lucide-react'; import Header from '@/app/components/Header'; import Footer from '@/app/components/Footer'; @@ -60,8 +60,18 @@ export default function NewsPageComponent() { const [news, setNews] = useState([]); const [loading, setLoading] = useState(true); const [totalNews, setTotalNews] = useState(0); + const [categories, setCategories] = useState<{ id: string; name: string; color: string }[]>([]); useEffect(() => { + (async () => { + try { + const res = await fetch('/api/categories', { cache: 'no-store' }); + const data = await res.json(); + if (res.ok && data?.data?.length) { + setCategories(data.data.map((c: any) => ({ id: c.slug, name: c.name, color: c.color || 'bg-gray-500' }))); + } + } catch {} + })(); const loadNews = async () => { try { setLoading(true); @@ -126,9 +136,7 @@ export default function NewsPageComponent() { }); }; - const getCategoryInfo = (categoryId: string) => { - return NEWS_CATEGORIES.find(cat => cat.id === categoryId); - }; + const getCategoryInfo = (categoryId: string) => categories.find(cat => cat.id === categoryId); const handleCategoryChange = (category: string) => { setSelectedCategory(category); @@ -237,7 +245,7 @@ export default function NewsPageComponent() { > Все - {NEWS_CATEGORIES.map((category) => ( + {categories.map((category) => (