Защита
Как устроена защита
Десять фактов о системе, которую мы отдаём заказчику. Каждый можно проверить: командой в терминале или по файлу и строке в коде — код с первого дня лежит в вашем репозитории.
-
01
Пароли хранятся хэшем bcrypt, и ответ занимает одинаковое время
В базе нет паролей — только bcrypt-хэш, из которого пароль не восстановить. Если такого логина нет, сервер всё равно выполняет сравнение с подставным хэшем: по скорости ответа нельзя узнать, существует ли адрес.
Проверьтеpsql -c 'SELECT LEFT("passwordHash",7) FROM "User" LIMIT 3' → каждая строка начинается с $2b$10$ — это bcrypt, не парольapps/crm/src/auth.ts:11 (DUMMY_HASH) · :107–109 (bcrypt.compare с подставным хэшем)
-
02
Пять неверных паролей — и вход закрыт на пятнадцать минут
Пять ошибок на один адрес или двадцать с одного IP за 15 минут — вход закрыт. Попытка в закрытом окне не продлевает его: пять неудачных попыток остаются записанными, а через пятнадцать минут вход снова открыт. Экран говорит «вход закрыт», а не «неверный пароль», чтобы сотрудник не продолжал.
Проверьте в своей системепять раз неверный пароль на один адрес → шестая попытка получает код locked, а не «неверный пароль»
apps/crm/src/auth.ts:90 (окно 15 минут) · :96–104 (счётчики и порог 5 / 20) · :15–17 (код locked)
-
03
Каждый вход записан: кто, когда, с какого адреса
Успешный вход в CRM — строка в журнале с именем, временем и IP-адресом. Вход с верным паролем, но без права входа в CRM, тоже попадает в журнал. Адрес берёт наш прокси-сервер из самого соединения, а не из заголовка, который прислал клиент.
Проверьте в своей системеЖурнал → действие login → у каждой строки в поле meta стоит ipapps/crm/src/auth.ts:139–143 (запись входа с ip) · :124–131 (login.denied: верный пароль без права входа в CRM) · :37–50 (X-Real-IP от прокси)
-
04
Права читаются из базы при каждом запросе
Роль, права и статус сотрудника сервер читает из базы на каждом запросе, а не из сохранённой сессии. Отключённый сотрудник вылетает на следующем клике — не через час и не завтра.
Проверьте в своей системеотключите сотрудника в карточке → его следующий клик в CRM заканчивается на экране входаapps/crm/src/auth.ts:56–58 · :65–76 (findUnique на каждом запросе, null при isActive = false) · apps/crm/src/auth.config.ts:17–19
-
05
Двадцать пять отдельных прав, три роли
Права — не «админ и остальные», а 25 отдельных ключей: продажа, сторно, правка платежей, выплаты, журнал, тарифы и так далее. Три роли: владелец, менеджер, сотрудник. Новый сотрудник не получает ничего, пока владелец не даст. Проверяет сервер в каждом действии — спрятанная кнопка защитой не считается.
Проверьтеgrep -c 'key: "' apps/crm/src/lib/permissions.ts → 25apps/crm/src/lib/permissions.ts:5–79 (25 ключей) · :83 (три роли) · :129–132 (STAFF: []) · :145–148 (hasPerm) · apps/crm/src/app/dashboard/bar/actions.ts:25–30 (проверка в действии)
-
06
Журнал: база данных сама запрещает менять и удалять записи — год
Записи журнала приложение может только создавать и читать: на уровне СУБД у него отозваны UPDATE и DELETE. Удалить можно только записи старше года, только владелец (проверяет приложение), только через функцию базы — и эта функция сама оставляет запись о том, сколько строк удалено и до какой даты.
Проверьтеpsql -c '\dp "AuditLog"' → у роли proform_app в списке прав нет ни w (update), ни d (delete)
packages/db/prisma/migrations/20260630045232_audit_append_only/migration.sql:4 · 20260806210000_audit_prune_one_year/migration.sql:37–42, 45–55 · apps/crm/src/app/dashboard/audit/actions.ts:22 (только FOUNDER)
-
07
Четыре пользователя базы, у каждого своя граница
Владелец схемы, приложение, только чтение — и отдельный пользователь у памяти ИИ, который видит только свою схему. Приложение работает с данными, но не может менять структуру таблиц; аналитика читает и ничего не пишет. У каждого свой пароль; пароли лежат в файле окружения на сервере, не в коде.
Проверьтеpsql -c '\du' → proform_owner · proform_app · proform_readonly · brain_svc
infra/postgres/init/01-init.sh:11–20 (proform_app: только DML) · :22–27 (proform_readonly: только SELECT) · infra/CRONTAB.md:174 (brain_svc: только схема brain) · infra/docker-compose.yml:6 (пароли из infra/.env)
-
08
HTTPS с HSTS-preload; экран нельзя встроить в чужой сайт
CRM отдаёт Strict-Transport-Security на два года с preload и Content-Security-Policy frame-ancestors 'none': браузер не откроет систему по http и не покажет её внутри чужой страницы. Плюс nosniff и Referrer-Policy на каждом ответе.
Проверьтеcurl -sI https://crm.proformdinamo.com/login | grep -iE 'strict|frame' → strict-transport-security: max-age=63072000; includeSubDomains; preload → content-security-policy: frame-ancestors 'none'
apps/crm/next.config.ts:21–31 (securityHeaders) · :50–52 (на все пути)
-
09
Токен приложения живёт пятнадцать минут
Токен доступа клиентского приложения подписан HS256 и действует 15 минут. У токена тренера своя область: он не открывает клиентские методы, и наоборот. Заблокированный аккаунт отвергается на каждом запросе, даже с живым токеном.
Проверьтедекодируйте payload токена (base64url, вторая часть) → exp − iat = 900 секунд
apps/crm/src/lib/member/jwt.ts:3–5 (ACCESS_TTL 15m) · :25–29 (scope trainer отклоняется) · apps/crm/src/lib/member/guard.ts:30–31 (appEnabled на каждом запросе)
-
10
Сервер в Ташкенте; база закрыта от интернета; копия каждую ночь
Сервер стоит в сети Uztelecom. База данных и кэш слушают только сам сервер — адрес 127.0.0.1. Резервная копия снимается каждую ночь; подробности и команды для проверки — в блоке ниже.
Проверьтеwhois 89.126.223.217 → netname: UZTELECOM · country: UZ · Tashkentinfra/docker-compose.yml:4 · :20 (127.0.0.1:5433) · :37 (127.0.0.1:6379) · infra/backup.sh:3–4 · :35–37
Где живут ваши данные
Сервер стоит в Ташкенте, в сети Uztelecom: адрес 89.126.223.217, на него указывают и digital.proformdinamo.com, и crm.proformdinamo.com. База данных PostgreSQL и кэш Redis слушают только сам сервер. Снаружи открыты три порта: 22 — SSH, вход только по ключу; 80 и 443 — сайт. Всё остальное закрыто.
Резервная копия базы снимается каждую ночь в 03:30: тридцать суточных копий по кругу, первая копия каждого месяца хранится бессрочно. Команда для восстановления записана в самом скрипте копирования — копия существует не как файл, а как способ вернуться.
$ whois 89.126.223.217 netname: UZTELECOM country: UZ address: Tashkent, Uzbekistan $ dig +short crm.proformdinamo.com 89.126.223.217 $ nc -zv 89.126.223.217 443 → succeeded $ nc -zv 89.126.223.217 5432 → Operation timed out — сервер не отвечает $ nc -zv 89.126.223.217 6379 → Operation timed out — сервер не отвечает $ ssh root@89.126.223.217 → Permission denied (publickey)
infra/docker-compose.yml:20 · :37 · infra/backup.sh:3–4 · :35–37 (команда восстановления) · infra/CRONTAB.md:17–18
Защита и доступы как услуга
То, что держит клуб, — и то, что мы добавляем для вас. Начинается не с договора, а с одного часа в вашей смене.
Состав и цену собирает калькулятор за три минуты. Точную сумму называет Тимур после разговора, когда видит объём. Телефон +998 94 045 54 48.