This commit is contained in:
2025-10-01 15:04:07 +05:00
commit b526a80e1f
33 changed files with 4581 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
ENV/
.venv
pip-log.txt
pip-delete-this-directory.txt
.pytest_cache/
# Node
node_modules/
dist/
build/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.DS_Store
# IDE
.vscode/
.idea/
*.swp
*.swo
# Environment
.env
.env.local
*.secret
# Kubernetes
*.local.yaml

58
FINAL_STATUS.md Normal file
View File

@ -0,0 +1,58 @@
### **Финальная архитектура в k8s:**
```mermaid
flowchart
A["python-navigator-demo.127.0.0.1.sslip.io"] --> B["Единый Ingress + DexAuthenticator<br/>(аутентификация)"]
B --> C["Frontend Service"]
B --> D["DexAuthenticator Service"]
B --> E["Backend Service"]
C --> F["Frontend Pods"]
D --> G["DexAuthenticator Pods"]
E --> H["Backend Pods"]
H --> I["PostgreSQL Service"]
classDef ingress fill:#e1f5fe
classDef service fill:#f3e5f5
classDef pod fill:#e8f5e8
classDef database fill:#fff3e0
class A,B ingress
class C,D,E service
class F,G,H pod
class I database
```
### **Правильный поток аутентификации:**
1. Пользователь заходит на `https://python-navigator-demo.127.0.0.1.sslip.io`
2. DexAuthenticator проверяет аутентификацию
3. Если не аутентифицирован → редирект на Dex (HTTP 302)
4. Если не аутентифицирован в Dex → редирект на Blitz IdP (HTTP 302)
4. После аутентификации в Blitz IdP → возврат в Dex
4. После аутентификации в Dex → возврат в приложение
5. Frontend загружается с аутентификацией
6. Frontend делает запрос к `/api/user-info`
7. Backend получает JWT токен и валидирует его
8. Backend возвращает данные пользователя из PostgreSQL
9. Frontend отображает информацию о пользователе и доступные ресурсы
### **Для тестирования:**
**Откройте браузер и перейдите на `https://python-navigator-demo.127.0.0.1.sslip.io`**
Вас должно перенаправить на Dex для аутентификации. После входа вы увидите:
- Информацию о пользователе (email, полное имя, организация)
- Его роли (admin, developer, user, manager)
- Доступные ресурсы на основе ролей
### **Тестовые пользователи:**
Убедитесь, что в вашем Dex и внешнем IdP есть пользователи:
- `admin@example.com` - полный доступ ко всем ресурсам
- `developer@example.com` - технические ресурсы (CI/CD, Git, Docs, Wiki)
- `user@example.com` - только база знаний
- `manager@example.com` - управленческие ресурсы (Проекты, Отчеты, Wiki)

BIN
FINAL_STATUS.pdf Normal file

Binary file not shown.

38
Makefile Normal file
View File

@ -0,0 +1,38 @@
.PHONY: help build-backend build-frontend build-all deploy undeploy clean
help:
@echo "Доступные команды:"
@echo " make build-backend - Собрать Docker образ бэкенда"
@echo " make build-frontend - Собрать Docker образ фронтенда"
@echo " make build-all - Собрать все Docker образы"
@echo " make deploy - Развернуть приложение в k8s"
@echo " make undeploy - Удалить приложение из k8s"
@echo " make clean - Очистить все ресурсы"
build-backend:
@echo "Сборка backend образа..."
docker build -t python-navigator-demo-backend:latest ./backend
build-frontend:
@echo "Сборка frontend образа..."
docker build -t python-navigator-demo-frontend:latest ./frontend
build-all: build-backend build-frontend
@echo "Все образы собраны успешно!"
deploy:
@echo "Развертывание приложения в Kubernetes..."
~/.kind-d8/kubectl apply -k k8s/
@echo "Приложение развернуто!"
@echo "Доступно по адресу: https://python-navigator-demo.127.0.0.1.sslip.io"
undeploy:
@echo "Удаление приложения из Kubernetes..."
~/.kind-d8/kubectl delete -k k8s/
@echo "Приложение удалено!"
clean: undeploy
@echo "Очистка Docker образов..."
docker rmi navigator-demo-backend:latest navigator-demo-frontend:latest || true
@echo "Очистка завершена!"

249
README.md Normal file
View File

@ -0,0 +1,249 @@
# Dex Demo Application
Демонстрационное приложение для аутентификации через DexAuthenticator в Kubernetes кластере с Deckhouse.
## Описание
Простое приложение, демонстрирующее интеграцию с DexAuthenticator:
- **Backend (Python/FastAPI)**: Валидирует JWT токены, получает данные пользователя из PostgreSQL
- **Frontend (React/Vite)**: Отображает информацию о пользователе и доступные ресурсы на основе ролей
- **PostgreSQL**: Хранит пользователей, роли и доступные ссылки
- **DexAuthenticator**: Обеспечивает аутентификацию через Dex
## Архитектура
```
┌─────────────┐
│ Browser │
└─────┬───────┘
┌─────────────────────────────────────┐
│ Ingress + DexAuthenticator │
│ (аутентификация через Dex) │
└─────┬───────────────────────────────┘
├──────────► Frontend (React)
│ │
│ ▼
└──────────► Backend (FastAPI)
PostgreSQL
```
## Предварительные требования
- Kubernetes кластер с Deckhouse
- Настроенный Dex по адресу `https://dex.127.0.0.1.sslip.io`
- Docker
- kubectl
- make
## Быстрый старт
### 1. Сборка Docker образов
```bash
# Собрать все образы
make build-all
# Или по отдельности:
make build-backend
make build-frontend
```
### 2. Развертывание в Kubernetes
```bash
make deploy
```
Приложение будет доступно по адресу: `https://python-navigator-demo.127.0.0.1.sslip.io`
### 3. Удаление приложения
```bash
make undeploy
# Или полная очистка (включая Docker образы):
make clean
```
## Структура проекта
```
.
├── backend/ # Python бэкенд
│ ├── main.py # FastAPI приложение
│ ├── requirements.txt # Python зависимости
│ └── Dockerfile # Docker образ
├── frontend/ # React фронтенд
│ ├── src/
│ │ ├── App.jsx # Главный компонент
│ │ └── App.css # Стили
│ ├── nginx.conf # Nginx конфигурация
│ └── Dockerfile # Docker образ
├── db/ # База данных
│ └── init.sql # SQL скрипт инициализации
├── k8s/ # Kubernetes манифесты
│ ├── namespace.yaml
│ ├── postgres.yaml
│ ├── backend.yaml
│ ├── frontend.yaml
│ ├── dex-authenticator.yaml
│ └── ingress.yaml
├── Makefile # Команды для сборки и развертывания
└── README.md # Документация
```
## Компоненты
### Backend (FastAPI)
**Эндпоинты:**
- `GET /api/health` - Проверка здоровья сервиса
- `GET /api/user-info` - Получение информации о пользователе
**Функциональность:**
- Валидация JWT токенов от Dex (проверка подписи, issuer, exp)
- Извлечение email пользователя из токена или заголовков
- Получение данных пользователя из PostgreSQL (организация, полное имя)
- Получение ролей пользователя
- Получение доступных ссылок на основе ролей
### Frontend (React + Vite)
**Функциональность:**
- Отображение информации о пользователе
- Список ролей
- Доступные ресурсы на основе ролей пользователя
- Никакой логики аутентификации (вся аутентификация на стороне DexAuthenticator)
### База данных (PostgreSQL)
**Схема:**
- `organizations` - Организации
- `users` - Пользователи
- `roles` - Роли
- `user_roles` - Связь пользователей и ролей
- `links` - Доступные ссылки
- `role_links` - Связь ролей и ссылок
### Тестовые данные
По умолчанию в БД загружаются следующие тестовые пользователи:
1. **admin@example.com** (Иван Администраторов)
- Роли: admin, developer
- Организация: Acme Corporation
- Доступ: ко всем ресурсам
2. **developer@example.com** (Мария Разработчикова)
- Роли: developer, user
- Организация: Tech Innovators Inc
- Доступ: технические ресурсы (CI/CD, Git, Docs, Wiki)
3. **user@example.com** (Петр Пользователев)
- Роли: user
- Организация: Global Solutions Ltd
- Доступ: только база знаний
4. **manager@example.com** (Анна Менеджерова)
- Роли: manager, user
- Организация: Acme Corporation
- Доступ: управленческие ресурсы (Проекты, Отчеты, Wiki)
**Важно:** Убедитесь, что эти email совпадают с пользователями в вашем Dex, или измените данные в `db/init.sql`
## Конфигурация
### Backend переменные окружения
- `DB_HOST` - Хост PostgreSQL (по умолчанию: `postgres`)
- `DB_PORT` - Порт PostgreSQL (по умолчанию: `5432`)
- `DB_NAME` - Имя БД (по умолчанию: `dexdemo`)
- `DB_USER` - Пользователь БД (по умолчанию: `dexdemo`)
- `DB_PASSWORD` - Пароль БД
- `DEX_ISSUER` - URL Dex issuer (по умолчанию: `https://dex.127.0.0.1.sslip.io/`)
### DexAuthenticator
Настройки в `k8s/dex-authenticator.yaml`:
- `applicationDomain` - Домен приложения
- `sendAuthorizationHeader` - Отправка заголовка Authorization с JWT
- `keepUsersLoggedInFor` - Время сессии (24h)
## Работа приложения
1. Пользователь открывает `https://python-navigator-demo.127.0.0.1.sslip.io`
2. Ingress перенаправляет на DexAuthenticator для аутентификации
3. DexAuthenticator редиректит на Dex для входа
4. После успешной аутентификации Dex возвращает токен
5. DexAuthenticator устанавливает заголовки (`X-Auth-Request-Email`,`X-Auth-Request-User`, `Authorization`)
6. Frontend загружается и делает запрос к `/api/user-info`
7. Backend:
- Валидирует JWT токен
- Извлекает email пользователя
- Получает данные из PostgreSQL
- Возвращает информацию о пользователе и доступных ресурсах
8. Frontend отображает полученные данные
## Разработка
### Локальная разработка backend
```bash
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
export DB_HOST=localhost
export DEX_ISSUER=https://dex.127.0.0.1.sslip.io/
python main.py
```
### Локальная разработка frontend
```bash
cd frontend
npm install
npm run dev
```
Frontend будет доступен на `http://localhost:5173` с проксированием API на `http://localhost:8000`
### Ошибка "User not found in database"
Убедитесь, что email пользователя из Dex совпадает с email в таблице `users` в PostgreSQL.
### JWT валидация не работает
Проверьте:
1. Доступность Dex JWKS endpoint: `https://dex.127.0.0.1.sslip.io/keys`
2. Переменную окружения `DEX_ISSUER` в backend
3. Логи backend для деталей ошибки
## Дополнительная настройка
### Изменение тестовых пользователей
Отредактируйте `db/init.sql` или `k8s/postgres.yaml` (ConfigMap `postgres-init`), затем:
```bash
kubectl delete pod -n navigator-demo -l app=postgres
```
### Использование собственного домена
Измените `applicationDomain` в `k8s/dex-authenticator.yaml` и `host` в `k8s/ingress.yaml`
### Production deployment
Для production окружения:
1. Используйте secrets для паролей БД
3. Включите TLS сертификаты
4. Настройте resource limits
5. Добавьте HorizontalPodAutoscaler
6. Используйте внешний PostgreSQL

17
backend/Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM python:3.11-slim
WORKDIR /app
# Установка зависимостей
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Копирование кода приложения
COPY main.py .
# Открытие порта
EXPOSE 8000
# Запуск приложения
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

226
backend/main.py Normal file
View File

@ -0,0 +1,226 @@
import os
import psycopg2
from fastapi import FastAPI, HTTPException, Request, Depends
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from typing import List, Optional
from jose import jwt, JWTError
import requests
from functools import lru_cache
app = FastAPI(title="Dex Demo Backend")
# CORS настройки
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Конфигурация из переменных окружения
DB_HOST = os.getenv("DB_HOST", "postgres")
DB_PORT = os.getenv("DB_PORT", "5432")
DB_NAME = os.getenv("DB_NAME", "dexdemo")
DB_USER = os.getenv("DB_USER", "dexdemo")
DB_PASSWORD = os.getenv("DB_PASSWORD", "dexdemo")
DEX_ISSUER = os.getenv("DEX_ISSUER", "https://dex.127.0.0.1.sslip.io/")
DEX_JWKS_URL = f"{DEX_ISSUER}keys"
class Organization(BaseModel):
id: int
name: str
class Role(BaseModel):
id: int
name: str
description: Optional[str]
class Link(BaseModel):
id: int
title: str
url: str
description: Optional[str]
class UserInfo(BaseModel):
email: str
full_name: str
organization: Optional[Organization]
roles: List[Role]
available_links: List[Link]
def get_db_connection():
"""Создание подключения к БД"""
try:
conn = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)
return conn
except Exception as e:
raise HTTPException(status_code=500, detail=f"Database connection error: {str(e)}")
@lru_cache()
def get_jwks():
"""Получение JWKS от Dex для валидации JWT"""
try:
response = requests.get(DEX_JWKS_URL, verify=False, timeout=5)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"Error fetching JWKS: {e}")
return None
def validate_jwt_token(token: str) -> dict:
"""Валидация JWT токена"""
try:
# Получаем JWKS
jwks = get_jwks()
if not jwks:
raise HTTPException(status_code=500, detail="Cannot fetch JWKS from Dex")
# Декодируем заголовок токена, чтобы получить kid
unverified_header = jwt.get_unverified_header(token)
kid = unverified_header.get("kid")
# Находим нужный ключ
key = None
for jwk in jwks.get("keys", []):
if jwk.get("kid") == kid:
key = jwk
break
if not key:
raise HTTPException(status_code=401, detail="Unable to find appropriate key")
# Валидируем токен
payload = jwt.decode(
token,
key,
algorithms=["RS256"],
audience=None,
issuer=DEX_ISSUER,
options={
"verify_aud": False, # Отключаем проверку audience для демо
"verify_at_hash": False
}
)
return payload
except JWTError as e:
raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")
except Exception as e:
raise HTTPException(status_code=401, detail=f"Token validation error: {str(e)}")
def get_user_email(request: Request) -> str:
"""Извлечение email пользователя из JWT или заголовков"""
# Попытка получить токен из заголовка Authorization
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.startswith("Bearer "):
token = auth_header.split(" ")[1]
payload = validate_jwt_token(token)
# Email может быть в разных полях
email = payload.get("email") # payload.get("name")
if email:
return email
# Или берем из заголока от oauth2-proxy/DexAuthenticator
email = request.headers.get("X-Auth-Request-Email") # request.headers.get("X-Auth-Request-User")
if not email:
raise HTTPException(status_code=401, detail="No authentication information found")
return email
@app.get("/api/health")
def health_check():
"""Проверка здоровья сервиса"""
return {"status": "ok"}
@app.get("/api/user-info", response_model=UserInfo)
def get_user_info(email: str = Depends(get_user_email)):
"""Получение информации о пользователе"""
conn = get_db_connection()
cursor = conn.cursor()
try:
# Получаем информацию о пользователе
cursor.execute("""
SELECT u.email, u.full_name, u.organization_id, o.name as org_name
FROM users u
LEFT JOIN organizations o ON u.organization_id = o.id
WHERE u.email = %s
""", (email,))
user_row = cursor.fetchone()
if not user_row:
raise HTTPException(status_code=404, detail="User not found in database")
user_email, full_name, org_id, org_name = user_row
organization = None
if org_id and org_name:
organization = Organization(id=org_id, name=org_name)
# Получаем роли пользователя
cursor.execute("""
SELECT r.id, r.name, r.description
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = %s
""", (email,))
roles = [
Role(id=row[0], name=row[1], description=row[2])
for row in cursor.fetchall()
]
# Получаем доступные ссылки на основе ролей
cursor.execute("""
SELECT DISTINCT l.id, l.title, l.url, l.description
FROM links l
JOIN role_links rl ON l.id = rl.link_id
JOIN user_roles ur ON rl.role_id = ur.role_id
JOIN users u ON ur.user_id = u.id
WHERE u.email = %s
ORDER BY l.id
""", (email,))
available_links = [
Link(id=row[0], title=row[1], url=row[2], description=row[3])
for row in cursor.fetchall()
]
return UserInfo(
email=user_email,
full_name=full_name,
organization=organization,
roles=roles,
available_links=available_links
)
finally:
cursor.close()
conn.close()
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

8
backend/requirements.txt Normal file
View File

@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
psycopg2-binary==2.9.9
pydantic==2.5.0
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
requests==2.31.0

120
db/init.sql Normal file
View File

@ -0,0 +1,120 @@
-- Создание таблиц для демо-приложения
-- Организации
CREATE TABLE IF NOT EXISTS organizations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Пользователи
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
full_name VARCHAR(255) NOT NULL,
organization_id INTEGER REFERENCES organizations(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Роли
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Связь пользователей и ролей
CREATE TABLE IF NOT EXISTS user_roles (
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
);
-- Доступные ссылки
CREATE TABLE IF NOT EXISTS links (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
url VARCHAR(512) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Связь ролей и доступных ссылок
CREATE TABLE IF NOT EXISTS role_links (
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, link_id)
);
-- Заполнение тестовыми данными
-- Организации
INSERT INTO organizations (name) VALUES
('Acme Corporation'),
('Tech Innovators Inc'),
('Global Solutions Ltd');
-- Роли
INSERT INTO roles (name, description) VALUES
('admin', 'Администратор с полным доступом'),
('developer', 'Разработчик с доступом к техническим ресурсам'),
('user', 'Обычный пользователь с базовым доступом'),
('manager', 'Менеджер с доступом к управленческим ресурсам');
-- Пользователи (используйте реальные email из Dex)
INSERT INTO users (email, full_name, organization_id) VALUES
('egor.muratov@gmail.com', 'Иван Администраторов', 1),
('developer@example.com', 'Мария Разработчикова', 2),
('user@example.com', 'Петр Пользователев', 3),
('manager@example.com', 'Анна Менеджерова', 1);
-- Назначение ролей пользователям
-- admin@example.com - admin + developer
INSERT INTO user_roles (user_id, role_id) VALUES
(1, 1), -- admin role
(1, 2); -- developer role
-- developer@example.com - developer + user
INSERT INTO user_roles (user_id, role_id) VALUES
(2, 2), -- developer role
(2, 3); -- user role
-- user@example.com - user
INSERT INTO user_roles (user_id, role_id) VALUES
(3, 3); -- user role
-- manager@example.com - manager + user
INSERT INTO user_roles (user_id, role_id) VALUES
(4, 4), -- manager role
(4, 3); -- user role
-- Ссылки
INSERT INTO links (title, url, description) VALUES
('Панель администрирования', 'https://admin.example.com', 'Управление системой'),
('Мониторинг', 'https://monitoring.example.com', 'Grafana и Prometheus'),
('CI/CD', 'https://ci.example.com', 'Jenkins/GitLab CI'),
('Документация API', 'https://docs.example.com', 'Swagger документация'),
('Git Repository', 'https://git.example.com', 'Репозиторий проекта'),
('Дашборд проектов', 'https://projects.example.com', 'Управление проектами'),
('Отчеты', 'https://reports.example.com', 'Аналитика и отчеты'),
('База знаний', 'https://wiki.example.com', 'Корпоративная wiki');
-- Связь ролей и ссылок
-- admin - доступ ко всему
INSERT INTO role_links (role_id, link_id) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8);
-- developer - технические ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(2, 2), (2, 3), (2, 4), (2, 5), (2, 8);
-- user - базовые ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(3, 8);
-- manager - управленческие ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(4, 6), (4, 7), (4, 8);

48
docker-compose.yml Normal file
View File

@ -0,0 +1,48 @@
# Docker Compose для локальной разработки и тестирования
version: '3.8'
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: dexdemo
POSTGRES_USER: dexdemo
POSTGRES_PASSWORD: dexdemo
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dexdemo"]
interval: 10s
timeout: 5s
retries: 5
backend:
build: ./backend
ports:
- "8000:8000"
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: dexdemo
DB_USER: dexdemo
DB_PASSWORD: dexdemo
DEX_ISSUER: https://dex.127.0.0.1.sslip.io
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
frontend:
build: ./frontend
ports:
- "8080:80"
depends_on:
- backend
restart: unless-stopped
volumes:
postgres_data:

8
frontend/.dockerignore Normal file
View File

@ -0,0 +1,8 @@
node_modules
dist
.git
.gitignore
*.md
.vscode
.idea

24
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

24
frontend/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
# Сборка приложения
FROM node:20-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production образ с nginx
FROM nginx:alpine
# Копирование собранного приложения
COPY --from=build /app/dist /usr/share/nginx/html
# Конфигурация nginx для проксирования API
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

16
frontend/README.md Normal file
View File

@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## React Compiler
The React Compiler is not enabled on this template. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

29
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,29 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs['recommended-latest'],
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
},
},
])

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>frontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

18
frontend/nginx.conf Normal file
View File

@ -0,0 +1,18 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
}

2823
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@vitejs/plugin-react": "^5.0.3",
"eslint": "^9.36.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.4.0",
"vite": "^7.1.7"
}
}

1
frontend/public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

213
frontend/src/App.css Normal file
View File

@ -0,0 +1,213 @@
:root {
--primary-color: #4f46e5;
--secondary-color: #06b6d4;
--background: #f8fafc;
--card-background: #ffffff;
--text-primary: #1e293b;
--text-secondary: #64748b;
--border-color: #e2e8f0;
--success-color: #10b981;
--error-color: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: var(--background);
color: var(--text-primary);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
text-align: center;
margin-bottom: 3rem;
}
.header h1 {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.subtitle {
color: var(--text-secondary);
font-size: 1.1rem;
}
.user-card {
background: var(--card-background);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.user-card h2 {
font-size: 1.75rem;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.info-grid {
display: grid;
gap: 1rem;
margin-bottom: 2rem;
}
.info-item {
display: flex;
padding: 0.75rem;
background: var(--background);
border-radius: 8px;
}
.info-item .label {
font-weight: 600;
color: var(--text-secondary);
min-width: 150px;
}
.info-item .value {
color: var(--text-primary);
font-weight: 500;
}
.roles-section {
margin: 2rem 0;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.roles-section h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.roles-list {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.role-badge {
display: inline-flex;
flex-direction: column;
padding: 0.5rem 1rem;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white;
border-radius: 8px;
font-weight: 500;
}
.role-name {
font-size: 0.95rem;
font-weight: 600;
}
.role-description {
font-size: 0.75rem;
opacity: 0.9;
margin-top: 0.25rem;
}
.links-section {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--border-color);
}
.links-section h3 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.links-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1rem;
}
.link-card {
display: block;
padding: 1.25rem;
background: var(--background);
border: 1px solid var(--border-color);
border-radius: 8px;
text-decoration: none;
color: inherit;
transition: all 0.2s ease;
}
.link-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
border-color: var(--primary-color);
}
.link-card h4 {
color: var(--primary-color);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
.link-card p {
color: var(--text-secondary);
font-size: 0.9rem;
}
.no-links {
color: var(--text-secondary);
font-style: italic;
}
.loading, .error {
text-align: center;
padding: 3rem;
font-size: 1.25rem;
}
.loading {
color: var(--text-secondary);
}
.error {
color: var(--error-color);
}
.error h2 {
margin-bottom: 1rem;
}
@media (max-width: 768px) {
.container {
padding: 1rem;
}
.header h1 {
font-size: 1.75rem;
}
.user-card {
padding: 1.5rem;
}
.links-grid {
grid-template-columns: 1fr;
}
}

113
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,113 @@
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [userInfo, setUserInfo] = useState(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
// Получаем информацию о пользователе от бэкенда
fetch('/api/user-info')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
})
.then(data => {
setUserInfo(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [])
if (loading) {
return (
<div className="container">
<div className="loading">Загрузка...</div>
</div>
)
}
if (error) {
return (
<div className="container">
<div className="error">
<h2>Ошибка</h2>
<p>{error}</p>
</div>
</div>
)
}
return (
<div className="container">
<header className="header">
<h1>Dex Authentication Demo</h1>
<p className="subtitle">Демонстрация аутентификации через DexAuthenticator</p>
</header>
<div className="user-card">
<h2>Информация о пользователе</h2>
<div className="info-grid">
<div className="info-item">
<span className="label">Email:</span>
<span className="value">{userInfo.email}</span>
</div>
<div className="info-item">
<span className="label">Полное имя:</span>
<span className="value">{userInfo.full_name}</span>
</div>
{userInfo.organization && (
<div className="info-item">
<span className="label">Организация:</span>
<span className="value">{userInfo.organization.name}</span>
</div>
)}
</div>
<div className="roles-section">
<h3>Роли</h3>
<div className="roles-list">
{userInfo.roles.map(role => (
<div key={role.id} className="role-badge">
<span className="role-name">{role.name}</span>
{role.description && (
<span className="role-description">{role.description}</span>
)}
</div>
))}
</div>
</div>
<div className="links-section">
<h3>Доступные ресурсы</h3>
{userInfo.available_links.length > 0 ? (
<div className="links-grid">
{userInfo.available_links.map(link => (
<a
key={link.id}
href={link.url}
className="link-card"
target="_blank"
rel="noopener noreferrer"
>
<h4>{link.title}</h4>
{link.description && <p>{link.description}</p>}
</a>
))}
</div>
) : (
<p className="no-links">У вас нет доступных ресурсов</p>
)}
</div>
</div>
</div>
)
}
export default App

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

10
frontend/src/index.css Normal file
View File

@ -0,0 +1,10 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
}
body {
margin: 0;
min-height: 100vh;
}

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)

15
frontend/vite.config.js Normal file
View File

@ -0,0 +1,15 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
})

73
k8s/backend.yaml Normal file
View File

@ -0,0 +1,73 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: backend-config
namespace: python-navigator-demo
data:
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: python-navigator-demo
DB_USER: python-navigator-demo
DEX_ISSUER: https://dex.127.0.0.1.sslip.io/
---
apiVersion: v1
kind: Secret
metadata:
name: backend-secret
namespace: python-navigator-demo
type: Opaque
stringData:
DB_PASSWORD: python-navigator-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: python-navigator-demo
spec:
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: navigator-demo-backend:latest
imagePullPolicy: Never # Для локальной разработки
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: backend-config
- secretRef:
name: backend-secret
livenessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: python-navigator-demo
spec:
selector:
app: backend
ports:
- port: 8000
targetPort: 8000
type: ClusterIP

View File

@ -0,0 +1,12 @@
apiVersion: deckhouse.io/v1
kind: DexAuthenticator
metadata:
name: python-navigator-demo-auth
namespace: python-navigator-demo
spec:
applicationDomain: python-navigator-demo.127.0.0.1.sslip.io
sendAuthorizationHeader: true
applicationIngressCertificateSecretName: python-navigator-demo-tls
applicationIngressClassName: nginx
keepUsersLoggedInFor: 24h

34
k8s/frontend.yaml Normal file
View File

@ -0,0 +1,34 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: python-navigator-demo
spec:
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: python-navigator-demo-frontend:latest
imagePullPolicy: Never
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: python-navigator-demo
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80
type: ClusterIP

41
k8s/ingress.yaml Normal file
View File

@ -0,0 +1,41 @@
# Единый Ingress с аутентификацией для всего приложения
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: python-navigator-demo
namespace: python-navigator-demo
annotations:
nginx.ingress.kubernetes.io/auth-signin: https://$host/dex-authenticator/sign_in
nginx.ingress.kubernetes.io/auth-url: https://python-navigator-demo-auth-dex-authenticator.python-navigator-demo.svc.cluster.local/dex-authenticator/auth
nginx.ingress.kubernetes.io/auth-response-headers: X-Auth-Request-User,X-Auth-Request-Email,Authorization
nginx.ingress.kubernetes.io/configuration-snippet: |
proxy_set_header X-Auth-Request-User $http_x_auth_request_user;
proxy_set_header X-Auth-Request-Email $http_x_auth_request_email;
proxy_set_header Authorization $http_authorization;
spec:
ingressClassName: nginx
tls:
- hosts:
- python-navigator-demo.127.0.0.1.sslip.io
secretName: python-navigator-demo-tls
rules:
- host: python-navigator-demo.127.0.0.1.sslip.io
http:
paths:
# API запросы идут напрямую в backend
- path: /api
pathType: Prefix
backend:
service:
name: backend
port:
number: 8000
# Все остальное идет в frontend
- path: /
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80

5
k8s/namespace.yaml Normal file
View File

@ -0,0 +1,5 @@
apiVersion: v1
kind: Namespace
metadata:
name: python-navigator-demo

199
k8s/postgres.yaml Normal file
View File

@ -0,0 +1,199 @@
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
namespace: python-navigator-demo
type: Opaque
stringData:
POSTGRES_DB: python-navigator-demo
POSTGRES_USER: python-navigator-demo
POSTGRES_PASSWORD: python-navigator-demo
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: python-navigator-demo
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-init
namespace: python-navigator-demo
data:
init.sql: |
-- Создание таблиц для демо-приложения
-- Организации
CREATE TABLE IF NOT EXISTS organizations (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Пользователи
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
full_name VARCHAR(255) NOT NULL,
organization_id INTEGER REFERENCES organizations(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Роли
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Связь пользователей и ролей
CREATE TABLE IF NOT EXISTS user_roles (
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, role_id)
);
-- Доступные ссылки
CREATE TABLE IF NOT EXISTS links (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
url VARCHAR(512) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Связь ролей и доступных ссылок
CREATE TABLE IF NOT EXISTS role_links (
role_id INTEGER REFERENCES roles(id) ON DELETE CASCADE,
link_id INTEGER REFERENCES links(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, link_id)
);
-- Заполнение тестовыми данными
-- Организации
INSERT INTO organizations (name) VALUES
('Acme Corporation'),
('Tech Innovators Inc'),
('Global Solutions Ltd');
-- Роли
INSERT INTO roles (name, description) VALUES
('admin', 'Администратор с полным доступом'),
('developer', 'Разработчик с доступом к техническим ресурсам'),
('user', 'Обычный пользователь с базовым доступом'),
('manager', 'Менеджер с доступом к управленческим ресурсам');
-- Пользователи (используйте реальные email из Dex)
INSERT INTO users (email, full_name, organization_id) VALUES
('egor.muratov@gmail.com', 'Иван Администраторов', 1),
('developer@example.com', 'Мария Разработчикова', 2),
('user@example.com', 'Петр Пользователев', 3),
('manager@example.com', 'Анна Менеджерова', 1);
-- Назначение ролей пользователям
-- admin@example.com - admin + developer
INSERT INTO user_roles (user_id, role_id) VALUES
(1, 1), -- admin role
(1, 2); -- developer role
-- developer@example.com - developer + user
INSERT INTO user_roles (user_id, role_id) VALUES
(2, 2), -- developer role
(2, 3); -- user role
-- user@example.com - user
INSERT INTO user_roles (user_id, role_id) VALUES
(3, 3); -- user role
-- manager@example.com - manager + user
INSERT INTO user_roles (user_id, role_id) VALUES
(4, 4), -- manager role
(4, 3); -- user role
-- Ссылки
INSERT INTO links (title, url, description) VALUES
('Панель администрирования', 'https://admin.example.com', 'Управление системой'),
('Мониторинг', 'https://monitoring.example.com', 'Grafana и Prometheus'),
('CI/CD', 'https://ci.example.com', 'Jenkins/GitLab CI'),
('Документация API', 'https://docs.example.com', 'Swagger документация'),
('Git Repository', 'https://git.example.com', 'Репозиторий проекта'),
('Дашборд проектов', 'https://projects.example.com', 'Управление проектами'),
('Отчеты', 'https://reports.example.com', 'Аналитика и отчеты'),
('База знаний', 'https://wiki.example.com', 'Корпоративная wiki');
-- Связь ролей и ссылок
-- admin - доступ ко всему
INSERT INTO role_links (role_id, link_id) VALUES
(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8);
-- developer - технические ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(2, 2), (2, 3), (2, 4), (2, 5), (2, 8);
-- user - базовые ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(3, 8);
-- manager - управленческие ресурсы
INSERT INTO role_links (role_id, link_id) VALUES
(4, 6), (4, 7), (4, 8);
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: python-navigator-demo
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: postgres-secret
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
- name: init-script
mountPath: /docker-entrypoint-initdb.d
volumes:
- name: postgres-storage
persistentVolumeClaim:
claimName: postgres-pvc
- name: init-script
configMap:
name: postgres-init
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: python-navigator-demo
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
type: ClusterIP

70
test-deployment.sh Executable file
View File

@ -0,0 +1,70 @@
#!/bin/bash
echo "=== Тест развертывания Dex Demo Application ==="
echo
# Проверка статуса подов
echo "1. Статус подов:"
~/.kind-d8/kubectl get pods -n navigator-demo
echo
# Проверка DexAuthenticator
echo "2. Статус DexAuthenticator:"
~/.kind-d8/kubectl get dexauthenticator -n navigator-demo
echo
# Проверка Ingress
echo "3. Статус Ingress:"
~/.kind-d8/kubectl get ingress -n navigator-demo
echo
# Проверка логов backend
echo "4. Логи Backend (последние 5 строк):"
~/.kind-d8/kubectl logs -n navigator-demo -l app=backend --tail=5
echo
# Проверка доступности приложения
echo "5. Тест доступности приложения:"
echo "URL: https://navigator-demo.127.0.0.1.sslip.io"
HTTP_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" https://navigator-demo.127.0.0.1.sslip.io)
echo "HTTP Status: $HTTP_STATUS"
if [ "$HTTP_STATUS" = "200" ]; then
echo "✅ Frontend доступен!"
elif [ "$HTTP_STATUS" = "302" ] || [ "$HTTP_STATUS" = "307" ]; then
echo "✅ Frontend правильно требует аутентификации!"
else
echo "❌ Ошибка доступа к frontend"
fi
# Проверка API (должен требовать аутентификации)
echo "6. Тест API endpoint:"
echo "URL: https://navigator-demo.127.0.0.1.sslip.io/api/user-info"
API_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" https://navigator-demo.127.0.0.1.sslip.io/api/user-info)
echo "API Status: $API_STATUS"
if [ "$API_STATUS" = "401" ] || [ "$API_STATUS" = "302" ] || [ "$API_STATUS" = "307" ]; then
echo "✅ API правильно требует аутентификации!"
elif [ "$API_STATUS" = "500" ]; then
echo "⚠️ API возвращает 500 (возможно, проблема с DexAuthenticator)"
else
echo "❓ Неожиданный статус API: $API_STATUS"
fi
echo
# Проверка health endpoint
echo "6. Тест health endpoint:"
~/.kind-d8/kubectl exec -n navigator-demo deployment/backend -- curl -s http://localhost:8000/api/health
echo
echo
echo "=== Инструкции по тестированию ==="
echo "1. Откройте браузер и перейдите на https://navigator-demo.127.0.0.1.sslip.io"
echo "2. Вас должно перенаправить на страницу аутентификации Dex"
echo "3. После входа вы увидите информацию о пользователе и доступные ресурсы"
echo
echo "Тестовые пользователи (убедитесь, что они есть в вашем Dex):"
echo "- admin@example.com (Иван Администраторов)"
echo "- developer@example.com (Мария Разработчикова)"
echo "- user@example.com (Петр Пользователев)"
echo "- manager@example.com (Анна Менеджерова)"