# Portal Stand Access (MVP)

MVP-портал доступа к стендам в Proxmox через единый вход `https://stend.4mont.ru`.

## 1. Архитектура

- `traefik`: входная точка TLS, маршрутизация на API и динамические сессионные контейнеры.
- `api` (FastAPI): auth, ACL, админ API, создание runtime-сессий, cleanup.
- `db` (PostgreSQL): пользователи, сервисы, ACL, сессии, аудит.
- `portal-kiosk` image: Chromium kiosk + noVNC (для WEB target).
- `portal-rdp-proxy` image: xfreerdp + websockify/noVNC proxy до удаленного RDP host:port.
- `portal-universal-runtime` image: универсальный runtime (WEB/RDP), используется общим warm pool `UNIVERSAL_POOL_SIZE`.
- Опциональный prewarm pool:
  - глобальный fallback `PREWARM_POOL_SIZE`;
  - приоритетный `warm_pool_size` на каждом сервисе в админке.

Поток:
1. Пользователь логинится на `/`.
2. Dashboard показывает разрешённые сервисы.
3. Клик по `/go/<slug>` -> ACL check + проверка `expires_at` + создание записи `sessions` + старт отдельного контейнера.
4. Пользователь редиректится на `/s/<session_id>/`.
5. Traefik отправляет `/s/<session_id>/...` в конкретный runtime контейнер по dynamic labels.
6. Runtime страница шлёт heartbeat в `/api/sessions/<session_id>/touch`.
7. Фоновый cleanup завершает сессии при idle > 30 минут.

При prewarm:
- создаются warm-контейнеры с маршрутом `/svc/<slug>/`;
- клик по плитке использует prewarmed runtime без задержки cold start;
- сессии в БД создаются, но контейнеры остаются пулом (без стопа на каждую сессию).
- в `/admin` есть:
  - отдельные разделы Users / WEB / RDP (список + форма выбранной записи);
  - `pool size` на сервис;
  - кнопка `Prewarm now`;
  - health `running/desired` по каждому пулу.
  - авто-генерация `slug` из названия (поддержка кириллицы -> латиница).

## 2. Схема БД

Основные таблицы:
- `users`
- `services`
- `user_service_access`
- `sessions`

Дополнительно:
- `audit_logs`

SQL-схема: `scripts/schema.sql`.

## 3. Безопасность

- Пароли: `argon2` (`passlib[argon2]`).
- Cookie auth: `HttpOnly`, `Secure`, `SameSite=Strict`.
- CSRF:
  - формы (`/login`) через hidden token + cookie;
  - admin JSON API через `X-CSRF-Token`.
- Проверки при каждом запросе:
  - пользователь `active=true`;
  - `expires_at > now()`;
  - ACL на `/go/<slug>`.
- Аудит: события входа и создания сессий в `audit_logs`.

## 4. Файлы

- `docker-compose.yml`
- `traefik/traefik.yml`
- `traefik/dynamic/security.yml`
- `app/main.py`
- `kiosk/Dockerfile`, `kiosk/entrypoint.sh`
- `rdp-proxy/Dockerfile`, `rdp-proxy/entrypoint.sh`

## 5. Запуск

```bash
cp .env.example .env
mkdir -p traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json
```

Собрать runtime образы:

```bash
docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image
```

Поднять систему:

```bash
docker compose up -d --build
```

Проверка:

```bash
docker compose ps
docker compose logs -f api traefik
```

Дефолтный админ берётся из `.env` (`ADMIN_USERNAME`, `ADMIN_PASSWORD`).

## 5.1 Развертывание На Другом Сервере (Gitea)

Пример для чистого Ubuntu-сервера.

1. Установить Docker + Compose plugin:

```bash
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin git
sudo usermod -aG docker $USER
newgrp docker
```

2. Клонировать проект из Gitea:

```bash
git clone http://gitea.ruslan.xyz/ruslan/Stend_mont.git
cd Stend_mont
```

3. Подготовить конфиг:

```bash
cp .env.example .env
mkdir -p traefik/letsencrypt
touch traefik/letsencrypt/acme.json
chmod 600 traefik/letsencrypt/acme.json
```

4. Отредактировать `.env`:

- `PUBLIC_HOST` -> домен нового стенда (например `stend.example.com`)
- `LETSENCRYPT_EMAIL` -> рабочий email
- `ADMIN_USERNAME`, `ADMIN_PASSWORD` -> учетка администратора
- `DATABASE_URL` при необходимости оставить как есть (внутренний docker postgres)

5. Собрать и запустить:

```bash
docker compose --profile build-only build kiosk-image rdp-proxy-image universal-runtime-image
docker compose up -d --build
```

6. Проверка:

```bash
docker compose ps
docker compose logs -f api traefik
```

Если домен уже смотрит на этот сервер и 80/443 открыты, админка будет доступна по:

```text
https://<PUBLIC_HOST>/admin
```

## 5.2 Вариант С Nginx Proxy Manager (NPM)

Если на сервере уже используется NPM, внешний трафик можно вести через него, а Traefik оставить только внутренним роутером проекта.

В этом репозитории уже настроены внутренние порты:

- `127.0.0.1:2288 -> traefik:443` (HTTPS upstream)
- `127.0.0.1:8288 -> traefik:80` (HTTP upstream, технический)

Важно: Traefik убирать нельзя, он нужен для динамических маршрутов сессий (`/s/*`, `/svc/*`).

Как настроить NPM:

1. Создать `Proxy Host` для домена (например `stend.example.com`):
- `Forward Hostname / IP`: `127.0.0.1`
- `Forward Port`: `2288`
- `Scheme`: `https`
- включить `Websockets Support`

2. На вкладке SSL в NPM выпустить/подключить сертификат для домена.

3. В `.env` проекта оставить:
- `PUBLIC_HOST=stend.example.com`

4. Перезапустить проект:

```bash
docker compose up -d --build
```

После этого вход в портал и сессии будут работать через NPM, а Traefik останется внутренним компонентом.

## 6. Примеры admin API

1) Создать сервис WEB:

```bash
curl -k -X POST "https://stend.4mont.ru/api/admin/services" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: <csrf_from_cookie>" \
  -H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
  -d '{"name":"CRM","slug":"crm","type":"WEB","target":"http://192.168.1.10:3000","active":true}'
```

2) Создать сервис RDP:

```bash
curl -k -X POST "https://stend.4mont.ru/api/admin/services" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: <csrf_from_cookie>" \
  -H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
  -d '{"name":"Windows Desktop","slug":"win-rdp1","type":"RDP","target":"192.168.1.60:3389","active":true}'
```

Для RDP `target` также поддерживает креды и параметры:

```text
rdp://user:password@192.168.1.60:3389?domain=AD&sec=nla
```

3) Создать пользователя:

```bash
curl -k -X POST "https://stend.4mont.ru/api/admin/users" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: <csrf_from_cookie>" \
  -H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
  -d '{"username":"user1","password":"Passw0rd!","expires_at":"2026-12-31T23:59:59+00:00","active":true}'
```

4) Назначить ACL:

```bash
curl -k -X PUT "https://stend.4mont.ru/api/admin/users/2/acl" \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: <csrf_from_cookie>" \
  -H "Cookie: portal_auth=<cookie>; csrf_token=<csrf>" \
  -d '{"service_ids":[1,2]}'
```

## 7. Ограничения MVP

- Нет отдельной UI-админки (есть admin API).
- TTL неактивности основан на heartbeat runtime-страницы.
- Для production стоит добавить Alembic-миграции, rate limiting и централизованный логинг.

## 8. Нагрузочное тестирование (актуально на 2026-04-24)

В проекте подтвержден рабочий плавный профиль нагрузки без резкого burst:
- +1 пользователь в минуту;
- целевой online: 20 пользователей;
- каждый запускает 2 WEB-сервиса (termidesk и vmmanager).

Результат прогона 20x2:
- логин и открытие сервисов: 100% успешных проверок;
- HTTP 5xx в тесте: 0;
- достигнуто 40 ACTIVE WEB-сессий (20 + 20).

Важно для повторения теста:
1. Перед прогоном временно увеличьте idle timeout (SESSION_IDLE_SECONDS) и пересоздайте WEB-пул, чтобы слоты получили новый IDLE_TIMEOUT.
2. Параллельно собирайте метрики хоста (uptime/free/df/docker stats) в файл.
3. После прогона проверяйте согласованность: ACTIVE-сессии в БД vs реально существующие portal-webpool-* контейнеры.

Замеченный риск:
- при агрессивном авторасширении/сжатии WEB-пула возможны ACTIVE-сессии, привязанные к уже удаленным слотам.
