Модуль franchiseoffice¶
Статус¶
Создан Spring Modulith-модуль Личного кабинета ФЧ. Реализованы отдельная security chain, полный минимальный auth-контур, список юридических лиц своего ФЧ и управление их iiko-подключениями.
Назначение и границы¶
Модуль franchiseoffice является HTTP/application-границей для сотрудников франчайзи. Он принимает
запросы /api/franchise/**, получает область доступа только из типизированного
FranchisePrincipal и не владеет доменными таблицами.
Разрешённые зависимости:
Прямой доступ к внутренним классам и таблицам доменных модулей запрещён. Зависимость на
restaurant::api будет добавлена вместе с первым ресторанным сценарием. Модуль расположен в
Gradle-модуле web; доменные модули не зависят от MVC.
Frontend-клиент¶
Frontend Личного кабинета ФЧ развивается независимо в репозитории
git@gitlab.tapir.ws:panam/backend/v2/franchise-lk.git и публикуется на
https://franchise.panampizza.ru. Это React SPA обращается к /api/franchise/v1/** через тот же
origin. Модуль franchiseoffice остаётся серверной HTTP/application-границей и не содержит
frontend-код.
Стек, сессия, генерация API-клиента и правила отображения интерфейса по permissions описаны в архитектуре административных frontend-приложений.
OpenAPI этого HTTP-контура публикуется отдельно на /v3/api-docs/franchise и содержит только
маршруты /api/franchise/**. Локальный снимок для генерации frontend-клиента создаётся командой
task openapi:export-franchise в web/build/openapi/franchise.json.
Реализованный endpoint¶
POST /api/franchise/v1/auth/login
Content-Type: application/json
{
"email": "employee@example.com",
"password": "permanent password"
}
Сценарий:
FranchiseAuthenticationпроверяет credentials, активное членство и отсутствиеoperationalBlock.FranchiseSessionsсоздаёт серверную refresh-сессию аудиторииFRANCHISE_OFFICE.FranchiseAccessTokensвыпускает JWT аудиторииfranchise-office.- Открытый refresh token помещается только в cookie.
- Access token,
userId,franchiseeId, отображаемое имя и permissions возвращаются в JSON.
Ответ:
{
"accessToken": "...",
"tokenType": "Bearer",
"accessExpiresAt": "2026-01-01T00:15:00Z",
"userId": "...",
"franchiseeId": "...",
"displayName": "Администратор ФЧ",
"permissions": ["RESTAURANT_MANAGE"],
"passwordChangeRequired": false
}
Refresh token отсутствует в JSON. Если выпуск access token завершается ошибкой после создания
сессии, новая refresh-сессия отзывается. Неверные credentials и недоступное членство возвращают
одинаковый 401 без cookie.
Refresh¶
Endpoint читает refresh token только из cookie своего контура, выполняет идемпотентную ротацию через
FranchiseSessions.rotate, выпускает новый access JWT и заменяет cookie. Успешный JSON-ответ
совпадает с login.
Старый token в течение 30 секунд повторно возвращает тот же successor при стабильном sid.
Отсутствующая, неизвестная, истёкшая, отозванная или повторно использованная после окна cookie
возвращает одинаковый 401 с Session refresh failed. Ответ
одновременно устанавливает пустую panam_franchise_refresh с Max-Age=0. Admin refresh-cookie не
читается и также приводит к этому ответу.
Logout¶
Logout отзывает серверную сессию по franchise refresh-cookie и всегда удаляет cookie через
Max-Age=0. Endpoint идемпотентен: отсутствие, неизвестность или повторный отзыв token не являются
ошибкой, ответ всегда 204 No Content. Bearer token не требуется и намеренно игнорируется, чтобы
устаревший Authorization header не мешал выходу. Admin-cookie не читается и не отзывает сессию ФЧ.
После logout связанный access JWT немедленно отклоняется, поскольку его проверка каждый раз проверяет состояние серверной сессии.
Юридические лица своего ФЧ¶
Endpoint требует FranchisePermission.LEGAL_ENTITY_MANAGE и возвращает юридические лица только
текущего ФЧ. franchiseeId не принимается ни в query, ни в body: application-сервис получает его
исключительно из типизированного FranchisePrincipal и вызывает
LegalEntities.findByFranchisee(principal.franchiseeId).
Ответ содержит id, реквизиты, статус, временные поля и version. franchiseeId в элемент ответа
не включён, поскольку scope уже однозначно задан аутентификацией. Отсутствующий или admin JWT даёт
401, а аутентифицированный principal без permission — 403.
iiko-подключения юридического лица¶
POST /api/franchise/v1/legal-entities/{legalEntityId}/iiko-connections
GET /api/franchise/v1/legal-entities/{legalEntityId}/iiko-connections
DELETE /api/franchise/v1/legal-entities/{legalEntityId}/iiko-connections/{connectionId}
Authorization: Bearer <franchise-access-token>
Все операции требуют FranchisePermission.IIKO_CONNECTION_MANAGE. Application-сервис сначала
проверяет, что legalEntityId принадлежит principal.franchiseeId, и только после этого вызывает
iiko::api. В POST принимаются только name и apiLogin; владелец берётся из path. GET
возвращает активные и отключённые подключения выбранного юридического лица. DELETE не удаляет
запись, а переводит подключение в DISABLED и возвращает 204 No Content.
Ответ содержит id, legalEntityId, имя, статус, временные поля и version. apiLogin никогда
не возвращается. Несуществующее или чужое юридическое лицо, чужой либо несуществующий connection ID
дают одинаковый 404, чтобы не раскрывать наличие чужих данных. Некорректные поля дают 400;
создание для неоперационного юридического лица — 409.
Refresh-cookie¶
name panam_franchise_refresh
path /api/franchise/v1/auth
HttpOnly true
Secure true по умолчанию
SameSite Strict
Max-Age оставшийся срок серверной refresh-сессии
Отдельные имя и path не позволяют браузеру отправить cookie в API Админки ГК. Для локального HTTP
Secure можно отключить через PANAM_FRANCHISE_OFFICE_AUTH_REFRESH_COOKIE_SECURE=false; в общих
средах и production он должен оставаться включённым.
Security chain¶
Отдельная цепочка с приоритетом после admin применяется только к /api/franchise/**. Без
аутентификации разрешены login, refresh и идемпотентный logout. Остальные маршруты требуют
bearer-аутентификацию.
CSRF отключён (csrf.disable()) и явно задан sessionManagement(STATELESS) для этой stateless API
chain: прикладные запросы используют Authorization header, а refresh/logout защищены отдельной
HttpOnly; Secure; SameSite=Strict cookie.
FranchiseOfficeBearerAuthenticationFilter проверяет token через FranchiseAccessTokens и
помещает в Security context отдельный FranchiseOfficeSecurityPrincipal: типизированный
FranchisePrincipal, sessionId и признак временного пароля. Spring authorities строятся только
из FranchisePermission с префиксом FRANCHISE_.
Фильтры обоих административных контуров дополнительно ограничены своими URI namespace в
shouldNotFilter, поскольку Spring bean типа OncePerRequestFilter может регистрироваться как
servlet filter вне конкретной security chain. Поэтому franchise-фильтр не обрабатывает /api/admin
и наоборот. Admin JWT не создаёт аутентификацию в контуре ФЧ.
Третья, замыкающая цепочка (@Order(3), DefaultSecurityConfiguration в корневом пакете web)
матчит /** и по умолчанию denyAll() всё, что не попало ни в эту, ни в admin-цепочку — кроме
/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-тест проверяет успешный login, JSON без refresh token и атрибуты cookie.
- HTTP-тест проверяет
franchiseeId, permissions и JWT черезFranchiseAccessTokens. - HTTP-тест проверяет одинаковый
401для неизвестного email и неверного пароля. - HTTP-тесты проверяют ротацию token/cookie, идемпотентный повтор, отсутствующую cookie, admin-cookie и удаление franchise-cookie при отказе.
- HTTP-тест проверяет закрытый маршрут без token и с реальным admin JWT.
- Test-only защищённый endpoint проверяет типизированный principal,
franchiseeIdи authorities. - HTTP-тесты проверяют logout, удаление cookie, немедленную недействительность access JWT, идемпотентность и изоляцию от admin-cookie.
- HTTP-тест списка юридических лиц создаёт данные двух ФЧ и проверяет, что чужие данные отсутствуют
в ответе, а
franchiseeIdберётся только из principal. - HTTP-тесты проверяют
401без franchise JWT и с admin JWT, а также403безLEGAL_ENTITY_MANAGE. - HTTP-тесты iiko-подключений проверяют создание и список без утечки
apiLogin, отключение без удаления, валидацию, неоперационное юридическое лицо,401,403и сокрытие чужих ресурсов. WebModularityTestsпроверяет шесть модулей runtime-приложения и объявленные границыfranchiseoffice → iam::api, franchise::api, iiko::api.
Следующий этап¶
После ручной проверки iiko-подключения зашифровать apiLogin отдельным принимаемым этапом.