Update: перенастройка сайта что бы открывать 1 порт на сайт

This commit is contained in:
2026-02-11 15:46:19 +05:00
parent 65a9143bd0
commit 62340e4406
25 changed files with 1393 additions and 1216 deletions

29
DEPLOY-SERVER.md Normal file
View 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 пропадёт.

View File

@@ -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 . .

View File

@@ -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://...` и при необходимости откройте порты вручную.

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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. Какие данные мы собираем",

View File

@@ -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

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -1,20 +1,48 @@
export default ({ env }) => ({
auth: {
secret: env('ADMIN_JWT_SECRET'),
},
apiToken: {
salt: env('API_TOKEN_SALT'),
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT'),
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: {
// Стратегия: если переменная окружения не задана, используем devсекрет по умолчанию,
// чтобы Strapi не падал с ошибкой Missing admin.auth.secret.
// В проде обязательно переопредели ADMIN_JWT_SECRET в .env!
secret: env('ADMIN_JWT_SECRET', 'change-me-admin-jwt-secret-dev-only'),
},
},
secrets: {
encryptionKey: env('ENCRYPTION_KEY'),
},
flags: {
nps: env.bool('FLAG_NPS', true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
},
});
apiToken: {
salt: apiTokenSaltFinal,
},
transfer: {
token: {
salt: env('TRANSFER_TOKEN_SALT'),
},
},
secrets: {
encryptionKey: env('ENCRYPTION_KEY'),
},
flags: {
nps: env.bool('FLAG_NPS', true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
},
};
};

View File

@@ -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',

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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');
}
},
};

View 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;

View File

@@ -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 &

View File

@@ -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
View 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
View File

@@ -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": {

View File

@@ -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

Binary file not shown.

BIN
strapi-image.tar Normal file

Binary file not shown.

View File

@@ -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 [];
}
}

View File

@@ -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 },
},
},
// ------------------------
};
});