Initial commit: TopSysOps site + admin panel

Multi-page public site (Next.js), Node/Express backend with Postgres,
and Vite admin/account app, restructured from a single-page layout
into dedicated service, promotions, cases, about, FAQ, and blog pages
for SEO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013otXCiTZsxdZ4SJKZ9wUky
This commit is contained in:
ogrechkoandClaude Sonnet 5 committed 2026-08-25 23:12:16 +03:00
commit 130c5382cd
108 files changed
+7146

No files matched your search

+390
View File
@@ -0,0 +1,390 @@
# TopSysOps - сайт + админ-панель
Продакшн-версия сайта top-sysops.ru: публичная часть с серверным рендерингом на
Next.js (Главная, Акции, Услуги, Кейсы, FAQ, Блог с рич-текст редактором, Отзывы,
Контакты), личный кабинет для клиентов (переписка с командой, ИИ-чат, профиль) и
админ-панель (дашборд посещаемости, модерация отзывов, редактор прайс-листа,
редактор акций, редактор кейсов и FAQ, редактор блога, переписка с клиентами,
ИИ-ассистент для сотрудников, виджет чата поддержки на сайте). SEO: sitemap.xml,
robots.txt и разметка schema.org генерируются самим Next.js.
## Из чего состоит
```
├── docker-compose.yml # четыре сервиса: db, backend, nextjs, web
├── .env.example # шаблон настроек, скопировать в .env
├── backend/ # Node.js/Express API + PostgreSQL
├── public-site/ # Next.js (SSR) - публичный сайт и блог
└── admin-app/ # React (Vite) - админка и личный кабинет + Caddy
```
- **db** - PostgreSQL. Хранит услуги/цены, акции, отзывы, статьи блога,
статистику визитов, клиентов и учётные записи администраторов.
- **backend** - REST API. При первом запуске сам накатывает миграции и создаёт
администратора и стартовый прайс-лист/акции (те же позиции, что были на старом сайте).
- **nextjs** - публичный сайт и блог, отдаются с сервера (SSR) - это и есть
ответ на "нужен SEO": краулер получает готовый HTML с разметкой schema.org,
а не пустую страницу, которую дорисовывает JavaScript. Не имеет отдельного
порта наружу - доступен только через Caddy.
- **web** - Caddy: собирает и раздаёт статику `admin-app` (админка и личный
кабинет - им SEO не нужен, поэтому остались обычным SPA), проксирует
`/api/*` на backend, всё остальное - на `nextjs`. Caddy же **сам получает и
продлевает HTTPS-сертификат** Let's Encrypt - вручную ничего настраивать не
нужно, только домен в `.env`.
Почему не переписали админку и кабинет на Next.js тоже: SEO им не нужно
вообще (они закрыты в `robots.txt` и требуют авторизации), а полный переезд
уже готовой и работающей админки ради нулевой выгоды - это только риск что-то
сломать. SSR применён именно там, где от него есть польза - на страницах,
которые должны индексироваться.
## Требования
- Сервер на Ubuntu 24.04 (2 ГБ RAM хватит с запасом).
- Домен, у которого A-запись указывает на IP сервера - нужен для автоматического HTTPS.
Если домена пока нет, можно временно развернуть по IP без HTTPS (см. `.env.example`).
- Открытые порты 80 и 443.
## 1. Установка Docker на Ubuntu 24
```bash
sudo apt update
sudo apt install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# чтобы не писать sudo перед каждой командой docker (перелогиньтесь после этого)
sudo usermod -aG docker $USER
```
Проверка: `docker compose version`.
## 2. Загрузка проекта на сервер
Скопируйте всю папку проекта на сервер, например через `scp` или `git`:
```bash
scp -r topsysops-app your-user@your-server-ip:~/topsysops-app
# или, если проект в git-репозитории:
git clone <ваш-репозиторий> topsysops-app
```
## 3. Настройка .env
```bash
cd topsysops-app
cp .env.example .env
nano .env
```
Обязательно замените:
- `POSTGRES_PASSWORD` - пароль базы данных;
- `JWT_SECRET` - длинная случайная строка (`openssl rand -hex 32`);
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` - логин и пароль первого администратора;
- `DOMAIN` - ваш домен (например `top-sysops.ru`) или `:80`, если тестируете
по IP без HTTPS.
Файл `.env` содержит секреты - он уже добавлен в `.gitignore`, не публикуйте его.
## 4. Запуск
```bash
docker compose up -d --build
```
Первый запуск соберёт образы (нужен интернет на сервере), поднимет базу,
применит миграции, создаст администратора и стартовый прайс-лист/акции, затем
запустит сайт. Проверить статус и логи:
```bash
docker compose ps
docker compose logs -f backend
```
Дождитесь строки `[start] backend listening on port 4000`, затем откройте
сайт в браузере по вашему домену (или `http://IP-сервера`, если `DOMAIN=:80`).
Если используете реальный домен, Caddy автоматически выпустит сертификат при
первом обращении по HTTPS - на это может уйти несколько секунд.
## Если TLS у вас на Nginx Proxy Manager
Именно этот сценарий - в `.env.example` он уже настроен по умолчанию:
`DOMAIN=http://top-sysops.ru`, `HTTP_PORT=7778`, порт 443 у контейнера
закомментирован в `docker-compose.yml`. Со стороны NPM:
1. **Hosts -> Proxy Hosts -> Add Proxy Host**.
2. **Domain Names**: ваш домен (`top-sysops.ru`).
3. **Scheme**: `http`, **Forward Hostname/IP**: IP или hostname сервера, где
запущен docker compose (`127.0.0.1`, если NPM на той же машине; иначе
реальный IP), **Forward Port**: `7778` (или то, что указали в `HTTP_PORT`).
4. Вкладка **SSL**: выберите/запросите сертификат Let's Encrypt, включите
**Force SSL** - весь HTTPS полностью на стороне NPM, контейнеру об этом
вообще думать не нужно.
5. Save. Проверьте, что `docker compose ps` показывает `web` со проброшенным
портом `7778->80` - именно на него должен смотреть Forward Port в NPM.
Если после этого видите "too many redirects" - смотрите раздел
"Устранение неполадок" ниже, обычно это означает, что `DOMAIN` в `.env`
задан без схемы `http://`.
## 5. Первый вход в админку
Откройте `https://ваш-домен/admin`, войдите под `ADMIN_USERNAME` / `ADMIN_PASSWORD`
из `.env`, затем сразу зайдите в **Настройки -> Сменить пароль** и задайте
собственный пароль - значение из `.env` дальше не используется системой, но
лучше не оставлять его действующим.
Из админки доступно:
- **Дашборд** - визиты (всего/сегодня/график за 7 дней), количество отзывов;
- **Отзывы** - одобрить / отклонить / удалить;
- **Клиенты** - список зарегистрированных клиентов и переписка с ними (ответ на
сообщения из личного кабинета, метка «ждёт ответа» для непрочитанных);
- **Прайс-лист** - добавление и редактирование категорий и позиций услуг,
скрытие позиции с сайта без удаления (иконка глаза);
- **Акции** - то же самое для акций;
- **Кейсы** - карточки портфолио (название, тег, статус с цветным индикатором,
описание), с возможностью скрыть без удаления;
- **FAQ** - вопросы и ответы для сайта;
- **Блог** - статьи (заголовок, слаг, краткое описание, текст в WYSIWYG-редакторе
с форматированием - заголовки, списки, ссылки, цитаты - публикация) -
попадают на по-настоящему серверно отрендеренную страницу `/blog/<slug>` и в
sitemap.xml сразу после сохранения;
- **Контакты** - адрес, телефон, email, режим работы, ссылка на карту;
- **ИИ-ассистент** - внутренний чат для сотрудников на базе OpenRouter (см. ниже);
- **Настройки** - смена пароля и управление администраторами (добавить нового
сотрудника с логином/паролем, удалить лишнего - кроме себя и последнего
оставшегося администратора).
Изменения в прайсе, акциях, кейсах и FAQ появляются на сайте в течение минуты:
Next.js кэширует данные с публичного сайта на 60 секунд (`revalidate: 60`), это
плата за серверный рендеринг - страницы отдаются мгновенно, а не после похода в
базу на каждый заход. В самой админке изменения видны сразу же, без задержки.
## Обновление прайс-листа на уже развёрнутом сайте
Стартовый прайс-лист (в `seed.js`) применяется только к пустой базе - то есть
только при самом первом запуске. Для уже развёрнутого сайта обновление новой
линейки услуг оформлено как обычные миграции
(`backend/migrations/006_update_services_2026_07.sql` и
`007_landing_tier.sql`) - они применяются **автоматически** при следующем
запуске бэкенда, ничего вручную запускать не нужно:
```bash
docker compose up -d --build
```
Система миграций (`backend/src/migrate.js`) гарантирует, что каждый файл
выполнится ровно один раз - если уже разворачивали сайт раньше и обновляете
код сейчас, эти миграции просто окажутся среди тех, что применятся при
следующем перезапуске `backend`, наравне с остальными.
Что делают эти миграции: убирают старую категорию "Разработка сайтов"
(от 5 000 ₽) и добавляют:
- **Разработка под ключ** - Лендинг (от 20 000 ₽, простая посадочная без
бота/админки), MVP «Быстрый старт» (от 120 000 ₽, с формой-ботом и простой
админкой), MVP Pro (от 450 000 ₽, с личным кабинетом и ИИ-ассистентом - как
этот сайт), продакшн-разработка (от 1 200 000 ₽, индивидуально),
Discovery-воркшоп (от 30 000 ₽), сопровождение проекта (от 40 000 ₽/мес);
- **White-label и партнёрство** - white-label «Кабинет + ИИ-ассистент»
(от 500 000 ₽), IT-мониторинг сайта (от 10 000 ₽/мес), реферальная и
партнёрская программы.
Цифры сверены с рынком Санкт-Петербурга (лендинги студийного уровня -
19 000-65 000 ₽, MVP с личным кабинетом у студий - от 260 000 ₽ и выше).
Дальше все эти позиции редактируются как обычно - через админку («Прайс-лист»).
Если после `docker compose up -d --build` старая категория всё равно видна -
проверьте `docker compose logs backend | grep migrate`: там должны быть строки
`applying 006_update_services_2026_07.sql` и `applying 007_landing_tier.sql`.
Если их нет - значит контейнер `backend` не пересобрался/не перезапустился с
новым кодом (проверьте `docker compose ps`, при необходимости
`docker compose up -d --build backend` отдельно).
## SEO
- **robots.txt** и **sitemap.xml** - генерируются самим Next.js (`public-site/app/robots.js`
и `app/sitemap.js`), закрывают `/admin` и `/account` от индексации, доступны
по адресам `https://ваш-домен/robots.txt` и `/sitemap.xml`. Sitemap включает
главную, `/blog` и все опубликованные статьи - обновляется автоматически.
- **schema.org (JSON-LD)** - рендерится прямо на сервере (это заслуга SSR: разметка
оказывается в HTML, который получает краулер, а не дорисовывается JS уже в
браузере). На сайте автоматически публикуется `LocalBusiness` (адрес, телефон,
email из раздела «Контакты», рейтинг из одобренных отзывов), `FAQPage` (из
раздела FAQ) и каталог услуг (`hasOfferCatalog`, из прайс-листа) - помогает
Google/Яндексу показывать расширенные сниппеты (рейтинг со звёздами,
раскрывающиеся вопросы в выдаче). На страницах статей блога - разметка `Article`.
- **Блог с рич-текст редактором** - основной канал органического трафика для
B2B IT-услуг. Пишите статьи через админку («Блог», обычный WYSIWYG-редактор:
заголовки, списки, ссылки, цитаты) - каждая становится отдельной,
по-настоящему серверно отрендеренной страницей на `/blog/<slug>` и сразу
появляется в sitemap.xml.
- **Серверный рендеринг (SSR)** - публичный сайт и блог теперь на Next.js:
краулер и превью в соцсетях получают готовый HTML сразу, без ожидания
JavaScript. Админка и личный кабинет остались SPA на Vite - им это не нужно,
они и так закрыты в `robots.txt` и требуют входа.
- Что дальше сделать руками (не автоматизируется из кода): зарегистрировать сайт
в Яндекс.Вебмастере и Google Search Console (там же отправить sitemap.xml
вручную первый раз), завести профиль в Яндекс.Картах/Google Business с тем же
адресом и телефоном, что в разделе «Контакты» (важно для локального SEO по
запросам вида «IT-аутсорсинг Санкт-Петербург»).
## Чат поддержки на сайте (TopTicket / Chatwoot)
Виджет чата для посетителей сайта настраивается в `.env` и встраивается на
этапе сборки Next.js-приложения (значения переменных `NEXT_PUBLIC_*` попадают
в клиентский бандл, поэтому смена провайдера требует пересборки контейнера
`nextjs`, а не `web`).
По умолчанию уже включён TopTicket (ваша собственная разработка, help.top-sysops.ru)
с реальными данными. Выберите вариант в `.env`:
```bash
# TopTicket (по умолчанию) - embed-сниппет вашей разработки:
# <script src="https://help.top-sysops.ru/widget.js" data-key="..." async>
CHAT_PROVIDER=topticket
TOPTICKET_WIDGET_URL=https://help.top-sysops.ru/widget.js
TOPTICKET_WIDGET_KEY=EBpDS4JqT7a-_MzZ
# либо Chatwoot
CHAT_PROVIDER=chatwoot
CHATWOOT_BASE_URL=https://chat.lamlaba.ru
CHATWOOT_WEBSITE_TOKEN=ваш-website-token # Inboxes -> ваш инбокс -> Configuration
# либо совсем без чата
CHAT_PROVIDER=none
```
После изменения `.env`:
```bash
docker compose up -d --build nextjs
```
Виджет подключается только на публичных страницах сайта, в админке не показывается.
## ИИ-ассистент в админке (OpenRouter)
Это отдельный, второй чат - не для посетителей сайта, а внутренний инструмент
для сотрудников: помогает набросать ответ на отзыв, посчитать что-то по
прайсу и т.д. Доступен только тем, кто вошёл в админку.
Работает через [OpenRouter](https://openrouter.ai/) - агрегатор моделей разных
провайдеров под одним API. Ключ используется только на бэкенде и никогда не
попадает в браузер:
```bash
OPENROUTER_API_KEY=sk-or-... # https://openrouter.ai/keys
OPENROUTER_MODEL=openai/gpt-4o-mini # любая модель, доступная в вашем аккаунте
```
После заполнения:
```bash
docker compose up -d backend
```
Если `OPENROUTER_API_KEY` не задан, раздел «ИИ-ассистент» в админке просто
покажет предупреждение и не будет пытаться обращаться к API - остальной сайт
при этом продолжает работать как обычно.
## Контакты
Адрес, телефон, email, режим работы и ссылка на карту редактируются в админке
(раздел «Контакты», `/admin/contact`) — как и прайс-лист, акции, кейсы и FAQ,
без пересборки. Пустое поле просто не показывается на сайте (например, если
не хотите публиковать телефон).
## Личный кабинет клиентов
Отдельная система входа для посетителей сайта - не путать с админкой. Регистрация
и вход доступны по адресам `/account/register` и `/account`, на главной странице
есть заметная плашка-приглашение с этими же ссылками.
После входа клиенту доступны:
- **Обзор** - приветствие и текущие акции;
- **Написать нам** - личная переписка с командой TopSysOps. Сообщения клиента
видны в админке (раздел «Клиенты»), сотрудник отвечает прямо оттуда, ответ
сразу появляется в кабинете клиента;
- **ИИ-ассистент** - второй, отдельный чат, отвечает на вопросы об услугах и
ценах через тот же OpenRouter, что и ассистент в админке (см. выше), но с
собственным системным промптом и более строгим лимитом запросов;
- **Профиль** - изменение имени/компании/телефона, смена пароля.
Регистрация клиентов и вход в админку - две независимые системы: у них разные
таблицы в базе (`customers` и `admin_users`) и разные сессионные cookie
(`ts_customer_session` и `ts_admin_session`), так что сотрудник может быть
одновременно залогинен и в кабинет, и в админку в одном браузере без конфликтов.
Дополнительная защита от спама/перебора: на регистрацию, вход, отправку
сообщений и обращения к ИИ-ассистенту в кабинете клиента тоже действует
`express-rate-limit`, как и в остальном API.
## Резервное копирование базы данных
```bash
docker compose exec db pg_dump -U topsysops topsysops > backup-$(date +%F).sql
```
Восстановление из бэкапа:
```bash
cat backup-2026-07-25.sql | docker compose exec -T db psql -U topsysops topsysops
```
Рекомендуется добавить это в cron, например раз в сутки.
## Обновление после изменений в коде
```bash
git pull # если используете git
docker compose up -d --build
```
Данные в PostgreSQL хранятся в именованном томе `db_data` и не теряются при
пересборке контейнеров.
## Устранение неполадок
- `docker compose logs -f backend` - если сайт не открывается, чаще всего
проблема видна тут (например, не заполнен `JWT_SECRET`).
- `docker compose logs -f nextjs` - если не открывается главная страница или
блог (например, не может достучаться до бэкенда - тогда секции будут просто
пустыми, а в логе будет `[serverApi] failed to fetch ...`).
- `docker compose logs -f web` - если не выдаётся HTTPS-сертификат, здесь будут
сообщения Caddy (обычно это неверный DNS: домен ещё не указывает на сервер).
- Если меняли `DOMAIN` в `.env`, перезапустите `web`: `docker compose up -d web`.
- **"Too many redirects" / бесконечный редирект** - значит перед контейнером
стоит внешний реверс-прокси со своим TLS-сертификатом (например, Nginx Proxy
Manager - см. раздел выше), а Caddy внутри контейнера всё равно пытается сам
сделать редирект http-&gt;https. Исправляется
так: в `.env` поставьте `DOMAIN=http://ваш-домен` (со схемой `http://`) и
перезапустите `web` (`docker compose up -d web`) - это отключит
автоматический HTTPS/редирект в Caddy, он будет просто отдавать http, а
шифрование останется на внешнем nginx. Также проверьте, что внешний nginx
передаёт заголовки `Host`, `X-Forwarded-Proto` и `X-Real-IP`/`X-Forwarded-For`.
- Полный сброс базы (например, во время тестов): `docker compose down -v`
удалит и данные - используйте только осознанно.
## Безопасность на будущее
- Не открывайте порт 5432 (PostgreSQL) наружу - в текущей конфигурации он и так
не проброшен на хост, доступен только другим контейнерам.
- Ограничьте доступ по SSH и настройте `ufw` (`ufw allow 80,443,OpenSSH`, затем
`ufw enable`). Если используете сценарий с внешним nginx и внутренним портом
вроде 7778 (`HTTP_PORT=7778`), этот порт наружу открывать не нужно - трафик
между nginx и контейнером идёт локально, через него наружу открыты только
80/443 самого nginx.
- `express-rate-limit` уже ограничивает количество попыток входа в админку и
количество отзывов с одного IP - это защита от перебора паролей и спама, но
не полноценный WAF; при росте нагрузки рассмотрите Cloudflare или аналог.