# Архитектура системы
## 1. Контекст (какие внешние участники и системы)
```mermaid
flowchart LR
user["Преподаватель / студент
(браузер)"]
nginx["nginx
lms.our-place.ru
TLS, reverse proxy"]
web["web BFF
Thymeleaf + Bootstrap
: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: access-токен живёт в HTTP-сессии web-сервиса
(`lms.token`), а в браузер уходит только cookie сессии `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, 7 дней sliding). Access-токен — 15 минут,
обновляется автоматически за 2 минуты до истечения по refresh-токену;
cookie `Max-Age` перевыпускается на каждом ответе.
- Все 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,
AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin
@RestController: ContentAssetProxy"]
wi["Interceptor'ы:
Csrf, NoCachePage, MustChangePassword, Auth, Admin"]
wapi["ApiClient (RestClient)
+ 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, ContentOrderController"]
acs["LabService, ContentService,
LabImportParser, AssetStorage"]
acr["LabRepository, LabLevelRepository, VariantRepository,
SemesterRepository, Content*Repository"]
acc --> acs --> acr
end
subgraph pr["progress-service (:8083)"]
prc["ProgressController, SubmissionController, DefenseAdminController,
DefenseMyController, StudentPurgeController, GroupLabDataController"]
prs["ProgressService, SubmissionService, DefenseService,
GradePublicationService, StudentPurgeService"]
prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,
SubmissionRepository, DefenseSessionRepository,
GradePublicationRepository, GradePublicationStudentRepository"]
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`, `TEACHER`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_`.
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,refresh,jwks.json}`,
`/api/v1/content/tree`, `/api/v1/content/{chapter}/{section}/{article}`.
`/api/v1/content/assets/**` gateway пропускает без JWT (публичные
`ALL`-активы), но academic проверяет видимость статьи и для курсовых или
ролевых активов требует токен. Файл без привязки к статье — `404` всем,
кроме админа (secure-by-default).
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
## 5. Рендеринг страниц и защита форм (web)
```mermaid
flowchart LR
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**
GET: выдаёт token в сессию
POST: сверяет _csrf"]
csrf --> nocahe["NoCachePageInterceptor /**
(страницы без кэша; /css, /js исключены)"]
nocahe --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"]
must --> auth["AuthInterceptor
/admin/**, /student/**, /change-password"]
auth --> adm["AdminInterceptor /admin/**"]
adm --> ctrl["Контроллер → ApiClient → gateway"]
```
- `GlobalModelAdvice` кладёт `currentUser` (SessionUser) во все шаблоны.
- Скрытые материалы и полное дерево материалов доступны только администратору.