Update: перенастройка сайта что бы открывать 1 порт на сайт
This commit is contained in:
29
DEPLOY-SERVER.md
Normal file
29
DEPLOY-SERVER.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Развёртывание на сервере (с прокси)
|
||||
|
||||
Чтобы сайт и Strapi работали без 404, на порту 85 должен слушать **прокси**, а не frontend.
|
||||
|
||||
## Что должно быть в стеке
|
||||
|
||||
- **iieasy_proxy** — единственный контейнер с портом **85:80** (nginx, раздаёт / → frontend, /api, /admin и т.д. → strapi).
|
||||
- **iieasy_backend** (Strapi) — **без** проброса портов наружу.
|
||||
- **iieasy_frontend** — **без** проброса портов наружу.
|
||||
|
||||
Если в Portainer/Docker видишь только frontend (85) и strapi (1340) и нет proxy — запросы к `/api` идут во frontend и дают 404.
|
||||
|
||||
## Как исправить на сервере
|
||||
|
||||
1. Убедись, что в каталоге проекта есть:
|
||||
- `docker-compose.yml` (в нём описан сервис **proxy** и у strapi/frontend нет `ports`).
|
||||
- Папка `nginx/conf.d/` с файлом `default.conf`.
|
||||
|
||||
2. Останови и удали текущий стек (в Portainer: Stack → iieasy → Remove. Или в терминале в каталоге с compose: `docker-compose down`).
|
||||
|
||||
3. Запусти заново из каталога, где лежит этот `docker-compose.yml`:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
Или в Portainer: загрузи/вставь актуальный `docker-compose.yml` и нажми Deploy.
|
||||
|
||||
4. Проверь контейнеры: должен быть **iieasy_proxy** с портом 85, **iieasy_backend** и **iieasy_frontend** без опубликованных портов.
|
||||
|
||||
После этого открывай сайт по `http://хост:85` — запросы к `/api` и `/admin` пойдут через прокси в Strapi, 404 пропадёт.
|
||||
@@ -1,6 +1,9 @@
|
||||
# Этап 1: Сборка
|
||||
FROM node:20-alpine AS build
|
||||
WORKDIR /app
|
||||
# Пустой VITE_STRAPI_URL при сборке за прокси — запросы идут на тот же origin (порт 85)
|
||||
ARG VITE_STRAPI_URL=
|
||||
ENV VITE_STRAPI_URL=$VITE_STRAPI_URL
|
||||
COPY package.json package-lock.json* ./
|
||||
RUN npm install
|
||||
COPY . .
|
||||
|
||||
10
README.md
10
README.md
@@ -12,3 +12,13 @@ This contains everything you need to run your app locally.
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
|
||||
## Docker (один открытый порт)
|
||||
|
||||
Запуск через `docker-compose up -d` из папки с `docker-compose.yml`:
|
||||
|
||||
- **Снаружи открыт только порт 85.** Сайт и API доступны по `http://хост:85` (главная, `/api/*`, `/admin`, `/uploads`).
|
||||
- Порты Strapi и фронта наружу не пробрасываются; доступ к ним только через Nginx-прокси.
|
||||
- Ollama снаружи не открыт; к нему обращается только Strapi по внутренней сети (`OLLAMA_URL` в `.env` Strapi).
|
||||
|
||||
Для деплоя без прокси (например, на другой машине с прямым доступом к Strapi) соберите фронт с `VITE_STRAPI_URL=http://...` и при необходимости откройте порты вручную.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { SECTION_IDS } from '../constants';
|
||||
import { SparklesIcon } from './icons'; // Using an icon for principles
|
||||
import { SparklesIcon } from './icons';
|
||||
import { fetchUploadFiles, STRAPI_URL } from '../strapiService';
|
||||
|
||||
const principlesData = [
|
||||
{
|
||||
@@ -57,7 +58,35 @@ const PrincipleItem: React.FC<{ title: string; children: React.ReactNode; index:
|
||||
);
|
||||
|
||||
|
||||
const ABOUT_IMAGE_ALT: [string, string][] = [
|
||||
['Команда iiEasy за работой в современном офисе в Уфе, обсуждает проект на фоне доски с диаграммами и кодом. Атмосфера сфокусированной совместной работы.', 'Команда iiEasy в Уфе: от идеи к реализации.'],
|
||||
['Крупный план экрана с кодом или дашбордом предиктивной аналитики. На заднем плане — сфокусированный взгляд разработчика, отражающийся в мониторе.', 'Глубокая техническая работа — основа наших решений.'],
|
||||
['Групповой снимок команды iiEasy. Разнообразные лица, уверенные и дружелюбные улыбки. Неформальная, но профессиональная обстановка.', 'Наша команда — наш главный актив.'],
|
||||
['Панорамный вид на Уфу с акцентом на современные здания (например, Конгресс-холл Торатау), символизирующий связь компании с городом и ее нацеленность на будущее развитие региона.', 'Нацелены на будущее развитие нашего региона.'],
|
||||
];
|
||||
|
||||
const PLACEHOLDER_IMAGES = [
|
||||
'https://picsum.photos/seed/iieasy-team-work/1200/800',
|
||||
'https://picsum.photos/seed/iieasy-code/1200/800',
|
||||
'https://picsum.photos/seed/iieasy-team-group/1200/800',
|
||||
'https://picsum.photos/seed/iieasy-ufa-view/1200/800',
|
||||
];
|
||||
|
||||
const AboutUsSection: React.FC = () => {
|
||||
const [aboutImages, setAboutImages] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const files = await fetchUploadFiles();
|
||||
if (cancelled) return;
|
||||
const sorted = [...files].sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||
const urls = sorted.slice(0, 4).map((f) => (f.url.startsWith('http') ? f.url : `${STRAPI_URL}${f.url}`));
|
||||
setAboutImages(urls);
|
||||
})();
|
||||
return () => { cancelled = true; };
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
@@ -68,16 +97,14 @@ const AboutUsSection: React.FC = () => {
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
threshold: 0.1,
|
||||
}
|
||||
{ threshold: 0.1 }
|
||||
);
|
||||
|
||||
const elements = document.querySelectorAll('.scroll-animate');
|
||||
elements.forEach((el) => observer.observe(el));
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
}, [aboutImages]);
|
||||
|
||||
const getImageUrl = (index: number) => aboutImages[index] ?? PLACEHOLDER_IMAGES[index];
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -100,8 +127,8 @@ const AboutUsSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
||||
<img src="https://picsum.photos/seed/iieasy-team-work/1200/800" alt="Команда iiEasy за работой в современном офисе в Уфе, обсуждает проект на фоне доски с диаграммами и кодом. Атмосфера сфокусированной совместной работы." className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Команда iiEasy в Уфе: от идеи к реализации.</figcaption>
|
||||
<img src={getImageUrl(0)} alt={ABOUT_IMAGE_ALT[0][0]} className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[0][1]}</figcaption>
|
||||
</figure>
|
||||
|
||||
{/* Mission */}
|
||||
@@ -130,8 +157,8 @@ const AboutUsSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
||||
<img src="https://picsum.photos/seed/iieasy-code/1200/800" alt="Крупный план экрана с кодом или дашбордом предиктивной аналитики. На заднем плане — сфокусированный взгляд разработчика, отражающийся в мониторе." className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Глубокая техническая работа — основа наших решений.</figcaption>
|
||||
<img src={getImageUrl(1)} alt={ABOUT_IMAGE_ALT[1][0]} className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[1][1]}</figcaption>
|
||||
</figure>
|
||||
|
||||
{/* Team */}
|
||||
@@ -143,8 +170,8 @@ const AboutUsSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
||||
<img src="https://picsum.photos/seed/iieasy-team-group/1200/800" alt="Групповой снимок команды iiEasy. Разнообразные лица, уверенные и дружелюбные улыбки. Неформальная, но профессиональная обстановка." className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Наша команда — наш главный актив.</figcaption>
|
||||
<img src={getImageUrl(2)} alt={ABOUT_IMAGE_ALT[2][0]} className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[2][1]}</figcaption>
|
||||
</figure>
|
||||
|
||||
{/* Principles */}
|
||||
@@ -160,8 +187,8 @@ const AboutUsSection: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<figure className="mt-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
||||
<img src="https://picsum.photos/seed/iieasy-ufa-view/1200/800" alt="Панорамный вид на Уфу с акцентом на современные здания (например, Конгресс-холл Торатау), символизирующий связь компании с городом и ее нацеленность на будущее развитие региона." className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">Нацелены на будущее развитие нашего региона.</figcaption>
|
||||
<img src={getImageUrl(3)} alt={ABOUT_IMAGE_ALT[3][0]} className="rounded-lg shadow-xl aspect-video object-cover"/>
|
||||
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[3][1]}</figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { CurrentView } from '../types';
|
||||
import { NAVIGABLE_VIEWS } from '../constants';
|
||||
import { ArrowUpIcon } from './icons';
|
||||
import { STRAPI_URL } from '../strapiService';
|
||||
|
||||
const PLACEHOLDERS = [
|
||||
"Создайте мне сайт для моего бизнеса",
|
||||
@@ -81,32 +82,35 @@ const ChatInput: React.FC<ChatInputProps> = ({ setCurrentView, isSticky = false,
|
||||
Your entire response must be only the JSON object, with no other text, explanation, or markdown formatting.`;
|
||||
|
||||
try {
|
||||
const response = await fetch('https://ai.iieasy.ru/v1/chat/completions', {
|
||||
// Запрос через прокси Strapi (/api/ollama/chat) — один origin с фронтом, нет CORS и работает из любой сети
|
||||
const response = await fetch(`${STRAPI_URL}/api/ollama/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: 'local-model',
|
||||
model: 'gemma3n:e4b',
|
||||
messages: [
|
||||
{ role: 'system', content: systemInstruction },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
temperature: 0.1,
|
||||
stream: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ошибка сети: ${response.status} ${response.statusText}. Убедитесь, что сервер LM Studio запущен.`);
|
||||
throw new Error(`Ошибка сети: ${response.status} ${response.statusText}. Убедитесь, что бэкенд и Ollama доступны.`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(`LM Studio вернула ошибку: ${data.error.message}`);
|
||||
throw new Error(`Сервер ИИ вернул ошибку: ${typeof data.error === 'string' ? data.error : data.error.message || 'неизвестная ошибка'}`);
|
||||
}
|
||||
|
||||
const responseJsonString = data.choices?.[0]?.message?.content;
|
||||
// Ответ Ollama в /api/chat (без стриминга):
|
||||
// { message: { role: 'assistant', content: '...' }, ... }
|
||||
const responseJsonString = data.message?.content;
|
||||
if (!responseJsonString) {
|
||||
throw new Error("LM Studio вернула пустой ответ.");
|
||||
throw new Error("Сервер ИИ вернул пустой ответ.");
|
||||
}
|
||||
|
||||
// More robust JSON extraction
|
||||
@@ -127,8 +131,8 @@ const ChatInput: React.FC<ChatInputProps> = ({ setCurrentView, isSticky = false,
|
||||
setError("К сожалению, я не смог понять ваш запрос. Попробуйте переформулировать.");
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error("Ошибка при обращении к LM Studio:", err);
|
||||
setError(err.message || "Произошла ошибка. Убедитесь, что сервер LM Studio запущен и модель загружена. Пожалуйста, попробуйте еще раз.");
|
||||
console.error("Ошибка при обращении к серверу ИИ (Ollama):", err);
|
||||
setError(err.message || "Произошла ошибка. Убедитесь, что бэкенд и сервер Ollama доступны. Пожалуйста, попробуйте еще раз.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@
|
||||
sections: [
|
||||
{
|
||||
heading: "1. Общие положения",
|
||||
content: "Настоящие Правила использования (далее – «Правила») регулируют отношения между ООО \"ИЗИ ГРУПП\" (далее – «Сервис») и Пользователем сети Интернет (далее – «Пользователь») возникающие при использовании сайта iieasy.example.com (далее – «Сайт»). Пользователь обязан полностью ознакомиться с настоящими Правилами до момента начала использования Сайта. Использование Сайта Пользователем означает полное и безоговорочное принятие Пользователем настоящих Правил в соответствии со ст. 438 Гражданского кодекса Российской Федерации."
|
||||
content: "Настоящие Правила использования (далее – «Правила») регулируют отношения между ООО \"ИЗИ ГРУПП\" (далее – «Сервис») и Пользователем сети Интернет (далее – «Пользователь») возникающие при использовании сайта iieasy.ru (далее – «Сайт»). Пользователь обязан полностью ознакомиться с настоящими Правилами до момента начала использования Сайта. Использование Сайта Пользователем означает полное и безоговорочное принятие Пользователем настоящих Правил в соответствии со ст. 438 Гражданского кодекса Российской Федерации."
|
||||
},
|
||||
{
|
||||
heading: "2. Интеллектуальная собственность",
|
||||
@@ -611,7 +611,7 @@
|
||||
sections: [
|
||||
{
|
||||
heading: "1. Введение",
|
||||
content: "Настоящая Политика конфиденциальности персональных данных (далее – «Политика») действует в отношении всей информации, которую ООО \"ИЗИ ГРУПП\" (далее – «Компания») может получить о Пользователе во время использования сайта iieasy.example.com (далее – «Сайт»). Использование Сайта означает безоговорочное согласие Пользователя с настоящей Политикой и указанными в ней условиями обработки его персональной информации; в случае несогласия с этими условиями Пользователь должен воздержаться от использования Сайта."
|
||||
content: "Настоящая Политика конфиденциальности персональных данных (далее – «Политика») действует в отношении всей информации, которую ООО \"ИЗИ ГРУПП\" (далее – «Компания») может получить о Пользователе во время использования сайта iieasy.ru (далее – «Сайт»). Использование Сайта означает безоговорочное согласие Пользователя с настоящей Политикой и указанными в ней условиями обработки его персональной информации; в случае несогласия с этими условиями Пользователь должен воздержаться от использования Сайта."
|
||||
},
|
||||
{
|
||||
heading: "2. Какие данные мы собираем",
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
# Снаружи открыт только порт 85 (прокси). Сайт и API по http://хост:85
|
||||
# Strapi и фронт доступны только внутри Docker-сети через прокси.
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# БЭКЕНД: Strapi (находится в подпапке iiEasy)
|
||||
# ПРОКСИ: единственная точка входа (порт 85)
|
||||
proxy:
|
||||
container_name: iieasy_proxy
|
||||
image: nginx:stable-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- "85:80"
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
- frontend
|
||||
- strapi
|
||||
|
||||
# БЭКЕНД: Strapi (порт наружу не пробрасывается, доступ через proxy)
|
||||
strapi:
|
||||
container_name: iieasy_backend
|
||||
build:
|
||||
context: ./iiEasy
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
STRAPI_ADMIN_BACKEND_URL: "https://iieasy.ru"
|
||||
restart: always
|
||||
ports:
|
||||
- "1340:1340"
|
||||
# Пробрасываем папку с загрузками, чтобы картинки не удалились при обновлении контейнера
|
||||
volumes:
|
||||
- ./iiEasy/public/uploads:/opt/app/public/uploads
|
||||
@@ -19,15 +35,15 @@ services:
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
|
||||
# ФРОНТЕНД: Сайт с Tailwind (находится в текущей папке)
|
||||
# ФРОНТЕНД: статика (порт наружу не пробрасывается, доступ через proxy)
|
||||
frontend:
|
||||
container_name: iieasy_frontend
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
VITE_STRAPI_URL: ""
|
||||
restart: always
|
||||
ports:
|
||||
- "85:80" # Твой запрос: заходим через 85 порт
|
||||
depends_on:
|
||||
- strapi
|
||||
|
||||
|
||||
BIN
frontend-image.tar
Normal file
BIN
frontend-image.tar
Normal file
Binary file not shown.
@@ -6,3 +6,7 @@ ADMIN_JWT_SECRET=tobemodified
|
||||
TRANSFER_TOKEN_SALT=tobemodified
|
||||
JWT_SECRET=tobemodified
|
||||
ENCRYPTION_KEY=tobemodified
|
||||
|
||||
# Ollama (прокси /api/ollama/chat). Сервер должен быть доступен с хоста, где запущен Strapi.
|
||||
OLLAMA_URL=http://192.168.88.160:11434
|
||||
OLLAMA_MODEL=gemma3n:e4b
|
||||
|
||||
@@ -26,6 +26,10 @@ RUN npm ci --omit=dev
|
||||
# Копируем весь код проекта
|
||||
COPY . .
|
||||
|
||||
# Публичный URL для админки (запросы из браузера идут на этот origin)
|
||||
ARG STRAPI_ADMIN_BACKEND_URL
|
||||
ENV STRAPI_ADMIN_BACKEND_URL=${STRAPI_ADMIN_BACKEND_URL}
|
||||
|
||||
# Собираем админку Strapi
|
||||
ENV NODE_ENV=production
|
||||
RUN npm run build
|
||||
|
||||
@@ -1,9 +1,36 @@
|
||||
export default ({ env }) => ({
|
||||
export default ({ env }) => {
|
||||
// #region agent log
|
||||
const apiTokenSaltValue = env('API_TOKEN_SALT');
|
||||
const apiTokenSaltFinal = apiTokenSaltValue || 'dev-api-token-salt-change-in-production';
|
||||
fetch('http://localhost:7246/ingest/f7a9af46-2b22-4f00-8298-4ecbb46f59d7', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: `log_${Date.now()}_apiTokenSalt`,
|
||||
timestamp: Date.now(),
|
||||
location: 'config/admin.ts',
|
||||
message: 'Check API_TOKEN_SALT presence',
|
||||
runId: 'post-fix',
|
||||
hypothesisId: 'H2',
|
||||
data: {
|
||||
hasApiTokenSaltEnv: !!process.env.API_TOKEN_SALT,
|
||||
envReturnsLength: (apiTokenSaltValue || '').length,
|
||||
envReturnsDefined: !!apiTokenSaltValue,
|
||||
saltUsedLength: apiTokenSaltFinal.length,
|
||||
},
|
||||
}),
|
||||
}).catch(() => {});
|
||||
// #endregion
|
||||
|
||||
return {
|
||||
auth: {
|
||||
secret: env('ADMIN_JWT_SECRET'),
|
||||
// Стратегия: если переменная окружения не задана, используем dev‑секрет по умолчанию,
|
||||
// чтобы Strapi не падал с ошибкой Missing admin.auth.secret.
|
||||
// В проде обязательно переопредели ADMIN_JWT_SECRET в .env!
|
||||
secret: env('ADMIN_JWT_SECRET', 'change-me-admin-jwt-secret-dev-only'),
|
||||
},
|
||||
apiToken: {
|
||||
salt: env('API_TOKEN_SALT'),
|
||||
salt: apiTokenSaltFinal,
|
||||
},
|
||||
transfer: {
|
||||
token: {
|
||||
@@ -17,4 +44,5 @@ export default ({ env }) => ({
|
||||
nps: env.bool('FLAG_NPS', true),
|
||||
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
||||
},
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@ export default [
|
||||
'strapi::logger',
|
||||
'strapi::errors',
|
||||
'strapi::security',
|
||||
'strapi::cors',
|
||||
{
|
||||
name: 'strapi::cors',
|
||||
config: {
|
||||
origin: '*', // разрешить любой origin (строка; true не поддерживается — вызывает originList.split)
|
||||
headers: ['Content-Type', 'Authorization'],
|
||||
},
|
||||
},
|
||||
'strapi::poweredBy',
|
||||
'strapi::query',
|
||||
'strapi::body',
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
export default ({ env }) => ({
|
||||
host: env('HOST', '0.0.0.0'),
|
||||
// Явно 0.0.0.0: иначе в некоторых сценариях Strapi может взять host из defaultConfig (localhost).
|
||||
// В консоли по-прежнему показывается http://localhost:PORT — это только для открытия в браузере, bind идёт на 0.0.0.0.
|
||||
host: env('HOST', '0.0.0.0') || '0.0.0.0',
|
||||
port: env.int('PORT', 1340),
|
||||
app: {
|
||||
keys: env.array('APP_KEYS'),
|
||||
// Если APP_KEYS не заданы в .env, используем dev‑значения по умолчанию,
|
||||
// чтобы middleware strapi::session не падал. В проде ОБЯЗАТЕЛЬНО переопредели APP_KEYS!
|
||||
keys: env.array('APP_KEYS', ['devKeyA-change-me', 'devKeyB-change-me']),
|
||||
},
|
||||
});
|
||||
|
||||
2092
iiEasy/package-lock.json
generated
2092
iiEasy/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
iiEasy/src/api/ollama/controllers/ollama.ts
Normal file
38
iiEasy/src/api/ollama/controllers/ollama.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Прокси к Ollama: запросы из браузера идут на тот же origin (Strapi), CORS не нужен.
|
||||
* Strapi дергает Ollama по внутренней сети (OLLAMA_URL).
|
||||
*/
|
||||
|
||||
export default {
|
||||
async chat(ctx) {
|
||||
const ollamaBase = process.env.OLLAMA_URL || 'http://192.168.88.160:11434';
|
||||
const model = process.env.OLLAMA_MODEL || 'gemma3n:e4b';
|
||||
const url = `${ollamaBase.replace(/\/$/, '')}/api/chat`;
|
||||
|
||||
const body = ctx.request.body as Record<string, unknown>;
|
||||
const payload = {
|
||||
model: body?.model ?? model,
|
||||
messages: body?.messages ?? [],
|
||||
stream: body?.stream ?? false,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
|
||||
ctx.status = response.status;
|
||||
ctx.body = data;
|
||||
ctx.set('Content-Type', 'application/json');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Ошибка прокси к Ollama';
|
||||
ctx.status = 502;
|
||||
ctx.body = { error: message };
|
||||
ctx.set('Content-Type', 'application/json');
|
||||
}
|
||||
},
|
||||
};
|
||||
22
iiEasy/src/api/ollama/routes/01-ollama.ts
Normal file
22
iiEasy/src/api/ollama/routes/01-ollama.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Прокси-маршрут для Ollama: POST /api/ollama/chat
|
||||
* Браузер вызывает тот же origin, что и Strapi — CORS не мешает, работает из любой сети.
|
||||
*/
|
||||
|
||||
import type { Core } from '@strapi/strapi';
|
||||
|
||||
const config: Core.RouterConfig = {
|
||||
type: 'content-api',
|
||||
routes: [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/ollama/chat', // полный путь: /api + /ollama/chat = /api/ollama/chat
|
||||
handler: 'api::ollama.ollama.chat',
|
||||
config: {
|
||||
auth: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
iiEasy/types/generated/contentTypes.d.ts
vendored
1
iiEasy/types/generated/contentTypes.d.ts
vendored
@@ -1073,6 +1073,7 @@ export interface PluginUploadFile extends Struct.CollectionTypeSchema {
|
||||
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||
Schema.Attribute.Private;
|
||||
ext: Schema.Attribute.String;
|
||||
focalPoint: Schema.Attribute.JSON;
|
||||
folder: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'> &
|
||||
Schema.Attribute.Private;
|
||||
folderPath: Schema.Attribute.String &
|
||||
|
||||
@@ -285,8 +285,8 @@
|
||||
"addressCountry": "RU"
|
||||
},
|
||||
"email": "hello@iieasy.ru",
|
||||
"url": "https://www.iieasy.example.com",
|
||||
"image": "https://www.iieasy.example.com/logo.png",
|
||||
"url": "https://iieasy.ru",
|
||||
"image": "https://iieasy.ru/logo.png",
|
||||
"telephone": "+7 963 890 8700",
|
||||
"priceRange": "$$",
|
||||
"areaServed": {
|
||||
|
||||
84
nginx/conf.d/default.conf
Normal file
84
nginx/conf.d/default.conf
Normal file
@@ -0,0 +1,84 @@
|
||||
# Обратный прокси: единственная точка входа. Снаружи открыт только порт 85.
|
||||
# /api, /uploads, /admin -> Strapi (внутренний strapi:1340)
|
||||
# / -> фронт (frontend:80)
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
client_max_body_size 200M;
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Strapi API
|
||||
location /api {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Strapi Upload plugin API (/api/upload уже под /api; на всякий случай явно)
|
||||
location /upload {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 200M;
|
||||
}
|
||||
|
||||
# Strapi uploads (медиа-файлы)
|
||||
location /uploads {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Strapi admin panel
|
||||
location /admin {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Strapi Content Manager API (/content-manager/init и др.)
|
||||
location /content-manager {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Strapi Content-Type Builder API (/content-type-builder/schema и др.)
|
||||
location /content-type-builder {
|
||||
proxy_pass http://strapi:1337;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Фронт: статический сайт
|
||||
location / {
|
||||
proxy_pass http://frontend:80;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
47
package-lock.json
generated
47
package-lock.json
generated
@@ -14,6 +14,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
@@ -745,10 +747,47 @@
|
||||
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
|
||||
"integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.25.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||
@@ -852,6 +891,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -893,6 +933,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -1004,9 +1045,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.0",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.2.0"
|
||||
}
|
||||
|
||||
BIN
strapi-data.tar.gz
Normal file
BIN
strapi-data.tar.gz
Normal file
Binary file not shown.
BIN
strapi-image.tar
Normal file
BIN
strapi-image.tar
Normal file
Binary file not shown.
@@ -1,9 +1,20 @@
|
||||
|
||||
|
||||
/// <reference types="vite/client" />
|
||||
import { Post, NewsArticle, ResearchPaper, BusinessStory, ServiceItemData, ClientLogo, BusinessCourse, StudentProgram, AcceleratorProject, InvestmentProject, Vacancy, GalleryItem } from './types';
|
||||
|
||||
const STRAPI_URL = 'https://n8n.iieasy.ru';
|
||||
const API_TOKEN = '91344ae88ae3e496f72d6ae9c157a3e929c3078b6269b57d3751d81026880cca6e7dcec074bffd77c8fe853dce9308f8d7dd494cf7b8c804a2875f0f4c24c0419cee196adf247d01cd5ddd5893d325bff34ad236febe0b508114ec906b12423e658d7210be94f2fcc37184334e11065538531c6d935de5becf81f2a48fdd6318';
|
||||
/**
|
||||
* Базовый URL Strapi API.
|
||||
* - В dev: пустая строка — запросы идут через Vite proxy на Strapi.
|
||||
* - В prod за прокси: задайте VITE_STRAPI_URL="" при сборке — запросы на тот же origin (порт 85).
|
||||
* - В prod без прокси: задайте VITE_STRAPI_URL=http://... при сборке; иначе используется запасной URL.
|
||||
*/
|
||||
const _url = import.meta.env?.VITE_STRAPI_URL;
|
||||
export const STRAPI_URL =
|
||||
import.meta.env?.DEV
|
||||
? ''
|
||||
: typeof _url === 'string'
|
||||
? _url
|
||||
: 'http://192.168.88.121:1337';
|
||||
const API_TOKEN = '9e4de071544acf9ca7a9bc2b01c2fdc913c1e92e930b6c04193a00cb71a27787c19fd546e4fcebee6ed34b284015506a6eb650cea99291149328f78b4020c5028337e3dcd45f50ca379944b0a4a8276bb7f9127aa816a8b35eb68a7beef54d52a0eeea84991776165347039c20fa8b447df6c4ff653a82d4684da9db9f7716db';
|
||||
|
||||
interface StrapiResponse<T> {
|
||||
data: StrapiDataItem<T>[];
|
||||
@@ -440,3 +451,40 @@ export const fetchStudentPrograms = (): Promise<StudentProgram[]> => fetchData('
|
||||
export const fetchAcceleratorProjects = (): Promise<AcceleratorProject[]> => fetchData('accelerator-projects', transformAcceleratorProject);
|
||||
export const fetchInvestmentProjects = (): Promise<InvestmentProject[]> => fetchData('investment-opportunities', transformInvestmentProject);
|
||||
export const fetchVacancies = (): Promise<Vacancy[]> => fetchData('vacancies', transformVacancy);
|
||||
|
||||
/** Список файлов из Strapi Media Library (для страницы «О нас» и др.).
|
||||
* В Strapi: Settings → API Tokens → роль токена должна иметь Plugins → Upload → find (доступ к медиа). */
|
||||
export interface StrapiUploadFile {
|
||||
url: string;
|
||||
name?: string;
|
||||
id?: number;
|
||||
}
|
||||
|
||||
function parseUploadItem(item: any): StrapiUploadFile | null {
|
||||
const attrs = item?.attributes ?? item;
|
||||
const url = typeof attrs?.url === 'string' ? attrs.url : typeof item?.url === 'string' ? item.url : '';
|
||||
if (!url) return null;
|
||||
const name = attrs?.name ?? attrs?.fileName ?? item?.name ?? '';
|
||||
return { id: item?.id ?? attrs?.id, url, name };
|
||||
}
|
||||
|
||||
/** Папки в Content API недоступны — запрос без фильтра folder. */
|
||||
export async function fetchUploadFiles(): Promise<StrapiUploadFile[]> {
|
||||
try {
|
||||
const response = await fetch(`${STRAPI_URL}/api/upload/files`, {
|
||||
headers: { Authorization: `Bearer ${API_TOKEN}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.warn('fetchUploadFiles:', response.status, await response.text());
|
||||
return [];
|
||||
}
|
||||
const json = await response.json();
|
||||
const list = Array.isArray(json) ? json : json?.data;
|
||||
if (!Array.isArray(list)) return [];
|
||||
const files = list.map((item: any) => parseUploadItem(item)).filter(Boolean) as StrapiUploadFile[];
|
||||
return files.sort((a, b) => (a.name ?? '').localeCompare(b.name ?? ''));
|
||||
} catch (e: any) {
|
||||
console.warn('fetchUploadFiles error:', e?.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, '.', '');
|
||||
const strapiTarget = env.VITE_STRAPI_PROXY_TARGET || 'http://localhost:1337';
|
||||
return {
|
||||
define: {
|
||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||
@@ -17,14 +18,15 @@ export default defineConfig(({ mode }) => {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
// --- ДОБАВЬ ЭТОТ БЛОК ---
|
||||
server: {
|
||||
host: '0.0.0.0', // Чтобы слушал все интерфейсы
|
||||
port: 5173, // Твой порт
|
||||
allowedHosts: ['iieasy.ru',
|
||||
'n8n.iieasy.ru'
|
||||
], // Разрешаем твой домен
|
||||
host: '0.0.0.0',
|
||||
port: 5173,
|
||||
allowedHosts: ['iieasy.ru', 'n8n.iieasy.ru'],
|
||||
// Прокси к Strapi: в dev запросы на /api и /uploads идут на Strapi без CORS и без хардкода IP
|
||||
proxy: {
|
||||
'/api': { target: strapiTarget, changeOrigin: true },
|
||||
'/uploads': { target: strapiTarget, changeOrigin: true },
|
||||
},
|
||||
},
|
||||
// ------------------------
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user