Модуль admin¶
Статус¶
Создан Spring Modulith-модуль Админки ГК. Реализованы HTTP-вход, refresh, logout, bearer-аутентификация и обязательная смена временного пароля. Прикладные endpoints управления франчайзингом и их проверки permissions ещё не реализованы.
Назначение и границы¶
Модуль admin является HTTP/application-границей для сотрудников головной компании. Он принимает
запросы /api/admin/**, преобразует HTTP-модель в вызовы публичных контрактов бизнес-модулей и не
владеет доменными таблицами.
На текущем этапе разрешена единственная зависимость:
Прямой доступ к внутренним классам и таблицам 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"
}
Сценарий:
AdminAuthenticationпроверяет credentials и активное членство ГК.AdminSessionsсоздаёт серверную refresh-сессию.AdminAccessTokensвыпускает access JWT для этой сессии.- Открытый refresh token помещается только в cookie.
- 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¶
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¶
Logout отзывает серверную сессию по refresh-cookie и всегда удаляет cookie через Max-Age=0.
Endpoint идемпотентен: отсутствие, неизвестность или повторный отзыв token не являются ошибкой,
ответ всегда 204 No Content. Bearer token не требуется и намеренно игнорируется, чтобы устаревший
Authorization header не мешал завершению выхода.
После отзыва сессии связанный access JWT немедленно отклоняется, поскольку bearer-проверка каждый
раз проверяет состояние sid в IAM.
Refresh-cookie¶
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, необходимые для приглашения первого сотрудника ФЧ.