# Архитектура системы ## 1. Контекст (какие внешние участники и системы) ```mermaid flowchart LR user["Преподаватель / студент
(браузер)"] nginx["nginx
lms.our-place.ru
TLS, reverse proxy"] web["web BFF
Thymeleaf + htmx
:8090"] gw["gateway
Spring Cloud Gateway
:8080"] id["identity-service
:8081"] ac["academic-service
:8082"] pr["progress-service
:8083"] db[("PostgreSQL 18
схемы identity / academic / progress")] fs[("Файловое хранилище
content-assets, submissions")] user -->|HTTPS| nginx nginx -->|HTTP 127.0.0.1:8090| web web -->|Bearer JWT| gw gw --> id gw --> ac gw --> pr id --> db ac --> db pr --> db ac --> fs pr --> fs pr -->|REST, внутренняя сеть| ac pr -->|REST, внутренняя сеть| id id -.->|JWKS: /api/v1/auth/jwks.json| gw id -.->|JWKS| ac id -.->|JWKS| pr ``` - Браузер никогда не получает JWT: токен живёт в HTTP-сессии web-сервиса (`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней). - Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`. - Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки `X-Account-Id`, `X-Account-Login`, `X-Account-Role`. - Каждый сервис дополнительно валидирует JWT самостоятельно (resource server). ## 2. Компоненты по сервисам ```mermaid flowchart TB subgraph web["web (BFF, :8090)"] wc["@Controller
AuthPage, Learn, Student, Admin, ContentAdmin"] wi["Interceptor'ы:
Csrf, Auth, Admin, MustChangePassword"] wapi["ApiClient (WebClient)
+ DTO"] wmd["MarkdownRenderer
(libs:markdown)"] wc --> wapi wc --> wmd wi --> wc end subgraph gw["gateway (:8080)"] routes["Маршруты
identity / academic / progress"] filter["JwtAuthenticationFilter
order -100"] filter --> routes end subgraph id["identity-service (:8081)"] idc["AuthController, GroupController, StudentController"] ids["AuthService, GroupService, Passwords,
StudentImportParser, CredentialsExporter"] idr["AccountRepository, GroupRepository,
EnrollmentRepository, StudentRepository, SettingRepository"] idc --> ids --> idr end subgraph ac["academic-service (:8082)"] acc["LabController, ContentPublicController,
ContentAdminController"] acs["LabService, ContentService,
LabImportParser, AssetStorage"] acr["LabRepository, LabLevelRepository, VariantRepository,
SemesterRepository, Content*Repository"] acc --> acs --> acr end subgraph pr["progress-service (:8083)"] prc["ProgressController, DefenseAdminController,
DefenseMyController"] prs["ProgressService, SubmissionService,
DefenseService, GradePublicationService"] prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository,
CommentTemplateRepository, SubmissionRepository,
DefenseSessionRepository, GradePublicationRepository"] prcl["AcademicClient, IdentityClient"] prc --> prs prs --> prr prs --> prcl end wapi -->|HTTP| routes routes --> idc routes --> acc routes --> prc prcl -->|GET /api/v1/labs/{id}| acc prcl -->|GET /api/v1/groups и /{id}| idc ``` ## 3. Модули Gradle ```mermaid flowchart LR web[":web"] --> common[":libs:common-http"] web --> md[":libs:markdown"] gw[":services:gateway"] --> sec[":libs:security-jwt"] gw --> common id[":services:identity-service"] --> common id --> sec ac[":services:academic-service"] --> common ac --> sec ac --> md pr[":services:progress-service"] --> common pr --> sec tests[":libs:test-support"] -.-> id tests -.-> ac tests -.-> pr ``` | Модуль | Назначение | |---|---| | `:libs:common-http` | единый формат ошибок (`ApiError`), иерархия `LmsException`, `LmsExceptionHandler`, генератор UUIDv7 (`Ids`) | | `:libs:security-jwt` | свойства JWT, парсинг RSA-ключей, автоконфигурация `JwtEncoder`/`JwtDecoder`/`PasswordEncoder`, enum `LmsRole` | | `:libs:markdown` | markdown → безопасный HTML (OWASP-санитайзер), таблицы, спойлеры `:::spoiler` | | `:libs:test-support` | константы для интеграционных тестов с PostgreSQL | ## 4. Аутентификация и авторизация ```mermaid flowchart TD a["Клиент: POST /api/v1/auth/login"] --> b["gateway: путь публичный
(login/setup/health/jwks)"] b --> c["identity: AuthService.login"] c --> d{"BCrypt совпал?"} d -->|нет| e["401 + ApiError"] d -->|да| f["JwtEncoder (RSA-2048, kid=lms)
claims: sub, login, role, exp"] f --> g["AuthResponse{token,...}"] h["Клиент: запрос к защищённому API
Authorization: Bearer ..."] --> i["gateway JwtAuthenticationFilter"] i --> j{"Токен валиден по JWKS?"} j -->|нет| k["401 Unauthorized"] j -->|да| l["Добавляет X-Account-Id/Login/Role"] l --> m["Сервис: SecurityConfig
oauth2ResourceServer(jwt)"] m --> n{"@PreAuthorize
ROLE_ADMIN / isAuthenticated"} n -->|нет| o["403 Forbidden"] n -->|да| p["Контроллер → сервис → БД"] ``` - Роли: `ADMIN`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_`. - Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`, `/api/v1/content/tree`, `/api/v1/content/assets/**`, `/api/v1/content/{chapter}/{section}/{article}`. - RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS. ## 5. Рендеринг страниц и защита форм (web) ```mermaid flowchart LR req["HTTP-запрос"] --> csrf["CsrfInterceptor /**
GET: выдаёт token в сессию
POST: сверяет _csrf"] csrf --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"] must --> auth["AuthInterceptor
/admin/**, /student/**, /change-password"] auth --> adm["AdminInterceptor /admin/**"] adm --> ctrl["Контроллер → ApiClient → gateway"] ``` - `GlobalModelAdvice` кладёт `currentUser` (SessionUser) во все шаблоны. - Скрытые материалы и полное дерево материалов доступны только администратору.