# 🏗 Архитектура системы ## Общая схема ```mermaid graph TD Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"] Caddy -->|:80| Frontend["Frontend
(Apache httpd:alpine)"] Caddy -->|/api/*| Backend["Backend
(Spring Boot 3.2.5)"] Backend --> TenantRouter{"TenantRoutingDataSource"} TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL
swsu_db"] TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL
mgu_db"] TenantRouter -->|...| DBn["PostgreSQL
tenant_n_db"] Backend -->|Метрики, Логи, Трейсы| OTel["OpenTelemetry Collector"] OTel --> SigNoz["SigNoz"] ``` ## Компоненты ### Frontend (Apache httpd:alpine) - **Тип:** Статические файлы (HTML/CSS/JS) - **Контейнер:** `httpd:alpine` — лёгкий Apache HTTP Server - **Порт:** 80 - **Содержание:** Три изолированных интерфейса — `admin/`, `teacher/`, `student/` - **JS-модули:** Vanilla JavaScript с ES6 Modules (`import`/`export`) ### Backend (Spring Boot 3.2.5) - **Тип:** REST API сервер - **Язык:** Java 17 - **Порт:** 8080 (внутренний) - **ORM:** Hibernate (JPA), `ddl-auto=none` - **Миграции:** Flyway (программный запуск при подключении тенанта) - **Аутентификация:** bcrypt (через `BCryptPasswordEncoder`), UUID-токены ### PostgreSQL - **Версия:** `postgres:alpine3.23` - **Локально:** Одна БД `app_db` (тенант `default`) - **Продакшн:** Множество БД, по одной на каждый университет (тенант) ### Caddy (реверс-прокси) - **Расположение:** `../caddy-proxy/` - **Назначение:** TLS-терминация, маршрутизация запросов к backend/frontend - **Домен:** `*.zuev.company` --- ## Мультитенантная архитектура Ключевая особенность системы — изоляция данных каждого университета в отдельной БД PostgreSQL. ### Принцип работы ```mermaid sequenceDiagram participant Browser as Браузер participant Interceptor as TenantInterceptor participant Context as TenantContext participant Router as TenantRoutingDataSource participant DB as PostgreSQL Browser->>Interceptor: GET /api/users
Host: swsu.zuev.company Interceptor->>Interceptor: resolveTenant("swsu.zuev.company") → "swsu" Interceptor->>Context: setCurrentTenant("swsu") Note over Interceptor: Проверка: hasTenant("swsu")? Interceptor-->>Browser: 404 если тенант не найден Note over Context,Router: Обработка запроса контроллером Router->>Router: determineCurrentLookupKey() → "swsu" Router->>DB: SQL запрос к swsu_db DB-->>Browser: Ответ с данными ``` ### Ключевые классы | Класс | Назначение | |-------|-----------| | `TenantInterceptor` | Извлекает поддомен из заголовка `Host` и определяет тенант | | `TenantContext` | `ThreadLocal`-хранилище имени текущего тенанта | | `TenantRoutingDataSource` | Наследует `AbstractRoutingDataSource`, маршрутизирует запросы к нужной БД | | `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы | | `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов | | `ConfigMapUpdater` | Обновляет Kubernetes ConfigMap при добавлении/удалении тенанта через API | | `TenantConfig` | POJO с параметрами тенанта: `name`, `domain`, `url`, `username`, `password` | ### Определение тенанта Логика определения тенанта по заголовку `Host`: | Host | Результат | |------|----------| | `swsu.zuev.company` | `swsu` | | `mgu.zuev.company` | `mgu` | | `localhost` | `default` | | `localhost:8080` | `default` | | `192.168.1.1` | `default` | ### Конфигурация тенантов Список тенантов хранится в JSON-файле: - **Локально:** `backend/tenants.json` - **Продакшн:** Kubernetes ConfigMap `tenants-config`, монтируется в `/config/tenants.json` Формат: ```json [ { "name": "ЮЗГУ", "domain": "swsu", "url": "jdbc:postgresql://db-host:5432/swsu_db", "username": "dbuser", "password": "dbpass" } ] ``` ### Жизненный цикл тенанта 1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap 2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет `tenants.json` → добавляет новые / удаляет отсутствующие тенанты 3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет ConfigMap ### Fallback при отсутствии тенантов Если при запуске нет ни одного настроенного тенанта: 1. Проверяется наличие `spring.datasource.url` → создаётся тенант `default` 2. Если datasource тоже нет → создаётся H2 in-memory заглушка для инициализации Spring JPA --- ## Аутентификация Система использует **простую модель аутентификации** без JWT или Spring Security фильтров: 1. Клиент отправляет `POST /api/auth/login` с `username` и `password` 2. Backend проверяет пароль через `BCryptPasswordEncoder` 3. При успехе возвращается: - UUID-токен (для заголовка `Authorization: Bearer`) - Роль пользователя (`ADMIN`, `TEACHER`, `STUDENT`) - Redirect URL (`/admin/`, `/teacher/`, `/student/`) 4. Токен хранится в `localStorage` на клиенте