Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62340e4406 | |||
| 65a9143bd0 | |||
| e2d565ad97 |
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: Сборка
|
# Этап 1: Сборка
|
||||||
FROM node:20-alpine AS build
|
FROM node:20-alpine AS build
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
# Пустой VITE_STRAPI_URL при сборке за прокси — запросы идут на тот же origin (порт 85)
|
||||||
|
ARG VITE_STRAPI_URL=
|
||||||
|
ENV VITE_STRAPI_URL=$VITE_STRAPI_URL
|
||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
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
|
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||||
3. Run the app:
|
3. Run the app:
|
||||||
`npm run dev`
|
`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 { 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 = [
|
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 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(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver(
|
||||||
(entries) => {
|
(entries) => {
|
||||||
@@ -68,16 +97,14 @@ const AboutUsSection: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{ threshold: 0.1 }
|
||||||
threshold: 0.1,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const elements = document.querySelectorAll('.scroll-animate');
|
const elements = document.querySelectorAll('.scroll-animate');
|
||||||
elements.forEach((el) => observer.observe(el));
|
elements.forEach((el) => observer.observe(el));
|
||||||
|
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, [aboutImages]);
|
||||||
|
|
||||||
|
const getImageUrl = (index: number) => aboutImages[index] ?? PLACEHOLDER_IMAGES[index];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
@@ -100,8 +127,8 @@ const AboutUsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
<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"/>
|
<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">Команда iiEasy в Уфе: от идеи к реализации.</figcaption>
|
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[0][1]}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{/* Mission */}
|
{/* Mission */}
|
||||||
@@ -130,8 +157,8 @@ const AboutUsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
<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"/>
|
<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">Глубокая техническая работа — основа наших решений.</figcaption>
|
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[1][1]}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{/* Team */}
|
{/* Team */}
|
||||||
@@ -143,8 +170,8 @@ const AboutUsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="my-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
<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"/>
|
<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">Наша команда — наш главный актив.</figcaption>
|
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[2][1]}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
{/* Principles */}
|
{/* Principles */}
|
||||||
@@ -160,8 +187,8 @@ const AboutUsSection: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<figure className="mt-12 md:my-16 scroll-animate" style={{ transitionDelay: '200ms' }}>
|
<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"/>
|
<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">Нацелены на будущее развитие нашего региона.</figcaption>
|
<figcaption className="text-center text-sm text-slate-500 mt-3 italic">{ABOUT_IMAGE_ALT[3][1]}</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
|||||||
import { CurrentView } from '../types';
|
import { CurrentView } from '../types';
|
||||||
import { NAVIGABLE_VIEWS } from '../constants';
|
import { NAVIGABLE_VIEWS } from '../constants';
|
||||||
import { ArrowUpIcon } from './icons';
|
import { ArrowUpIcon } from './icons';
|
||||||
|
import { STRAPI_URL } from '../strapiService';
|
||||||
|
|
||||||
const PLACEHOLDERS = [
|
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.`;
|
Your entire response must be only the JSON object, with no other text, explanation, or markdown formatting.`;
|
||||||
|
|
||||||
try {
|
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',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
model: 'local-model',
|
model: 'gemma3n:e4b',
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: systemInstruction },
|
{ role: 'system', content: systemInstruction },
|
||||||
{ role: 'user', content: prompt }
|
{ role: 'user', content: prompt }
|
||||||
],
|
],
|
||||||
temperature: 0.1,
|
stream: false,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.error) {
|
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) {
|
if (!responseJsonString) {
|
||||||
throw new Error("LM Studio вернула пустой ответ.");
|
throw new Error("Сервер ИИ вернул пустой ответ.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// More robust JSON extraction
|
// More robust JSON extraction
|
||||||
@@ -127,8 +131,8 @@ const ChatInput: React.FC<ChatInputProps> = ({ setCurrentView, isSticky = false,
|
|||||||
setError("К сожалению, я не смог понять ваш запрос. Попробуйте переформулировать.");
|
setError("К сожалению, я не смог понять ваш запрос. Попробуйте переформулировать.");
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Ошибка при обращении к LM Studio:", err);
|
console.error("Ошибка при обращении к серверу ИИ (Ollama):", err);
|
||||||
setError(err.message || "Произошла ошибка. Убедитесь, что сервер LM Studio запущен и модель загружена. Пожалуйста, попробуйте еще раз.");
|
setError(err.message || "Произошла ошибка. Убедитесь, что бэкенд и сервер Ollama доступны. Пожалуйста, попробуйте еще раз.");
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,7 +586,7 @@
|
|||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
heading: "1. Общие положения",
|
heading: "1. Общие положения",
|
||||||
content: "Настоящие Правила использования (далее – «Правила») регулируют отношения между ООО \"ИЗИ ГРУПП\" (далее – «Сервис») и Пользователем сети Интернет (далее – «Пользователь») возникающие при использовании сайта iieasy.example.com (далее – «Сайт»). Пользователь обязан полностью ознакомиться с настоящими Правилами до момента начала использования Сайта. Использование Сайта Пользователем означает полное и безоговорочное принятие Пользователем настоящих Правил в соответствии со ст. 438 Гражданского кодекса Российской Федерации."
|
content: "Настоящие Правила использования (далее – «Правила») регулируют отношения между ООО \"ИЗИ ГРУПП\" (далее – «Сервис») и Пользователем сети Интернет (далее – «Пользователь») возникающие при использовании сайта iieasy.ru (далее – «Сайт»). Пользователь обязан полностью ознакомиться с настоящими Правилами до момента начала использования Сайта. Использование Сайта Пользователем означает полное и безоговорочное принятие Пользователем настоящих Правил в соответствии со ст. 438 Гражданского кодекса Российской Федерации."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "2. Интеллектуальная собственность",
|
heading: "2. Интеллектуальная собственность",
|
||||||
@@ -611,7 +611,7 @@
|
|||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
heading: "1. Введение",
|
heading: "1. Введение",
|
||||||
content: "Настоящая Политика конфиденциальности персональных данных (далее – «Политика») действует в отношении всей информации, которую ООО \"ИЗИ ГРУПП\" (далее – «Компания») может получить о Пользователе во время использования сайта iieasy.example.com (далее – «Сайт»). Использование Сайта означает безоговорочное согласие Пользователя с настоящей Политикой и указанными в ней условиями обработки его персональной информации; в случае несогласия с этими условиями Пользователь должен воздержаться от использования Сайта."
|
content: "Настоящая Политика конфиденциальности персональных данных (далее – «Политика») действует в отношении всей информации, которую ООО \"ИЗИ ГРУПП\" (далее – «Компания») может получить о Пользователе во время использования сайта iieasy.ru (далее – «Сайт»). Использование Сайта означает безоговорочное согласие Пользователя с настоящей Политикой и указанными в ней условиями обработки его персональной информации; в случае несогласия с этими условиями Пользователь должен воздержаться от использования Сайта."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
heading: "2. Какие данные мы собираем",
|
heading: "2. Какие данные мы собираем",
|
||||||
|
|||||||
@@ -1,15 +1,31 @@
|
|||||||
|
# Снаружи открыт только порт 85 (прокси). Сайт и API по http://хост:85
|
||||||
|
# Strapi и фронт доступны только внутри Docker-сети через прокси.
|
||||||
|
|
||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
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:
|
strapi:
|
||||||
container_name: iieasy_backend
|
container_name: iieasy_backend
|
||||||
build:
|
build:
|
||||||
context: ./iiEasy
|
context: ./iiEasy
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
STRAPI_ADMIN_BACKEND_URL: "https://iieasy.ru"
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
|
||||||
- "1340:1340"
|
|
||||||
# Пробрасываем папку с загрузками, чтобы картинки не удалились при обновлении контейнера
|
# Пробрасываем папку с загрузками, чтобы картинки не удалились при обновлении контейнера
|
||||||
volumes:
|
volumes:
|
||||||
- ./iiEasy/public/uploads:/opt/app/public/uploads
|
- ./iiEasy/public/uploads:/opt/app/public/uploads
|
||||||
@@ -19,15 +35,15 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
|
||||||
# ФРОНТЕНД: Сайт с Tailwind (находится в текущей папке)
|
# ФРОНТЕНД: статика (порт наружу не пробрасывается, доступ через proxy)
|
||||||
frontend:
|
frontend:
|
||||||
container_name: iieasy_frontend
|
container_name: iieasy_frontend
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
VITE_STRAPI_URL: ""
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
|
||||||
- "85:80" # Твой запрос: заходим через 85 порт
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- strapi
|
- 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
|
TRANSFER_TOKEN_SALT=tobemodified
|
||||||
JWT_SECRET=tobemodified
|
JWT_SECRET=tobemodified
|
||||||
ENCRYPTION_KEY=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 . .
|
COPY . .
|
||||||
|
|
||||||
|
# Публичный URL для админки (запросы из браузера идут на этот origin)
|
||||||
|
ARG STRAPI_ADMIN_BACKEND_URL
|
||||||
|
ENV STRAPI_ADMIN_BACKEND_URL=${STRAPI_ADMIN_BACKEND_URL}
|
||||||
|
|
||||||
# Собираем админку Strapi
|
# Собираем админку Strapi
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN npm run build
|
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: {
|
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: {
|
apiToken: {
|
||||||
salt: env('API_TOKEN_SALT'),
|
salt: apiTokenSaltFinal,
|
||||||
},
|
},
|
||||||
transfer: {
|
transfer: {
|
||||||
token: {
|
token: {
|
||||||
@@ -17,4 +44,5 @@ export default ({ env }) => ({
|
|||||||
nps: env.bool('FLAG_NPS', true),
|
nps: env.bool('FLAG_NPS', true),
|
||||||
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ export default [
|
|||||||
'strapi::logger',
|
'strapi::logger',
|
||||||
'strapi::errors',
|
'strapi::errors',
|
||||||
'strapi::security',
|
'strapi::security',
|
||||||
'strapi::cors',
|
{
|
||||||
|
name: 'strapi::cors',
|
||||||
|
config: {
|
||||||
|
origin: '*', // разрешить любой origin (строка; true не поддерживается — вызывает originList.split)
|
||||||
|
headers: ['Content-Type', 'Authorization'],
|
||||||
|
},
|
||||||
|
},
|
||||||
'strapi::poweredBy',
|
'strapi::poweredBy',
|
||||||
'strapi::query',
|
'strapi::query',
|
||||||
'strapi::body',
|
'strapi::body',
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
export default ({ env }) => ({
|
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),
|
port: env.int('PORT', 1340),
|
||||||
app: {
|
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'> &
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
ext: Schema.Attribute.String;
|
ext: Schema.Attribute.String;
|
||||||
|
focalPoint: Schema.Attribute.JSON;
|
||||||
folder: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'> &
|
folder: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'> &
|
||||||
Schema.Attribute.Private;
|
Schema.Attribute.Private;
|
||||||
folderPath: Schema.Attribute.String &
|
folderPath: Schema.Attribute.String &
|
||||||
|
|||||||
@@ -285,8 +285,8 @@
|
|||||||
"addressCountry": "RU"
|
"addressCountry": "RU"
|
||||||
},
|
},
|
||||||
"email": "hello@iieasy.ru",
|
"email": "hello@iieasy.ru",
|
||||||
"url": "https://www.iieasy.example.com",
|
"url": "https://iieasy.ru",
|
||||||
"image": "https://www.iieasy.example.com/logo.png",
|
"image": "https://iieasy.ru/logo.png",
|
||||||
"telephone": "+7 963 890 8700",
|
"telephone": "+7 963 890 8700",
|
||||||
"priceRange": "$$",
|
"priceRange": "$$",
|
||||||
"areaServed": {
|
"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": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
@@ -745,10 +747,47 @@
|
|||||||
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
|
"integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"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": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.5",
|
"version": "0.25.5",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
|
||||||
@@ -852,6 +891,7 @@
|
|||||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -893,6 +933,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -1004,9 +1045,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.0",
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"vite": "^6.2.0"
|
"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,10 +1,20 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
|
||||||
import { Post, NewsArticle, ResearchPaper, BusinessStory, ServiceItemData, ClientLogo, BusinessCourse, StudentProgram, AcceleratorProject, InvestmentProject, Vacancy, GalleryItem } from './types';
|
import { Post, NewsArticle, ResearchPaper, BusinessStory, ServiceItemData, ClientLogo, BusinessCourse, StudentProgram, AcceleratorProject, InvestmentProject, Vacancy, GalleryItem } from './types';
|
||||||
|
|
||||||
const STRAPI_URL = 'https://next.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> {
|
interface StrapiResponse<T> {
|
||||||
data: StrapiDataItem<T>[];
|
data: StrapiDataItem<T>[];
|
||||||
@@ -441,3 +451,40 @@ export const fetchStudentPrograms = (): Promise<StudentProgram[]> => fetchData('
|
|||||||
export const fetchAcceleratorProjects = (): Promise<AcceleratorProject[]> => fetchData('accelerator-projects', transformAcceleratorProject);
|
export const fetchAcceleratorProjects = (): Promise<AcceleratorProject[]> => fetchData('accelerator-projects', transformAcceleratorProject);
|
||||||
export const fetchInvestmentProjects = (): Promise<InvestmentProject[]> => fetchData('investment-opportunities', transformInvestmentProject);
|
export const fetchInvestmentProjects = (): Promise<InvestmentProject[]> => fetchData('investment-opportunities', transformInvestmentProject);
|
||||||
export const fetchVacancies = (): Promise<Vacancy[]> => fetchData('vacancies', transformVacancy);
|
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 }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, '.', '');
|
const env = loadEnv(mode, '.', '');
|
||||||
|
const strapiTarget = env.VITE_STRAPI_PROXY_TARGET || 'http://localhost:1337';
|
||||||
return {
|
return {
|
||||||
define: {
|
define: {
|
||||||
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
|
||||||
@@ -17,14 +18,15 @@ export default defineConfig(({ mode }) => {
|
|||||||
'@': path.resolve(__dirname, '.'),
|
'@': path.resolve(__dirname, '.'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// --- ДОБАВЬ ЭТОТ БЛОК ---
|
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // Чтобы слушал все интерфейсы
|
host: '0.0.0.0',
|
||||||
port: 5173, // Твой порт
|
port: 5173,
|
||||||
allowedHosts: ['iieasy.ru',
|
allowedHosts: ['iieasy.ru', 'n8n.iieasy.ru'],
|
||||||
'next.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