diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..2a72b2a --- /dev/null +++ b/Caddyfile @@ -0,0 +1,51 @@ +auth.campfiregg.ru { + root * /data/www-auth + encode gzip + try_files {path} /index.html + file_server + + # Настройки безопасности + header { + # Включаем HSTS + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + # Запрещаем встраивание в фреймы + X-Frame-Options "SAMEORIGIN" + # Включаем XSS защиту + X-XSS-Protection "1; mode=block" + # Запрещаем MIME-sniffing + X-Content-Type-Options "nosniff" + # Настройки CSP + Content-Security-Policy "default-src 'self' https://api.campfiregg.ru; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https://api.campfiregg.ru" + } + + # Настройки CORS + @cors_preflight method OPTIONS + handle @cors_preflight { + header Access-Control-Allow-Origin "https://mneie.campfiregg.ru https://staff.campfiregg.ru https://game.campfiregg.ru" + header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization" + header Access-Control-Max-Age "3600" + respond 204 + } + + handle { + header Access-Control-Allow-Origin "https://mneie.campfiregg.ru https://staff.campfiregg.ru https://game.campfiregg.ru" + header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, Authorization" + } +} + +# Остальные настройки для других доменов +:80 { + root * /data/www + try_files {path} /index.html + file_server browse + php_fastcgi unix//run/php/php-fpm.sock +} + +:443 { + root * /data/www + try_files {path} /index.html + file_server browse + php_fastcgi unix//run/php/php-fpm.sock +} \ No newline at end of file diff --git a/campfirecritics_alpha_6_2.code-workspace b/campfirecritics_alpha_6_2.code-workspace new file mode 100644 index 0000000..785bd45 --- /dev/null +++ b/campfirecritics_alpha_6_2.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../campfire-auth" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index b85a870..0000000 Binary files a/public/icon.png and /dev/null differ diff --git a/src/components/admin/AICreateModal.jsx b/src/components/admin/AICreateModal.jsx new file mode 100644 index 0000000..4a8d12e --- /dev/null +++ b/src/components/admin/AICreateModal.jsx @@ -0,0 +1,238 @@ +import React, { useState } from 'react'; +import { FaRobot, FaSpinner, FaMagic } from 'react-icons/fa'; + +const AICreateModal = ({ onClose, onImport }) => { + const [query, setQuery] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (e) => { + e.preventDefault(); + if (!query.trim()) return; + + setLoading(true); + setError(null); + + try { + // Формируем промпт для AI + const prompt = `Создай JSON объект для медиа контента на основе запроса: "${query}". + +Пожалуйста, верни только валидный JSON без дополнительного текста, который содержит следующие поля: + +{ + "title": "Название на русском языке", + "path": "url-slug-на-латинице", + "type": "movie|tv|game|anime", + "overview": "Описание на русском языке", + "release_date": "YYYY-MM-DD", + "progress_type": "hours|watched|completed", + "characteristics": { + "story": "Сюжет", + "acting": "Актерская игра", + "visuals": "Визуал", + "sound": "Звук", + "atmosphere": "Атмосфера", + "pacing": "Темп" + } +} + +Тип должен соответствовать запросу: +- movie - для фильмов +- tv - для сериалов +- game - для игр +- anime - для аниме + +progress_type должен соответствовать типу: +- hours - для игр +- watched - для фильмов/сериалов/аниме +- completed - для сюжетных игр + +Используй веб-поиск для получения актуальной информации.`; + + // Отправляем запрос к GPTunnel API + console.log('Отправляем запрос к GPTunnel с API ключом:', import.meta.env.VITE_GPTUNNEL_API ? '***' + import.meta.env.VITE_GPTUNNEL_API.slice(-4) : 'НЕ УСТАНОВЛЕН'); + const response = await fetch('https://gptunnel.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${import.meta.env.VITE_GPTUNNEL_API}` + }, + body: JSON.stringify({ + model: 'gpt-5-mini', + messages: [ + { + role: 'user', + content: prompt + } + ], + tools: [ + { + type: 'web_search' + } + ], + tool_choice: 'auto' + }) + }); + + if (!response.ok) { + console.error('GPTunnel API ответил с ошибкой:', response.status, response.statusText); + if (response.status === 402) { + throw new Error('Ошибка оплаты GPTunnel: проверьте баланс или подписку'); + } else if (response.status === 401) { + throw new Error('Неверный API ключ GPTunnel'); + } else if (response.status === 429) { + throw new Error('Превышен лимит запросов к GPTunnel'); + } else { + throw new Error(`Ошибка при обращении к CampFireAI: ${response.status} ${response.statusText}`); + } + } + + const data = await response.json(); + + if (!data.choices || !data.choices[0] || !data.choices[0].message) { + throw new Error('Неожиданный ответ от CampFireAI'); + } + + const aiResponse = data.choices[0].message.content; + + // Пытаемся извлечь JSON из ответа + let jsonMatch = aiResponse.match(/\{[\s\S]*\}/); + if (!jsonMatch) { + throw new Error('CampFireAI не вернул валидный JSON'); + } + + let mediaData; + try { + mediaData = JSON.parse(jsonMatch[0]); + } catch (parseError) { + throw new Error('Ошибка парсинга JSON от CampFireAI'); + } + + // Валидируем обязательные поля + if (!mediaData.title || !mediaData.type || !mediaData.overview) { + throw new Error('CampFireAI вернул неполные данные'); + } + + // Формируем данные для импорта + const importData = { + title: mediaData.title, + path: mediaData.path || mediaData.title.toLowerCase().replace(/[^a-z0-9]+/g, '-'), + type: mediaData.type, + overview: mediaData.overview, + release_date: mediaData.release_date || '', + progress_type: mediaData.progress_type || 'completed', + characteristics: mediaData.characteristics || {}, + tmdb_id: null, + tmdb_type: null + }; + + onImport(importData); + onClose(); + } catch (err) { + console.error('Error creating with AI:', err); + setError(err.message || 'Произошла ошибка при создании с помощью CampFireAI'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+ +
+

CampFireAI

+

+ Опишите контент в свободной форме, и ИИ создаст карточку с помощью веб-поиска +

+
+ +
+
+ +