Files
magistr/docs/ARCHITECTURE.md

143 lines
6.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🏗 Архитектура системы
## Общая схема
```mermaid
graph TD
Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"]
Caddy -->|:80| Frontend["Frontend<br/>(Apache httpd:alpine)"]
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.2.5)"]
Backend --> TenantRouter{"TenantRoutingDataSource"}
TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL<br/>swsu_db"]
TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL<br/>mgu_db"]
TenantRouter -->|...| DBn["PostgreSQL<br/>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<br/>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` на клиенте