Перейти к содержанию

Модуль admin

Статус

Создан Spring Modulith-модуль Админки ГК. Реализованы HTTP-вход, refresh, logout, bearer-аутентификация и обязательная смена временного пароля. Прикладные endpoints управления франчайзингом и их проверки permissions ещё не реализованы.

Назначение и границы

Модуль admin является HTTP/application-границей для сотрудников головной компании. Он принимает запросы /api/admin/**, преобразует HTTP-модель в вызовы публичных контрактов бизнес-модулей и не владеет доменными таблицами.

На текущем этапе разрешена единственная зависимость:

admin ──▶ iam::api

Прямой доступ к внутренним классам и таблицам iam запрещён. Зависимости на franchise::api, iiko::api и restaurant::api будут добавляться только вместе с конкретными административными сценариями.

Модуль расположен в Gradle-модуле web, поскольку владеет Spring MVC и HTTP security chain. Доменные модули остаются в библиотеке app и не зависят от MVC.

Frontend-клиент

Frontend Админки ГК развивается независимо в репозитории git@gitlab.tapir.ws:panam/backend/v2/admin.git и публикуется на https://admin.panampizza.ru. Это React SPA обращается к /api/admin/v1/** через тот же origin. Модуль admin остаётся серверной HTTP/application-границей и не содержит frontend-код.

Стек, сессия, генерация API-клиента и правила отображения интерфейса по permissions описаны в архитектуре административных frontend-приложений. Auth-ответ содержит userId и permissions текущего principal; frontend не должен извлекать их декодированием JWT.

OpenAPI этого HTTP-контура публикуется отдельно на /v3/api-docs/admin и содержит только маршруты /api/admin/**. Локальный снимок для генерации frontend-клиента создаётся командой task openapi:export-admin в web/build/openapi/admin.json.

Все поля auth request/response помечены в схеме обязательными. AdminPermission опубликован как отдельная reusable schema, защищённые операции ссылаются на adminBearer, успешные JSON-ответы имеют media type application/json, а прикладные ошибки — application/problem+json. Для каждого auth endpoint явно закреплены фактические HTTP-коды, включая 204 у logout и 400/401/409 у смены временного пароля.

Реализованный endpoint

POST /api/admin/v1/auth/login
Content-Type: application/json

{
  "email": "admin@example.com",
  "password": "temporary password"
}

Сценарий:

  1. AdminAuthentication проверяет credentials и активное членство ГК.
  2. AdminSessions создаёт серверную refresh-сессию.
  3. AdminAccessTokens выпускает access JWT для этой сессии.
  4. Открытый refresh token помещается только в cookie.
  5. Access token и несекретные сведения возвращаются в JSON.

Ответ:

{
  "accessToken": "...",
  "tokenType": "Bearer",
  "accessExpiresAt": "2026-01-01T00:15:00Z",
  "userId": "...",
  "displayName": "Первый администратор",
  "permissions": [],
  "passwordChangeRequired": true
}

userId и permissions входят в одинаковый успешный ответ login, refresh и смены временного пароля. Пока passwordChangeRequired=true, permissions пусты. После смены пароля ответ содержит permissions текущей роли; frontend использует их напрямую и не декодирует JWT.

Если выпуск access token завершился ошибкой после создания сессии, новая refresh-сессия отзывается. Неверный email, пароль или административный доступ возвращают одинаковый 401 без cookie.

Refresh

POST /api/admin/v1/auth/refresh
Cookie: panam_admin_refresh=...

Endpoint читает refresh token только из cookie, выполняет идемпотентную ротацию через AdminSessions.rotate, выпускает новый access JWT и заменяет cookie. Формат успешного JSON-ответа совпадает с login.

Старый token в течение 30 секунд повторно возвращает тот же successor, поэтому параллельные вкладки не конкурируют за cookie и сохраняют стабильный sid. Отсутствующая, неизвестная, истёкшая, отозванная или повторно использованная после этого окна cookie возвращает одинаковый 401 с Session refresh failed. Ответ одновременно устанавливает пустую cookie с Max-Age=0, удаляя непригодный token из браузера.

Смена временного пароля

POST /api/admin/v1/auth/change-temporary-password
Authorization: Bearer <access-token>
Content-Type: application/json

{
  "currentPassword": "temporary password",
  "newPassword": "new permanent password"
}

Ограниченный access JWT, полученный при входе с временным паролем, не содержит permissions, но может аутентифицировать этот endpoint. AdminPasswordChange повторно проверяет текущий пароль, меняет hash и отзывает все прежние refresh-сессии. После этого модуль admin создаёт новую сессию, выпускает JWT с permissions роли и заменяет refresh-cookie. Повторный login не требуется.

Access JWT старой сессии перестаёт приниматься немедленно. Запрос без bearer token, с неверной подписью, истёкшим JWT или отозванным sid получает одинаковый 401.

Прикладные отказы различаются и возвращают application/problem+json:

Ситуация Ответ title
новый пароль вне 12–72 UTF-8 байт 400 Password rejected
новый пароль совпадает с текущим 400 Password rejected
временный пароль уже заменён 409 Password change not required
неверный текущий пароль или доступ отключён 401 Authentication failed

Неверный текущий пароль намеренно не отличается от отключённого доступа. Остальные случаи — ошибки заполнения формы, поэтому причина сообщается клиенту явно и ни один из них не изменяет credential.

Logout

POST /api/admin/v1/auth/logout
Cookie: panam_admin_refresh=...

Logout отзывает серверную сессию по refresh-cookie и всегда удаляет cookie через Max-Age=0. Endpoint идемпотентен: отсутствие, неизвестность или повторный отзыв token не являются ошибкой, ответ всегда 204 No Content. Bearer token не требуется и намеренно игнорируется, чтобы устаревший Authorization header не мешал завершению выхода.

После отзыва сессии связанный access JWT немедленно отклоняется, поскольку bearer-проверка каждый раз проверяет состояние sid в IAM.

name      panam_admin_refresh
path      /api/admin/v1/auth
HttpOnly  true
Secure    true по умолчанию
SameSite  Strict
Max-Age   оставшийся срок серверной refresh-сессии

Refresh token не включается в JSON и недоступен JavaScript. Для локального HTTP значение Secure можно отключить через PANAM_ADMIN_AUTH_REFRESH_COOKIE_SECURE=false; в общих средах и production оно должно оставаться включённым.

Security chain

Отдельная цепочка применяется только к /api/admin/**. Без bearer-аутентификации разрешены login, refresh и идемпотентный logout. Endpoint смены пароля требует bearer-аутентификацию. Остальные административные маршруты пока запрещены.

CSRF полностью отключён (csrf.disable()) и явно задан sessionManagement(STATELESS): login не использует cookie, refresh/logout защищены SameSite=Strict cookie, смена пароля — Authorization header, HttpSession под CSRF-токен или анонимную аутентификацию Spring не заводит.

AdminBearerAuthenticationFilter проверяет token через AdminAccessTokens и помещает в Spring Security context AdminSecurityPrincipal: типизированный AdminPrincipal, sessionId и признак обязательной смены пароля. Spring authorities строятся только из AdminPermission; строковые роли не используются. shouldNotFilter дополнительно ограничивает фильтр URI namespace /api/admin/**, поэтому servlet-регистрация Spring bean не позволяет ему обрабатывать JWT Личного кабинета ФЧ.

Третья, замыкающая цепочка (@Order(3), DefaultSecurityConfiguration в корневом пакете web) матчит /** и по умолчанию denyAll() всё, что не попало ни в эту, ни во franchise-цепочку — кроме /v3/api-docs/** и /swagger-ui/**, которые остаются публичными. Это гарантирует, что новый эндпоинт вне /api/admin/** и /api/franchise/** по умолчанию закрыт, а не открыт.

LoginRateLimiter (корневой пакет web, общий для admin и franchise) ограничивает /login и /refresh по IP клиента: panam.security.login-rate-limit.login-limit попыток на login-window (по умолчанию 10 / минуту) и отдельно refresh-limit на refresh-window (по умолчанию 30 / минуту). Превышение отдаёт 429 application/problem+json с Retry-After. Счётчики хранятся в памяти процесса и не переживают перезапуск; при переходе на несколько инстансов их нужно вынести в общее хранилище.

Проверки

  • HTTP-тест проверяет успешный вход, JSON без refresh token и атрибуты cookie.
  • HTTP-тест проверяет одинаковый 401 для неизвестного email и неверного пароля.
  • HTTP-тесты проверяют ротацию token/cookie, идемпотентный повтор, отсутствующую cookie и её удаление при отказе.
  • HTTP-тесты проверяют смену временного пароля, выдачу новой пары, немедленный отказ старого JWT и обязательность bearer-аутентификации.
  • HTTP-тесты проверяют 400 для слишком короткого и повторно использованного пароля и 409 для повторной смены уже постоянного пароля.
  • HTTP-тесты проверяют отзыв сессии, удаление cookie, недействительность JWT после logout и идемпотентный повторный выход.
  • WebModularityTests проверяет пять модулей runtime-приложения и границу admin → iam::api.

Следующий этап

Минимальный auth-контур Админки ГК завершён. Следующий отдельно принимаемый этап — вернуться в iam и реализовать FranchiseMembership, типизированные FranchiseRole и FranchisePermission, необходимые для приглашения первого сотрудника ФЧ.