Skip to content

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Shortli

Shortli — самостоятельно размещаемый сервис сокращения URL-адресов. Он поддерживает пользовательские псевдонимы, ограничение срока действия ссылок, QR-коды, аналитику переходов, управление учётными записями, инструменты модерации и публичный API.

Локальная разработка

Порты среды разработки намеренно не используют распространённые диапазоны 5173, 3000 и 8080:

  • frontend: http://127.0.0.1:43177;
  • backend: http://127.0.0.1:43178;
  • порт PostgreSQL на хосте: 5001.

Сначала запустите PostgreSQL, а затем приложения в отдельных терминалах:

docker-compose up -d postgres
cd backend
go run ./cmd/shortliService
cd frontend
npm ci
npm run dev

В режиме разработки backend читает backend/.env. Если этого файла нет, создайте его на основе backend/.env.example.

Развёртывание в Docker

Полный стек состоит из PostgreSQL, API на Go и веб-шлюза Nginx. Шлюз раздаёт SPA, проксирует запросы /api/*, а остальные одноуровневые пути передаёт обработчику перенаправлений.

  1. Скопируйте .env.example в .env.
  2. Замените все тестовые секреты независимыми случайными значениями. POSTGRES_USER используется только для инициализации и миграций, а DATABASE_USER — постоянным процессом API; эти имена должны различаться. Если DATABASE_PASSWORD оставить пустым, Compose создаст отдельный случайный пароль приложения в закрытом Docker volume.
  3. Укажите итоговый публичный адрес в PUBLIC_BASE_URL и FRONTEND_ORIGIN.
  4. Для публичного домена используйте только HTTPS и задайте COOKIE_SECURE=true. Backend отклонит небезопасную production-конфигурацию.
  5. Соберите и запустите стек:
docker-compose up -d --build
docker-compose ps

По умолчанию шлюз привязан к 127.0.0.1:43176. Благодаря этому Shortli может работать рядом с другими проектами и находиться за Caddy, Nginx, Traefik или управляемым туннелем. Внешний прокси должен завершать TLS-соединения и перенаправлять трафик на этот адрес. Не открывайте PostgreSQL в публичную сеть.

Перед развёртыванием обновления создайте резервную копию:

.\ops\backup.ps1
docker-compose up -d --build
docker-compose ps

Логи контейнеров ротируются: для каждого сервиса хранится до пяти файлов размером не более 10 МБ. Исходные IP-адреса в журнал запросов не попадают: вместо них записывается стабильный псевдоним client_id, вычисленный с помощью ANALYTICS_SALT.

Миграции базы данных

SQL-миграции находятся в backend/internal/database/migrations и встроены в исполняемый файл backend. Compose сначала идемпотентно создаёт ограниченную роль приложения, затем запускает одноразовый контейнер migrate с ролью владельца базы и только после его успешного завершения запускает API. Постоянный backend не получает права суперпользователя, создания ролей, баз данных или схемы.

Применённые версии записываются в таблицу schema_migrations. Консультативная блокировка PostgreSQL не позволяет нескольким экземплярам одновременно выполнять миграции, поэтому параллельный запуск контейнеров безопасен. Миграции работают только вперёд: перед установкой новой версии создайте резервную копию, а для отката схемы или данных восстановите эту копию.

Никогда не изменяйте уже применённую миграцию. Создавайте новый .sql-файл с лексикографически более поздним именем.

Резервное копирование и восстановление

Создание сжатой резервной копии PostgreSQL:

.\ops\backup.ps1

Копии записываются в backups/ и исключены из Git. Переносите их на другой компьютер или в объектное хранилище: копия на том же диске не защищает от аварии сервера.

Для восстановления необходимо явно подтвердить разрушительную операцию:

.\ops\restore.ps1 -Backup .\backups\shortli-YYYYMMDD-HHMMSS.dump -ConfirmDatabaseReset

На Linux используйте ops/backup.sh и следующую команду восстановления:

CONFIRM_DATABASE_RESET=yes ./ops/restore.sh backups/shortli-file.dump

Рекомендуемое расписание:

  • создавать копию базы данных каждые 24 часа;
  • сразу переносить новую копию за пределы сервера;
  • хранить 7 ежедневных, 4 еженедельные и 6 ежемесячных копий;
  • не реже раза в месяц выполнять тестовое восстановление.

Проверка состояния, метрики и оповещения

  • GET /api/health/live подтверждает, что процесс работает.
  • GET /api/health/ready проверяет PostgreSQL и показывает состояние надёжной очереди переходов.
  • GET /api/metrics отдаёт метрики процессов, HTTP-запросов и очереди в формате Prometheus. Передавайте заголовок Authorization: Bearer <METRICS_TOKEN>.

Пример:

Invoke-WebRequest http://127.0.0.1:43176/api/metrics `
  -Headers @{ Authorization = "Bearer $env:METRICS_TOKEN" }

Рекомендуемые первоначальные оповещения:

  • endpoint готовности недоступен дольше двух минут;
  • значение shortli_click_spool_pending остаётся выше нуля пять минут;
  • shortli_click_spool_bytes превышает 80% от shortli_click_spool_max_bytes;
  • shortli_click_buffer_depth превышает 80% от shortli_click_buffer_capacity;
  • shortli_click_events_dropped_total увеличился хотя бы на единицу;
  • доля ответов 5xx превышает 2% за десять минут;
  • свободное место на диске опустилось ниже 15%;
  • успешная резервная копия не появлялась более 30 часов.

Подробные события переходов хранятся в течение ANALYTICS_RETENTION_DAYS дней, а обработанные обращения модерации — в течение REPORT_RETENTION_DAYS дней. Общий счётчик переходов сохраняется у ссылки и после удаления подробных событий.

Надёжная очередь аналитики

Путь перенаправления не выполняет файловых операций: событие помещается в ограниченный буфер размером CLICK_BUFFER_CAPACITY, после чего отдельный writer записывает его в shortli-click-spool с fsync. Размер spool вычисляется один раз при старте и затем поддерживается O(1)-счётчиками, поэтому рост очереди при аварии PostgreSQL не замедляет редиректы. Сохранённые файлы переживают перезапуск контейнера и автоматически повторяются. Каждое событие имеет уникальный ключ в базе данных, поэтому сбой между фиксацией транзакции и удалением файла не удваивает переход.

При штатном завершении API прекращает принимать запросы, writer переносит буфер на диск, а обработчики пытаются записать оставшиеся события в PostgreSQL. При аварийном завершении процесса могут потеряться только события, которые ещё оставались в памяти; это осознанный компромисс в пользу доступности редиректов.

Размер очереди жёстко ограничен параметром CLICK_SPOOL_MAX_BYTES (по умолчанию 256 МиБ), поэтому длительная недоступность PostgreSQL не заполнит весь диск. После достижения лимита перенаправления продолжают работать, но новые события аналитики отбрасываются и отражаются в метрике shortli_click_events_dropped_total.

Защита от злоупотреблений

Публичная страница /report принимает сообщения о фишинге, вредоносном ПО, спаме, выдаче себя за другое лицо и других опасных сценариях. Обращения:

  • ограничиваются по сетевому адресу;
  • не дублируются, пока находятся на рассмотрении;
  • защищены honeypot-полем от автоматической отправки;
  • хранят односторонний хеш IP-адреса вместо исходного адреса.

Владелец, администраторы и техподдержка рассматривают обращения на странице /stats. Обращение можно отклонить, отметить рассмотренным, приостановить ссылку либо одновременно приостановить её и заблокировать домен назначения. Заблокированный домен и все его поддомены нельзя использовать для новых ссылок, пока владелец или администратор не снимет блокировку.

На этой же странице действует ролевая модель доступа:

  • владелец имеет полный доступ и управляет всеми ролями;
  • администратор управляет сервисом и назначает администраторов, техподдержку и пользователей, но не владельцев;
  • техподдержка работает со списком ссылок и очередью жалоб без доступа к управлению командой;
  • пользователь управляет только своими ссылками и не имеет доступа к администрированию.

Для создания ссылок, аутентификации и отправки обращений действуют отдельные ограничения частоты запросов. Ответ 429 содержит Retry-After и заголовки текущего лимита. Валидатор URL также отклоняет учётные данные в адресе, схемы кроме HTTP/HTTPS, localhost, частные адреса и зарезервированные псевдонимы.

Текущая Docker-конфигурация рассчитана на один экземпляр backend, поэтому лимиты хранятся в памяти процесса. Перед горизонтальным масштабированием перенесите их в общий Redis-совместимый стор; иначе каждый экземпляр будет считать лимит независимо.

Проверка перед выпуском

Перед каждым выпуском выполните локальные проверки:

cd backend
go test -race ./...
go vet ./...
go run honnef.co/go/tools/cmd/staticcheck@v0.7.0 ./...
go run golang.org/x/vuln/cmd/govulncheck@v1.7.0 ./...

cd ..\frontend
npm ci
npm audit --audit-level=high
npm run lint
npm run build

Эти же проверки выполняются в GitHub Actions. CI также запускает интеграционный сценарий с PostgreSQL, ищет секреты через Gitleaks, проверяет Compose и сканирует готовые контейнеры через Trivy. Внешние действия закреплены по полным commit SHA, а Dependabot еженедельно предлагает контролируемые обновления Go, npm, Docker и GitHub Actions.

Первый владелец

На чистой базе задайте в .env случайный ADMIN_BOOTSTRAP_TOKEN длиной не менее 32 символов и запустите стек. Затем один раз выполните запрос:

$body = @{
  email = "admin@example.com"
  password = "replace-with-a-strong-password-42"
  token = $env:ADMIN_BOOTSTRAP_TOKEN
} | ConvertTo-Json
Invoke-RestMethod -Method Post -Uri http://127.0.0.1:43176/api/admin/bootstrap `
  -ContentType "application/json" -Body $body

После успешного ответа удалите ADMIN_BOOTSTRAP_TOKEN из .env и перезапустите backend. Повторный bootstrap невозможен, пока в базе существует хотя бы один владелец. Последнего владельца нельзя удалить или лишить этой роли.

Доверенные прокси и адрес клиента

TRUSTED_PROXY_CIDRS содержит только подсети Nginx и внешних прокси, которым разрешено передавать клиентский IP. Backend разбирает X-Forwarded-For справа налево и игнорирует заголовок, если непосредственный отправитель не входит в этот список. Для Docker подходит сеть 172.16.0.0/12; при отдельном reverse proxy добавьте его точную подсеть. Никогда не указывайте 0.0.0.0/0 или ::/0.

Политика учётной записи

Удаление учётной записи удаляет и все принадлежащие ей короткие ссылки. Смена пароля завершает все сеансы и отзывает активные API-ключи, поэтому после неё нужно войти снова и при необходимости создать новые ключи. Миграция 0004 один раз завершит существующие сеансы, поскольку старые значения были сохранены без хеша.

About

Open-source link shortener

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages