Files

20 KiB
Raw Permalink Blame History

WhatIDo

Учётная система для образовательного центра: журнал посещений, проектные работы воспитанников, галерея групп, откреплённые файлы и публичные страницы-витрины (share-ссылки).

Возможности

  • Журнал записей — отметки о занятиях с фото и прикреплёнными файлами (проектные работы), мягкое удаление и корзина; в режиме карточек у записи показывается бейдж группы поверх фото и иконка статуса AI-проверки
  • AI-проверка (воркер) — фоновый авто-чек текста записей (worker.js); статус каждой записи (очередь / проверка / проверено / пропущено / ошибка) отображается компактной иконкой в журнале с всплывающей подсказкой
  • Файлы — централизованный раздел со всеми загруженными файлами, фильтры (имя воспитанника, группа, даты, поиск) и вкладка «Откреплённые»
  • Группы — учебные группы, расписание (день недели, время), фотохроника группы
  • Воспитанники — справочник с привязкой к группам
  • Share-ссылки — публичные страницы-витрины с выбором группы / воспитанника / диапазона дат
  • Дашборд — статистика, активные группы, активность за 14 дней, последние записи, топ воспитанников
  • Резервное копирование — экспорт/импорт полного дампа (БД + файлы) в tar.gz
  • Настройки — тексты футера, анти-спам интервал
  • Публикация через Tailscale — приложение открывается по постоянному адресу https://whatido.<tailnet>.ts.net без проброса портов, внешнего IP и reverse-proxy

Технологии

  • Node.js + Express
  • PostgreSQL (pg)
  • Multer (загрузка файлов), Tar (бэкапы)
  • Lucide (иконки UI)
  • Docker / Docker Compose
  • Tailscale (Serve / Funnel) — публикация по HTTPS

Быстрый старт

Требования

  • Linux-хост с Docker и плагином docker compose
  • Free порта 443 на хосте (его займёт tailscale для Funnel)
  • Аккаунт Tailscale (для публикации по ссылке)

Запуск

# 1) создайте .env из примера и задайте свои пароли
cp .env.example .env
$EDITOR .env

# 2) соберите и поднимите стек
docker compose up -d --build

После старта (без публикации через tailscale):

  • HTTP http://localhost:3003 — редирект на HTTPS
  • HTTPS https://localhost:3443 — приложение (самоподписанный сертификат, примите предупреждение браузера)
  • PostgreSQL — доступен только внутри docker-сети (наружу не публикуется)

Управление:

docker compose ps            # статус
docker compose logs -f app   # логи приложения
docker compose down          # остановка (данные сохраняются)

Приложение не запустится без ADMIN_PASSWORD (защита от пароля по умолчанию). DB_PASSWORD задаёт пароль пользователя app в PostgreSQL.

Конфигурация

Переменные окружения (.env):

Переменная По умолчанию Назначение
ADMIN_PASSWORD — (обязательно) Пароль администратора (X-Admin-Token). Без него сервер не стартует
DB_PASSWORD — (обязательно) Пароль пользователя app в PostgreSQL

Пример .env (в репозитории — .env.example):

ADMIN_PASSWORD=сложный-пароль
DB_PASSWORD=случайная-длинная-строка

DB_PASSWORD подставляется в docker-compose.yml в POSTGRES_PASSWORD и DATABASE_URL. Если БД уже была инициализирована ранее, значение DB_PASSWORD должно совпадать с фактическим паролем пользователя app в БД (иначе приложение не подключится).

Имя узла Tailscale задаётся в docker-compose.yml (tailscale.hostname, по умолчанию whatido).

Публичный доступ через Tailscale

Стек не требует внешнего IP и проброса портов: контейнер tailscale запускается с network_mode: host, входит в вашу tailnet-сеть и через Serve открывает приложение внутри tailnet, а через Funnel — в публичном интернете.

Цепочка:

Интернет / tailnet → https://whatido.<tailnet>.ts.net   (TLS от Tailscale)
                 → localhost:443 контейнера tailscale
                 → https://127.0.0.1:3443               (приложение, самоподписанный cert)

Контейнер доверяет самоподписанному сертификату приложения через SSL_CERT_FILE=/etc/tailscale/app-certs/cert.pem (файл монтируется из ./certs/cert.pem).

1. Вход в tailnet при первом запуске

При первом старте контейнер автоматически выполняет tailscale up и печатает ссылку для авторизации — откройте её в браузере и добавьте устройство в аккаунт. Ввести устройство вручную там не нужно: tailscale status покажет статус:

docker exec -it whatido-tailscale-1 tailscale status

Если авторизация по какой-то причине не прошла, выполните вход вручную:

docker exec -it whatido-tailscale-1 tailscale up --hostname=whatido
# откроется ссылка вида https://login.tailscale.com/a/... — войдите в браузер

2. Включите функции и найдите адрес

После авторизации узел получит имя вида whatido и адрес:

docker exec -it whatido-tailscale-1 tailscale status
# что-то вроде: whatido.taile47725.ts.net 100.x.x.x  receiver  online

Чтобы публиковать сайт, для tailnet должны быть включены:

  • HTTPS Certificates — автоматически выдается при первом Serve/Funnel
  • Serve (доступ из tailnet) и Funnel (доступ из интернета) — включаются в админ-консоли Tailscale, например по прямой ссылке на узел: https://login.tailscale.com/f/serve?node=<node-id> и https://login.tailscale.com/funnel?node=<node-id> (id узла берётся из tailscale status)

3. Запуск Serve / Funnel

Контейнер при старте сам выполняет:

tailscale funnel --bg --yes https://127.0.0.1:3443

(funnel включает и serve-часть; флаг --bg — работа в фоне, --yes — не спрашивать подтверждения.)

Если сервис уже запущен и команды в compose не отработали (например, функции только что включили в консоли), выполните вручную:

docker exec whatido-tailscale-1 tailscale funnel --bg --yes https://127.0.0.1:3443

Проверить состояние:

docker exec whatido-tailscale-1 tailscale funnel status
# https://whatido.<tailnet>.ts.net/  (Funnel on)

4. Готово

  • Из любого устройства вашей tailnet: https://whatido.<tailnet>.ts.net/
  • Из интернета (при включённом Funnel): тот же адрес
  • Админка: https://whatido.<tailnet>.ts.net/admin

Важные замечания

  • Порт 443 на хосте должен быть свободен — tailscale слушает его напрямую (поэтому у сервиса network_mode: host, а приложение опубликовано на 127.0.0.1:3003/3443).

  • Сертификат приложения: контейнер приложения генерирует self-signed cert.pem при сборке образа (это не секрет — публичный сертификат). Он монтируется в tailscale через ./certs/cert.pem. Если образ приложения пересобирали впервые на новом хосте — скопируйте сертификат и перезапустите tailscale:

    docker compose up -d --build app
    docker cp $(docker compose ps -q app):/app/certs/cert.pem certs/cert.pem
    docker compose up -d tailscale
    
  • /lib/modules монтируется в контейнер tailscale, чтобы на LC/дистрибутивах без авто-загрузки модулей корректно инициализировался netfilter (иначе tailscaled падает с Table does not exist и узел мигает online/offline).

  • Ограничения сети: если у провайдера нет глобального IPv6 и соединения к ПК-ядрам Tailscale нестабильные (CGNAT), отвечать наружу узел может с перебоями — классический признак: tailscale status показывает online, а из интернета URL не открывается. Внутри tailnet сайт работает всегда.

Альтернативная публикация (Caddy)

Исторически проект публиковался через reverse-proxy Caddy + Let's Encrypt (файлы Caddyfile.example, закомментированный сервис в старых версиях compose). Если нужно классическое публичное HTTPS на собственном домене с проброшенными портами 80/443 — этот вариант остаётся возможным: раскомментируйте/восстановите сервис caddy, укажите домен в Caddyfile и уберите сетевые блокировки. По умолчанию сейчас рекомендуется Tailscale-схема выше.

Публикация наружу через Cloudflare (Quick Tunnel)

Помимо Tailscale, стек умеет публиковать приложение через контейнер cloudflared (сервис в docker-compose.yml). Quick Tunnel даёт случайный публичный адрес *.trycloudflare.com через Cloudflare — без своего домена, банковской карты и проброса портов (подойдёт, если Zero Trust требует привязать карту).

Узнать текущий адрес для доступа:

docker compose logs cloudflared | grep trycloud

Опциональный WireGuard для эгресса Cloudflare

cloudflared собирается из Dockerfile.cloudflared, в который добавлена поддержка опционального WireGuard — полезно, когда Cloudflare не работает с прямого IP хоста, и исход туннеля нужно пустить через VPN.

  • Положите рабочий конфиг провайдера в wg/wg0.conf (папка wg/ монтируется в контейнер как /etc/wireguard). Пример-заглушка уже лежит в wg/wg0.conf — замените значения на свои.
  • Если wg/wg0.conf есть → контейнер сначала поднимает WireGuard и ждёт handshake (весь эгресс Cloudflare идёт через VPN). Если handshake не установился за WG_HANDSHAKE_TIMEOUT сек (по умолчанию 60) — fallback: туннель стартует напрямую без VPN, а в логах пишется предупреждение (сайт не лежит из-за недоступного VPN-провайдера).
  • Если wg0.conf нет или папка пустая → VPN не включается, туннель стартует сразу, как в базовой схеме.

Проверка:

docker compose logs -f cloudflared        # в идеале сначала "WireGuard is up", затем URL
docker compose exec cloudflared wg show   # есть handshake — VPN поднят

Примечания:

  • wg/wg0.conf содержит приватный ключ, поэтому папка wg/ добавлена в .gitignore (в репозиторий не попадёт).
  • При полном туннеле (AllowedIPs = 0.0.0.0/0) существующий маршрут docker-сети сохраняется, поэтому cloudflared по-прежнему достаёт app локально, а наружу уходит через wg0. Если вдруг app станет недоступен из-за VPN — переключите CLOUDFLARE_TUNNEL_URL в .env на docker-шлюз (http://<gateway-ip>:3003).

Адрес Quick Tunnel меняется при каждом перезапуске контейнера cloudflared. Для постоянного адреса используйте именованный туннель (для этого в start-cloudflared.sh замените последнюю команду на exec cloudflared --no-autoupdate tunnel run --token "$CLOUDFLARE_TUNNEL_TOKEN" и задайте токен; токен — из Cloudflare Zero Trust Networks → Tunnels) или схему через Tailscale выше.

Структура данных

  • groups — группы, расписание (day_of_week, time_start, time_end)
  • students — воспитанники
  • entries — записи журнала (фото photo_path, описание, мягкое удаление deleted_at)
  • project_files — файлы записей (token, path, привязка entry_id, отметка detached_at)
  • group_photos — фотохроника групп
  • share_links — публичные ссылки-витрины
  • settings — пары ключ/значение (анти-спам интервал, футер)

Схема инициализируется при первом запуске из db/init.sql; миграции существующей БД — в db/migration.sql.

Хранилище файлов

Загруженные фото и файлы хранятся в каталоге uploads/ на хосте и монтируются в контейнер (./uploads:/app/uploads). Это даёт прямой доступ к данным из-под хост-системы. Данные БД хранятся в именованном томе pgdata.

Бэкапы

В админке (Настройки → Бэкап) можно:

  • Скачать полный бэкап — tar.gz, содержащий data.json (все таблицы) и uploads/
  • Восстановить из файла бэкапа

Также доступны скрипты на хосте:

./scripts/backup.sh    # дамп БД + фото в backups/whatido-backup-<дата>.tar.gz
./scripts/restore.sh   # восстановление из архива

Безопасность

  • Пароль администратора обязателен (ADMIN_PASSWORD); фолбэка на admin нет.
  • CORS отключён — кросс-доменные запросы к API запрещены.
  • Rate limiting по IP на публичные роуты: POST /api/entries — 10 запросов / 15 мин, загрузка файлов и share-ссылки — 300 / 15 мин.
  • Загрузки ограничены: 30 МБ суммарно на запись, 10 МБ на файл; заблокированы опасные расширения (.html, .js, .svg, .xml, .exe и др.); SVG не отдаётся inline.
  • Restore проходит полную валидацию данных бэкапа; удаление файлов ограничено каталогом uploads/.
  • Заголовки: helmet — X-Frame-Options, nosniff, HSTS, Referrer-Policy.
  • Порт БД 5432 наружу не публикуется (доступ только внутри docker-сети).
  • TLS: снаружи HTTPS терминируется Tailscale (сертификат Let's Encrypt для *.ts.net); между tailscale и приложением используется самоподписанный сертификат приложения.

Основные API

Метод Путь Назначение
GET /api/entries Записи журнала (с фильтрами)
POST /api/entries Создать запись (photo + files)
PUT/DELETE /api/entries/:id Обновить / мягко удалить
GET /api/files Все файлы (фильтры)
GET /api/files/detached Откреплённые файлы
POST /api/files/:id/detach Открепить файл от записи
GET /api/files/:token Скачать/показать файл по токену (публично)
GET /api/groups Список групп
POST/PUT/DELETE /api/groups/:id(?) CRUD групп
GET/POST /api/groups/:id/photos Фотохроника группы
GET/POST/PUT/DELETE /api/share/..., /api/links Публичные ссылки
GET /api/backup Скачать бэкап
POST /api/restore Восстановить из бэкапа
GET /api/dashboard, /api/stats Статистика

Защищённые админ-маршруты требуют заголовок X-Admin-Token с ADMIN_PASSWORD.

Структура проекта

├── docker-compose.yml   # сервисы: app + db + tailscale
├── .env.example         # шаблон переменных окружения
├── Dockerfile           # сборка образа (Node 20, генерация TLS-сертификата)
├── server.js            # Express-приложение
├── worker.js            # фоновый worker AI-проверки записей
├── certs/               # cert.pem приложения (монтируется в tailscale, в git не хранится)
├── db/
│   ├── init.sql         # схема при первом запуске
│   └── migration.sql    # миграции существующей БД
├── public/              # статика (HTML/CSS/JS админки и витрин)
├── scripts/             # вспомогательные скрипты
├── uploads/             # загруженные файлы (bind-монт, вне git)
└── backups/             # локальные бэкапы