Skip to content

Админ-сервис (admin.partizap.ru)

Админ-панель выделена в отдельный сервис, изолированный от маркетплейса на 4 уровнях:

УровеньМеханизм
СетьmTLS — без клиентского сертификата nginx возвращает 400
ПриложениеAPP_MODE=admin, отдельные routes (marketplace routes недоступны)
БДPostgreSQL roles с column-level grants
СессииRedis DB 4, cookie PARTIZAP_ADMIN_SESSION, SameSite=Strict

ADR: ADR-010, ADR-012

Схема деплоя

VPS 1 (Production, 85.239.48.136)
├── /var/www/partizap/production          # APP_MODE=marketplace, partizap.ru
├── /var/www/partizap/admin-production    # APP_MODE=admin, admin.partizap.ru
└── nginx: mTLS + SSL termination

VPS 2 (DevOps, 192.168.0.5)
├── /var/www/partizap/development         # APP_MODE=marketplace, dev.partizap.ru
├── /var/www/partizap/admin-development   # APP_MODE=admin, dev-admin.partizap.ru
└── nginx: порт 8084 (reverse proxy через VPS 1)
DevProd
URLhttps://dev-admin.partizap.ruhttps://admin.partizap.ru
Branchdevelopmain
DB userpartizap_admin_devpartizap_admin_prod
.envAPP_MODE=admin, REDIS_SESSION_DB=4APP_MODE=admin, REDIS_SESSION_DB=4

Аутентификация: три фактора

ФакторТипУровень
Клиентский сертификат (.p12)УстройствоTLS (Nginx)
Пароль admin_usersЗнаниеApp (PHP)
TOTP-кодУстройство (телефон)App (PHP)

Brute-force защита

МеханизмЗначение
Login lockout3 попытки / 30 мин (по email)
Progressive delays2→2s, 3→5s
Account lockout3 попытки → locked_until +30 мин
TOTP attemptsMax 3 в сессии, затем re-login
TOTP expiry5 минут после ввода пароля
Alert emailКаждая неудачная попытка
New IP alertПри логине с неизвестного IP
Rate limit (login)20 неуд. попыток / 1800s (по IP, для защиты от email-rotation brute force на shared NAT)
Rate limit (global)30 req / 60s

INFO

Lockout привязан к email, а не к паре (IP, email) — иначе пользователи за shared NAT блокировали друг друга, когда один набирал пароль неверно. IP-адрес определяется через ClientIpResolver (см. Архитектура → Client IP и TRUSTED_PROXIES), поэтому заголовок X-Real-IP не подделывается произвольным клиентом.

Операции

Добавление нового админа

Шаг 1: Создать учётную запись в admin_users

bash
# Dev
cd /var/www/partizap/admin-development
./bin/console app:create-admin --email=newadmin@partizap.ru --name="Имя Фамилия"
# Пароль: интерактивный ввод (минимум 16 символов, 3 из 4 классов: a-z, A-Z, 0-9, спецсимволы)

# Prod
ssh serpens@192.168.0.4
cd /var/www/partizap/admin-production
./bin/console app:create-admin --email=newadmin@partizap.ru --name="Имя Фамилия"

Шаг 2: Выпустить клиентский сертификат

На VPS 1 (production):

bash
ssh serpens@192.168.0.4
sudo -i
cd /etc/nginx/ssl/admin-mtls

ADMIN_EMAIL="newadmin@partizap.ru"
ADMIN_NAME="newadmin"

# Генерация ключа и CSR
openssl genrsa -out ${ADMIN_NAME}.key 2048
openssl req -new -key ${ADMIN_NAME}.key \
    -out ${ADMIN_NAME}.csr \
    -subj "/CN=${ADMIN_EMAIL}/O=Partizap"

# Подпись CA (1 год)
openssl x509 -req -days 365 \
    -in ${ADMIN_NAME}.csr \
    -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out ${ADMIN_NAME}.crt

# Упаковка в .p12 для браузера (формат совместимый с Chrome/Linux)
openssl pkcs12 -export \
    -out ${ADMIN_NAME}.p12 \
    -inkey ${ADMIN_NAME}.key \
    -in ${ADMIN_NAME}.crt \
    -name "Partizap Admin (${ADMIN_EMAIL})" \
    -passout pass:SECURE_PASSWORD_HERE \
    -certpbe PBE-SHA1-3DES \
    -keypbe PBE-SHA1-3DES \
    -macalg SHA1

# Убрать промежуточные файлы
rm ${ADMIN_NAME}.csr
chmod 600 ${ADMIN_NAME}.key
chmod 644 ${ADMIN_NAME}.p12

Важно: -certpbe PBE-SHA1-3DES -keypbe PBE-SHA1-3DES -macalg SHA1 — обязательно для совместимости с Chrome/Chromium на Linux. OpenSSL 3.0 по умолчанию использует алгоритмы, которые Chrome не понимает.

Важно: НЕ включать CA cert в .p12 (не использовать -certfile ca.crt), иначе Chrome импортирует его как Intermediate CA вместо клиентского сертификата.

Шаг 3: Передать .p12 файл и пароль

  • Файл .p12 передать лично или через защищённый канал
  • Пароль сообщить отдельно, не в том же канале
  • Не отправлять через мессенджер вместе

Шаг 4: Импорт в браузер

Linux (Chromium/Chrome snap)
bash
# 1. Установить NSS tools (если нет)
sudo apt install libnss3-tools

# 2. Найти NSS DB браузера
#    Для snap Chromium:
find ~/snap/chromium -name "cert9.db" 2>/dev/null
#    Для обычного Chrome:
ls ~/.pki/nssdb/cert9.db

# 3. Импортировать (подставить правильный путь к nssdb)
pk12util -d sql:$HOME/snap/chromium/common/.pki/nssdb \
    -i ~/Downloads/newadmin.p12 \
    -W "ПАРОЛЬ_ОТ_P12"

# 4. Проверить
certutil -d sql:$HOME/snap/chromium/common/.pki/nssdb -L
# Должен показать "Partizap Admin (...)" в списке

# 5. Перезапустить браузер

Внимание: snap-версия Chromium использует изолированный NSS store. Путь ~/.pki/nssdb не работает — нужен ~/snap/chromium/common/.pki/nssdb.

Linux (Firefox)
  1. Settings → Privacy & Security → Certificates → View Certificates
  2. Your Certificates → Import
  3. Выбрать .p12 файл → ввести пароль
  4. Перезапустить Firefox
macOS (Chrome/Safari)
bash
# Через терминал:
security import ~/Downloads/newadmin.p12 -k ~/Library/Keychains/login.keychain-db -P "ПАРОЛЬ"

# Или через Keychain Access:
# 1. Двойной клик на .p12 файл
# 2. Ввести пароль
# 3. Сертификат появится в login keychain

Для Chrome — перезапустить после импорта.

Windows (Chrome/Edge)
  1. Двойной клик на .p12 файл → запустится Certificate Import Wizard
  2. Store Location: Current User → Next
  3. Ввести пароль → Next
  4. "Automatically select the certificate store" → Next → Finish
  5. Перезапустить Chrome

Или через PowerShell:

powershell
Import-PfxCertificate -FilePath .\newadmin.p12 -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String "ПАРОЛЬ" -AsPlainText -Force)
Android (Chrome)
  1. Передать .p12 файл на устройство (через email, облако, USB)
  2. Settings → Security → Encryption & credentials → Install a certificate → VPN & app user certificate
  3. Выбрать .p12 файл → ввести пароль
  4. При заходе на admin.partizap.ru Chrome предложит выбрать сертификат

На Android 11+ путь может быть: Settings → Security → More security settings → Encryption & credentials → Install a certificate.

iOS (Safari/Chrome)
  1. Передать .p12 файл на устройство (AirDrop, email, Files)
  2. Открыть файл → iOS предложит установить профиль
  3. Settings → General → VPN & Device Management → установить профиль
  4. Ввести пароль от .p12
  5. Settings → General → About → Certificate Trust Settings → включить доверие для Partizap Admin CA
  6. Safari: при заходе на admin.partizap.ru предложит выбрать сертификат

Chrome на iOS использует системное хранилище сертификатов, поэтому тоже будет работать после установки профиля.

Шаг 5: Проверить доступ

Открыть в браузере: https://admin.partizap.ru/health

  • Браузер предложит выбрать сертификат → выбрать Partizap Admin (...)
  • Ответ: {"data":{"status":"healthy"}}

Без сертификата → 400 Bad Request: No required SSL certificate was sent

Шаг 6: Настроить TOTP

  1. Войти: POST /admin/auth/login с email и паролем
  2. GET /admin/auth/totp-setup → получить QR-код (provisioning URI)
  3. Отсканировать QR в приложении (Google Authenticator, Authy, 1Password)
  4. POST /admin/auth/totp-confirm с 6-значным кодом
  5. Последующие входы: email+password → TOTP код

Отзыв сертификата

При компрометации устройства или увольнении:

bash
ssh serpens@192.168.0.4
sudo -i
cd /etc/nginx/ssl/admin-mtls

# Инициализация CRL (только при первом отзыве)
touch index.txt
echo 01 > crlnumber

# Отозвать сертификат
openssl ca -revoke newadmin.crt \
    -keyfile ca.key -cert ca.crt \
    -config <(echo -e "[ca]\ndefault_ca=CA_default\n[CA_default]\ndatabase=index.txt\ncrlnumber=crlnumber")

# Сгенерировать CRL
openssl ca -gencrl \
    -keyfile ca.key -cert ca.crt \
    -out ca.crl \
    -config <(echo -e "[ca]\ndefault_ca=CA_default\n[CA_default]\ndatabase=index.txt\ncrlnumber=crlnumber\ndefault_crl_days=3650")

# Включить CRL в nginx (раскомментировать в admin-partizap конфиге)
# ssl_crl /etc/nginx/ssl/admin-mtls/ca.crl;

# Деплой через nginx repo
cd ~/Projects/Partizap/nginx
# Раскомментировать ssl_crl, commit, push → CI deploy

# Или экстренно:
nginx -t && systemctl reload nginx

После reload — отозванный сертификат мгновенно перестаёт работать.

Перевыпуск сертификата

Клиентские сертификаты выдаются на 1 год. За 2 недели до истечения:

bash
# Проверить срок
openssl x509 -in serpens.crt -noout -enddate

# Перевыпустить (те же команды, что в Шаг 2)

Деактивация админа (без отзыва сертификата)

bash
cd /var/www/partizap/admin-production
./bin/console app:create-admin --email=admin@partizap.ru --name="Admin"
# Затем в БД: UPDATE admin_users SET is_active = false WHERE email = '...';

PostgreSQL роли

РольНазначение
partizap_ownerDDL, миграции. Владеет всеми таблицами
partizap_dev / partizap_prodMarketplace runtime. Полный доступ ко всем таблицам, кроме admin_users, admin_sessions, admin_audit_log
partizap_admin_dev / partizap_admin_prodAdmin runtime. Column-level grants

Cross-side CLI-команды

Маркетплейс-роль не видит admin-таблицы, а admin-роль не видит marketplace-таблицы. Консольные команды, которым нужны обе стороны (например, app:clean-expired-sessions — чистит user_sessions и admin_sessions), не могут быть запущены из одного деплоя и обязаны gracefully skip недоступную таблицу при SQLSTATE[42501].

Паттерн: одна команда — два запуска по cron, по одному на каждое развёртывание, каждый чистит свою таблицу, другую пропускает с note. Реальное расписание: docs/dev/architecture.md#очистка-истёкших-сессий и deploy/cron/partizap-session-gc.{dev,prod} в server-репо.

Column-level grants для admin role

ТаблицаSELECTINSERTUPDATEDELETE
usersALL-is_active only-
productsALL-status, published_at, rejection_reasonYes
admin_usersALLYesYesNo (деактивация через is_active)
admin_sessionsALLYesYesYes
admin_audit_logALLYesNo (immutable)No (immutable)
categories, car_*, regions, cities, districts, metro_stationsALLYesYesYes
ОстальныеALL---

Redis DB layout

DBНазначение
0Marketplace sessions + Doctrine metadata cache (dc2_*)
1Application cache (categories, car_makes, geo, products)
2Queues
3Rate limits
4Admin sessions

Nginx конфигурация

Версионируется в git@127.0.0.1:d_vyaznikov/nginx.git:

ФайлСерверОписание
main/sites-available/admin-partizapVPS 1admin.partizap.ru (mTLS + PHP-FPM) + dev-admin reverse proxy
dev/sites-available/admin-partizap-devVPS 2dev-admin backend (порт 8084)
main/nginx.confVPS 1map $ssl_client_s_dn → $ssl_client_cn, log_format admin_access

Не редактировать конфиги напрямую на серверах. Push в nginx repo → CI deploy автоматически.

Cache flush после деплоя

При изменении entity/миграций — полный сброс кеша:

bash
DEPLOY_DIR=/var/www/partizap/production  # или development, admin-production, admin-development
PREFIX=prod  # или dev

# Doctrine metadata (Redis DB 0)
redis-cli -n 0 KEYS "dc2_*" | xargs -r redis-cli -n 0 DEL

# DI container + Doctrine proxies
sudo rm -rf ${DEPLOY_DIR}/var/cache/* ${DEPLOY_DIR}/var/doctrine/proxies/*
sudo chown -R www-data:www-data ${DEPLOY_DIR}/var/

# Regenerate proxies (production only, APP_DEBUG=false)
cd ${DEPLOY_DIR}
sudo -u www-data php -r 'require "vendor/autoload.php"; $s=require "config/app.php"; $d=require "config/doctrine.php"; $em=$d($s); $em->getProxyFactory()->generateProxyClasses($em->getMetadataFactory()->getAllMetadata());'

# OPcache
sudo systemctl restart php8.3-fpm

# Application cache (если изменились справочные данные)
# redis-cli -n 1 KEYS "${PREFIX}:*" | xargs -r redis-cli -n 1 DEL

API endpoints (admin mode)

Auth (public, rate limited)

MethodPathОписание
POST/admin/auth/loginEmail + password → session (или totp_required)
POST/admin/auth/totp-verifyTOTP код → полная сессия

Auth (authenticated)

MethodPathОписание
GET/admin/auth/meТекущий admin user
POST/admin/auth/logoutЗавершить сессию
GET/admin/auth/totp-setupПолучить QR для настройки TOTP
POST/admin/auth/totp-confirmПодтвердить TOTP setup

Admin CRUD (authenticated + admin middleware)

Все существующие /admin/* endpoints (stats, users, products, reference data CRUD).

Store read-only (authenticated)

GET-only endpoints из /store/* для просмотра справочных данных (categories, cars, geo).

Frontend (Nuxt admin app)

Admin frontend — отдельное Nuxt-приложение в монорепо partizap-frontend/apps/admin/.

ADR: ADR-011

Архитектура

partizap-frontend/
├── apps/main/     ← marketplace (partizap.ru)
├── apps/admin/    ← admin panel (admin.partizap.ru) ← THIS
└── packages/shared/  ← shared entities, API client, utils
  • SPA-only (ssr: false) — админка не нуждается в SSR
  • Shared code через symlinks: apps/admin/app/entities/*packages/shared/
  • Отдельный auth flow: useAdminAuthStore + useAdminApi (cookie PARTIZAP_ADMIN_SESSION)
  • Dev server: https://localhost:3001 (main на 3000)

Auth flow

/login          → email + password
                  ↓ requires_totp=true
/login/totp     → 6-digit TOTP code (3 попытки, 5 мин)
                  ↓ totp_enabled=false (первый вход)
/login/totp-setup → QR-код + подтверждение
                  ↓ success
/               → dashboard

Разделы

РазделRouteОписание
Dashboard/Статистика: пользователи, продукты, модерация
Products/admin/productsМодерация: approve/reject, polling 30s
Users/admin/usersСписок, блокировка, поиск, фильтры
References/admin/referencesСправочники: авто, категории, гео

Команды разработки

bash
pnpm dev:admin                        # Dev server (https://localhost:3001)
pnpm build:admin                      # Production build
pnpm --filter @partizap/admin run final   # typecheck + test + lint

CI/CD

TODO — admin Dockerfile + pipeline job для деплоя на admin.partizap.ru. На текущий момент бэкенд деплоится вручную, фронт через monorepo CI (main app only).