Files
WhatIDo/README.md

284 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (для публикации по ссылке)
### Запуск
```bash
# 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-сети (наружу не публикуется)
Управление:
```bash
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` покажет статус:
```bash
docker exec -it whatido-tailscale-1 tailscale status
```
Если авторизация по какой-то причине не прошла, выполните вход вручную:
```bash
docker exec -it whatido-tailscale-1 tailscale up --hostname=whatido
# откроется ссылка вида https://login.tailscale.com/a/... — войдите в браузер
```
### 2. Включите функции и найдите адрес
После авторизации узел получит имя вида `whatido` и адрес:
```bash
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
Контейнер при старте сам выполняет:
```bash
tailscale funnel --bg --yes https://127.0.0.1:3443
```
(`funnel` включает и serve-часть; флаг `--bg` — работа в фоне, `--yes` — не спрашивать подтверждения.)
Если сервис уже запущен и команды в compose не отработали (например, функции только что включили в консоли), выполните вручную:
```bash
docker exec whatido-tailscale-1 tailscale funnel --bg --yes https://127.0.0.1:3443
```
Проверить состояние:
```bash
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:
```bash
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 требует привязать карту).
Узнать текущий адрес для доступа:
```bash
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 не включается, туннель стартует сразу, как в базовой схеме.
Проверка:
```bash
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/`
- Восстановить из файла бэкапа
Также доступны скрипты на хосте:
```bash
./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/ # локальные бэкапы
```