# Бэкенд МГТУ «СТАНКИН» — международный кабинет (этап 1) Серверная часть личного кабинета на **NestJS + Prisma + PostgreSQL**. Реализован полный контур этапа 1: **регистрация → подача заявки и документов → проверка в ДМС → решение → уведомление**, со всей защитой данных (RLS, шифрование ПДн, аудит, одноразовые токены, антивирус загрузок). > Код проверен сквозным тестом против живого PostgreSQL под непривилегированной ролью БД — **44 проверок** (`npm run e2e`). ## Что реализовано **Аутентификация и доступ** - Регистрация с обязательным согласием 152-ФЗ; подтверждение email по одноразовому токену (в БД только хеш); вход (Argon2id, защита от перебора, `login_audit`, JWT); профиль. - **Сброс пароля** (`password/forgot` + `password/reset`) на том же механизме токенов. - **Смена пароля из кабинета** (`password/change`, для залогиненного): требует верный текущий пароль; новый, совпадающий с текущим, отклоняется. - **Почтовый сервис**: письма подтверждения email, сброса пароля и приглашения сотрудника. Транспорт выбирается конфигом — SMTP в проде, in-memory/лог в dev. **Токены уходят только письмом и из API не возвращаются.** **Заявки (абитуриент)** — подача, список своих, просмотр своей заявки с историей статусов. **Документы** - Загрузка к заявке (`POST /applications/:id/documents`): валидация типа (PDF/JPG/PNG) и размера (≤10 МБ), контрольная сумма sha-256, **антивирус-проверка (ClamAV)**, хранение в S3-РФ (в dev — локально, тот же интерфейс). - Дозагрузка автоматически возвращает заявку `docs_requested → review`. - Список и удаление своих непринятых документов. - Проверка ДМС (`PATCH /staff/documents/:id/review`): принять/отклонить с комментарием → `document_review` + статус + уведомление абитуриенту. **Очередь и проверка (ДМС)** — очередь с фильтрами/пагинацией/непрочитанными; детальная заявка с ПДн (по роли через RLS); назначение; смена статуса с валидацией переходов (state machine, недопустимые → 409), историей и уведомлением. **Уведомления** — список, счётчик непрочитанных, отметка прочитанными (строго свой аккаунт). **Админка** — приглашение сотрудника ДМС (создаёт аккаунт + токен задания пароля); блокировка/разблокировка аккаунтов (заблокированный не входит). **Сквозная защита** — RLS-контекст на каждый запрос (`SET LOCAL`), гварды JWT/ролей, аудит операций (триггеры БД), шифрование ПДн. ## Структура ``` src/ prisma/ PrismaService (+ RLS-контекст) common/ одноразовые токены (генерация/хеш) storage/ StorageService (S3-РФ / локально) + ScannerService (ClamAV / stub) auth/ регистрация, подтверждение, вход, сброс пароля; гварды applications/ подача и просмотр своих заявок documents/ загрузка/список/удаление + проверка ДМС staff/ очередь, проверка, смена статусов; state machine notifications/ уведомления admin/ приглашение сотрудников, блокировки app.module.ts, main.ts test/e2e.flow.ts сквозной тест этапа (44 проверок) ``` ## Запуск через Docker (одной командой) Поднимает весь контур: PostgreSQL (со схемой, слоем безопасности и ролью приложения — автоматически при первой инициализации), приложение, **MinIO** как S3-РФ для документов и **MailHog** для просмотра писем. ```bash docker compose up --build ``` После старта: - API — http://localhost:3000/api/v1 - письма (MailHog UI) — http://localhost:8025 - хранилище (MinIO консоль) — http://localhost:9001 (minioadmin / minioadmin) - PostgreSQL — localhost:5432 (postgres / postgres, БД `stankin`) Приложение в compose подключается к БД под ролью `app_user` (RLS активен), пишет файлы в MinIO (`STORAGE_DRIVER=s3`) и отправляет письма в MailHog. Все секреты в compose — **демо**; для прода вынести в секрет-хранилище и заменить MinIO/MailHog на хранилище и SMTP в контуре РФ. ## Запуск вручную (без Docker) ```bash npm install && npm run prisma:generate psql -d stankin -f ../schema.sql # схема + слой безопасности (RLS/триггеры/роли/шифрование) # роль приложения: CREATE ROLE app_user LOGIN PASSWORD '…'; GRANT app_rw TO app_user; cp .env.example .env # ключи ПДн/JWT — из KMS/секретов; UPLOAD_DIR или S3-настройки npm run build && npm start # :3000, префикс /api/v1 npm run e2e # сквозной тест против БД ``` ## Важно про безопасность - Приложение **обязано** подключаться под непривилегированной ролью (член `app_rw`, не суперпользователь) — иначе RLS не действует (проверяется тестом). - Слой RLS/триггеров/ролей/шифрования — в `schema.sql`, поверх `prisma migrate`. - Пароли — Argon2id; токены — только хеши; ПДн — `pgp_sym_encrypt` ключом вне БД; файлы — в РФ, в БД только метаданные и `file_key`. ## Дальше Этапы 1 и 2 реализованы и проверены сквозными тестами (`npm run e2e`, `npm run e2e:stage2`), всё обёрнуто в Docker. **Этап 3 (интеграционный) — начат.** Первый срез готов и проверён (`npm run e2e:stage3`): фоновые напоминания о сроках виз и паспортов (`src/reminders`). Планировщик без внешних зависимостей читает готовые поля `visa_record.valid_until` и `person_document.expires_at`, по порогам 30/14/7/3/1 дн. и при истечении создаёт `notification` (идемпотентно) и шлёт письмо на e-mail аккаунта на его языке (RU/EN/中文). Ручной запуск: `POST /admin/reminders/run` (ДМС/админ). Структура БД не менялась. Осталось по этапу: активация онлайн-оплаты (эквайринг) на полях-заделах `payment.*`, синхронизация с 1С:Университет и обмен с МВД (`external_id`/`source_system`/`synced_at`). Тоже без изменения структуры БД. Сейчас для них добавлены **заглушки-заделы** (seam готов, реальная реализация позже): - Эквайринг: `AcquiringProvider` (абстракция) + `StubAcquiringProvider`, роуты `POST /payments/:id/online` и вебхук `POST /payments/webhook/:provider`. Реальный провайдер (ЮKassa/Сбер) подключается заменой `useClass` в `payments.module`. - Интеграции: модуль `integrations` — 1С:Университет (заглушка, драйвер `ONEC_DRIVER`) и эквайринг (заглушка, `ACQUIRING_PROVIDER`); админ-роуты `GET /admin/integrations/status`, `POST /admin/integrations/1c/sync-student/:id`, `POST /admin/integrations/1c/pull-students`. **С МВД интеграции нет и не планируется** — визовый/миграционный учёт ведёт ДМС вручную в кабинете. - Платёжная квитанция: PDF, **только на русском**, формируется всегда (не зависит от эквайринга) — `GET /payments/:id/receipt` (студент — свою, сотрудник/админ — любую; реквизиты получателя из `PAYEE_*`). ## Каталог программ обучения Модель `Program` расширена под витрину направлений: трилингвальные `title_*` / `description_*` (RU/EN/中文), `tuition_per_year` (₽) + `currency`, `language` (коды через запятую: `ru,en`), `duration_semesters`, `study_form`, `faculty`, `level`, `code` (уникальный), `is_active` (+ задел `external_id` / `source_system`). **Публичное чтение** (без авторизации, для сайта и формы подачи заявки): - `GET /api/v1/programs` — активные программы; фильтры `?level=`, `?language=` (подстрока), `?active=all` (включая снятые с публикации). - `GET /api/v1/programs/:id` — карточка с полными описаниями. **Управление каталогом** (роль `staff_dms` / `admin`): - `GET /api/v1/admin/programs` — все, включая неактивные. - `POST /api/v1/admin/programs` — создать; `PATCH /api/v1/admin/programs/:id` — изменить/снять с публикации (`isActive`). - `POST /api/v1/admin/programs/import` — загрузка каталога файлом (поле `file`, Excel `.xlsx` или CSV; до 5 МБ). Upsert по `code`; ответ — `{ total, created, updated, skipped, errors[] }`. **Массовая загрузка из файла (CLI):** ``` npm run programs:import -- /путь/к/файлу.xlsx ``` **Колонки таблицы импорта** (заголовок строки 1; принимаются рус./англ. варианты): `code`, `level`, `faculty`, `language`, `tuition_per_year`, `currency`, `duration_semesters`, `study_form`, `title_ru`, `title_en`, `title_zh`, `description_ru`, `description_en`, `description_zh`, `is_active`. Обязательны `code`, `level`, `title_ru`. Готовый шаблон — `programs-template.xlsx`. Заявка абитуриента уже умеет ссылаться на программу (`application.program_id`) — каталог даёт источник для выбора и показа стоимости.