Files
LMS/docs/01-architecture.md
Vladislav Kopnin 6ffc6304e3 docs: add UML and architecture documentation (Mermaid)
- docs/README.md — index and short system overview
- 01-architecture.md — context, per-service components, Gradle modules,
  authentication/authorization, web interceptors (flowcharts)
- 02-deployment.md — deployment diagram, containers/env/volumes, build stages,
  nginx, update sequence
- 03-domain-model.md — class diagrams for identity/academic/progress and web,
  ER model of all schemas (cross-schema references noted)
- 04-sequences.md — 10 sequence diagrams (login, JWT check, forced password
  change, lab import, distribution, submission, defense, grades+publication,
  content publishing, group page aggregation)
- 05-states.md — state diagrams for defense session, grade publication, lab
  status, content visibility, submission lifecycle, variant assignment, password
MS
git log --oneline -3
2026-09-16 18:44:14 +03:00

7.0 KiB
Raw Blame History

Архитектура системы

1. Контекст (какие внешние участники и системы)

flowchart LR
    user["Преподаватель / студент<br/>(браузер)"]
    nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"]
    web["web BFF<br/>Thymeleaf + htmx<br/>:8090"]
    gw["gateway<br/>Spring Cloud Gateway<br/>:8080"]
    id["identity-service<br/>:8081"]
    ac["academic-service<br/>:8082"]
    pr["progress-service<br/>:8083"]
    db[("PostgreSQL 18<br/>схемы identity / academic / progress")]
    fs[("Файловое хранилище<br/>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. Компоненты по сервисам

flowchart TB
    subgraph web["web (BFF, :8090)"]
        wc["@Controller<br/>AuthPage, Learn, Student, Admin, ContentAdmin"]
        wi["Interceptor'ы:<br/>Csrf, Auth, Admin, MustChangePassword"]
        wapi["ApiClient (WebClient)<br/>+ DTO"]
        wmd["MarkdownRenderer<br/>(libs:markdown)"]
        wc --> wapi
        wc --> wmd
        wi --> wc
    end

    subgraph gw["gateway (:8080)"]
        routes["Маршруты<br/>identity / academic / progress"]
        filter["JwtAuthenticationFilter<br/>order -100"]
        filter --> routes
    end

    subgraph id["identity-service (:8081)"]
        idc["AuthController, GroupController, StudentController"]
        ids["AuthService, GroupService, Passwords,<br/>StudentImportParser, CredentialsExporter"]
        idr["AccountRepository, GroupRepository,<br/>EnrollmentRepository, StudentRepository, SettingRepository"]
        idc --> ids --> idr
    end

    subgraph ac["academic-service (:8082)"]
        acc["LabController, ContentPublicController,<br/>ContentAdminController"]
        acs["LabService, ContentService,<br/>LabImportParser, AssetStorage"]
        acr["LabRepository, LabLevelRepository, VariantRepository,<br/>SemesterRepository, Content*Repository"]
        acc --> acs --> acr
    end

    subgraph pr["progress-service (:8083)"]
        prc["ProgressController, DefenseAdminController,<br/>DefenseMyController"]
        prs["ProgressService, SubmissionService,<br/>DefenseService, GradePublicationService"]
        prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository,<br/>CommentTemplateRepository, SubmissionRepository,<br/>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

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. Аутентификация и авторизация

flowchart TD
    a["Клиент: POST /api/v1/auth/login"] --> b["gateway: путь публичный<br/>(login/setup/health/jwks)"]
    b --> c["identity: AuthService.login"]
    c --> d{"BCrypt совпал?"}
    d -->|нет| e["401 + ApiError"]
    d -->|да| f["JwtEncoder (RSA-2048, kid=lms)<br/>claims: sub, login, role, exp"]
    f --> g["AuthResponse{token,...}"]

    h["Клиент: запрос к защищённому API<br/>Authorization: Bearer ..."] --> i["gateway JwtAuthenticationFilter"]
    i --> j{"Токен валиден по JWKS?"}
    j -->|нет| k["401 Unauthorized"]
    j -->|да| l["Добавляет X-Account-Id/Login/Role"]
    l --> m["Сервис: SecurityConfig<br/>oauth2ResourceServer(jwt)"]
    m --> n{"@PreAuthorize<br/>ROLE_ADMIN / isAuthenticated"}
    n -->|нет| o["403 Forbidden"]
    n -->|да| p["Контроллер → сервис → БД"]
  • Роли: ADMIN, STUDENT; конвертер ролей читает claim role как 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)

flowchart LR
    req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"]
    csrf --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
    must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"]
    auth --> adm["AdminInterceptor /admin/**"]
    adm --> ctrl["Контроллер → ApiClient → gateway"]
  • GlobalModelAdvice кладёт currentUser (SessionUser) во все шаблоны.
  • Скрытые материалы и полное дерево материалов доступны только администратору.