Backend Architecture
FastAPI-based backend with PostgreSQL, Redis, and LXD container management.
Directory Structure
backend/app/
├── api/ # API endpoint routers
│ ├── admin.py # Admin CRUD (users, modules, exams, labs)
│ ├── dashboard.py # User dashboard stats & activity
│ ├── exams.py # Exam operations
│ ├── laboratories.py # Lab operations
│ ├── lessons.py # Lesson content fetching
│ ├── modules.py # Module listing
│ ├── profile.py # User profile
│ └── ...
├── crud/ # Database CRUD operations
│ ├── base.py # Generic base CRUD class
│ ├── exam.py # Exam, attempt, task result CRUD
│ ├── module.py # Module CRUD
│ └── user.py # User CRUD + authentication
├── db/ # Database layer
│ ├── models.py # SQLAlchemy ORM models
│ └── init_db.py # DB initialization + seeding
├── schemas/ # Pydantic request/response schemas
│ ├── auth.py, user.py, exam.py, laboratory.py, ...
├── services/ # Business logic services
│ ├── exam_verifier.py # Runs bash verification in containers
│ ├── laboratory_verifier.py
│ └── lessons_service.py # Fetches markdown from GitLab repo
├── auth.py # JWT creation, validation, get_current_user
├── config.py # Pydantic Settings (reads from .env)
├── database.py # AsyncSession factory
├── lxc_manager.py # LXD API client wrapper
├── main.py # FastAPI app, lifespan, core routes
└── websocket.py # WebSocket terminal handler (SSH ↔ browser)
Key Components
1. Authentication (auth.py)
JWT Bearer tokens with HS256. Two FastAPI dependencies:
get_current_user— validates token, returnsUser. Raises401for missing/invalid token,403for inactive user.require_admin— wrapsget_current_user, raises403ifrole != 'admin'.
2. Database Models (db/models.py)
Three-tier lesson system: Module → Section → Theme
Exam system: Exam → ExamTask with ExamAttempt → ExamTaskResult
Labs system: Laboratory → LaboratoryTask with LaboratoryAttempt → LaboratoryTaskResult
Container tracking: Container stores all LXD containers (sandbox + exam + lab)
3. LXD Management (lxc_manager.py)
Wraps pylxd client connecting to https://10.0.0.3:8443 via TLS client certificates.
# All LXD operations are blocking — always wrap with asyncio.to_thread()
container = await asyncio.to_thread(lxc_manager.create_container, name, image)
4. Exam Verifier (services/exam_verifier.py)
SSH into container via jump host, execute bash verification scripts, parse PASS/FAIL output, write results to DB.
5. WebSocket Terminal (websocket.py)
Upgrades HTTP to WebSocket, then SSH-connects to the container via jump host and bridges stdin/stdout bidirectionally.
API Endpoints Summary
Authentication
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register | Public | Register new user |
| POST | /api/auth/login | Public | Login, returns JWT |
| GET | /api/auth/me | Student | Current user info |
Containers
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/containers/create | Student | Create sandbox container |
| GET | /api/containers/sandbox | Student | Get user's sandbox |
| DELETE | /api/containers/{id} | Student | Delete container |
| WS | /ws/terminal/{id}?token= | Student | Terminal WebSocket |
Exams
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/exams | Student | List active exams |
| GET | /api/exams/{id} | Student | Exam details + tasks |
| POST | /api/exams/{id}/start | Student | Start exam (creates container) |
| POST | /api/exams/{id}/submit | Student | Submit + auto-grade |
| GET | /api/exams/attempts/{id}/results | Student | Detailed results |
Admin
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /api/admin/stats | Admin | System statistics |
| GET/POST | /api/admin/users | Admin | List / create users |
| DELETE | /api/admin/users/{id} | Admin | Delete user |
| GET/POST | /api/admin/exams | Admin | List / create exams |
Configuration
All configuration is in backend/app/config.py as Pydantic BaseSettings. Values are read from environment variables (or backend/.env).
Key variables:
DATABASE_URL=postgresql+asyncpg://...
SECRET_KEY=...
REDIS_URL=redis://...
LXD_ENDPOINT=https://10.0.0.3:8443
LXD_CERT_PATH=/app/.lxd/client.crt
LXD_KEY_PATH=/app/.lxd/client.key
LXD_SSH_JUMP_HOST=65.109.236.163
LESSONS_REPO_URL=git@gitlab.com:zsaidov1988/lessons-content.git
LESSONS_SSH_KEY_PATH=/app/private_key
See docs/standards/config.md for the full variable list.
Troubleshooting
| Error | Likely Cause | Fix |
|---|---|---|
MissingGreenlet | Lazy-loading SQLAlchemy relation outside async context | Use selectinload() in query |
SSH authentication failed | Missing or incorrect SSH key mount | Check /app/private_key exists with 600 permissions |
LXD connection refused | Wrong endpoint or expired certs | Verify LXD_ENDPOINT and cert paths |
422 Unprocessable Entity | Bad request body (Pydantic validation) | Check request schema against Swagger UI |