270 lines
9.4 KiB
Markdown
270 lines
9.4 KiB
Markdown
# Лабараторная 2
|
||||
|
|
Минимальный костяк сервиса управления пользователями и OAuth-клиентами. Предоставляет REST API по пути `/auth/*`, GraphQL-эндпоинт по адресу `/graphql` и небольшое админ-SPA на чистом JS, отдаваемое по `/admin`.
|
|||
|
|
|
|||
|
|
Основные технологии:
|
|||
|
|
- Python 3.12
|
|||
|
|
- **FastAPI** - REST API (`/auth/register`, `/auth/login`, `/auth/me`)
|
|||
|
|
- **Ariadne** - GraphQL-сервер
|
|||
|
|
- **SQLAlchemy 2.x** - ORM
|
|||
|
|
- **Alembic** - миграции
|
|||
|
|
- **SQLite** - база данных
|
|||
|
|
- **argon2-cffi** - хеширование паролей Argon2id + стойкость через `zxcvbn`
|
|||
|
|
- **python-jose** - JWT токены
|
|||
|
|
|
|||
|
|
## Установка
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
git clone https://vantablacktea.ddns.net/Agzamus/scripts-2.git
|
|||
|
|
cd scripts-2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### С помощью uv
|
|||
|
|
|
|||
|
|
[Скачать uv](https://docs.astral.sh/uv/)
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
uv sync
|
|||
|
|
uv run python run.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Тесты:
|
|||
|
|
```sh
|
|||
|
|
uv run pytest -q
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Альтернатива с pip
|
|||
|
|
|
|||
|
|
Linux:
|
|||
|
|
```sh
|
|||
|
|
python -m venv .venv
|
|||
|
|
source .venv/bin/activate
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Windows:
|
|||
|
|
```sh
|
|||
|
|
python -m venv .venv
|
|||
|
|
source .venv\Scripts\activate
|
|||
|
|
pip install -r requirements.txt
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Настройка
|
|||
|
|
|
|||
|
|
Необходимо создать `.env` файл, например скопировав пример:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
cp .env.example .env
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> Записать для `JWT_SECRET` и `PASSWORD_PEPPER` длинные случайные строки
|
|||
|
|
> `JWT_SECRET` подписывает каждый access-токен, а `PASSWORD_PEPPER` добавляется к паролю перед хешированием
|
|||
|
|
|
|||
|
|
Переменные из `.env.example`:
|
|||
|
|
|
|||
|
|
| Переменная | Пример | Назначение |
|
|||
|
|
| ---------------------- | ----------------------------------------- | ------------------------------------------------- |
|
|||
|
|
| `DATABASE_URL` | `sqlite:///./vault.db` | Строка подключения SQLAlchemy |
|
|||
|
|
| `JWT_SECRET` | `replace_with_a_long_random_string` | Секрет для подписи/проверки JWT |
|
|||
|
|
| `JWT_ALGORITHM` | `HS256` | Алгоритм подписи JWT |
|
|||
|
|
| `ACCESS_TOKEN_TTL_MIN` | `60` | Время жизни access-токена в минутах |
|
|||
|
|
| `PASSWORD_PEPPER` | `replace_with_another_long_random_string` | Глобальный секрет, добавляемый перед хешированием |
|
|||
|
|
| `PASSWORD_MIN_LENGTH` | `12` | Минимальная длина пароля |
|
|||
|
|
| `ENVIRONMENT` | `development` | Метка окружения выполнения |
|
|||
|
|
| `APP_NAME` | `Vault` | Заголовок приложения |
|
|||
|
|
|
|||
|
|
## Запуск
|
|||
|
|
|
|||
|
|
Примените миграции базы данных, затем запустите сервер:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run alembic upgrade head # создать/обновить схему базы данных
|
|||
|
|
uv run python run.py # uvicorn на 0.0.0.0:8000 с reload
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Или напрямую:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run uvicorn app.main:app --reload
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
После запуска:
|
|||
|
|
|
|||
|
|
- GraphQL Playground: <http://localhost:8000/graphql>
|
|||
|
|
- Admin UI: <http://localhost:8000/admin>
|
|||
|
|
|
|||
|
|
### Заполнение демо-данными
|
|||
|
|
|
|||
|
|
|
|||
|
|
##### Администратор
|
|||
|
|
Может открыть админ-панель
|
|||
|
|
|
|||
|
|
Почта:
|
|||
|
|
```
|
|||
|
|
admin@example.com
|
|||
|
|
```
|
|||
|
|
Пароль:
|
|||
|
|
```
|
|||
|
|
Kz8#Fj2!Lm9@Qw4$xR
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
##### Пользователь
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
user@example.com
|
|||
|
|
```
|
|||
|
|
```
|
|||
|
|
DemoUser-Pass#2024
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
##### OAuth-клиент
|
|||
|
|
`demo-client` - "Demo App"
|
|||
|
|
```
|
|||
|
|
redirect_uris = ["https://example.com/callback"]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Заполнение:
|
|||
|
|
|
|||
|
|
Убедитесь, что схема создана (запустите один раз или при каждом изменении миграций):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run alembic upgrade head
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Запустите скрипт заполнения:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run python seed.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Запустите сервер:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run python run.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Админ панель: <http://localhost:8000/admin>
|
|||
|
|
|
|||
|
|
|
|||
|
|
#### Свои учётные данные
|
|||
|
|
|
|||
|
|
Переопределите значения по умолчанию через переменные окружения, например:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
SEED_ADMIN_EMAIL=root@corp.io \
|
|||
|
|
SEED_ADMIN_PASSWORD='S0me-very-Long-Random-Passphrase!42' \
|
|||
|
|
uv run python seed.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Переменная | Значение по умолчанию |
|
|||
|
|
| --------------------- | --------------------- |
|
|||
|
|
| `SEED_ADMIN_EMAIL` | `admin@example.com` |
|
|||
|
|
| `SEED_ADMIN_PASSWORD` | `Kz8#Fj2!Lm9@Qw4$xR` |
|
|||
|
|
| `SEED_USER_EMAIL` | `user@example.com` |
|
|||
|
|
| `SEED_USER_PASSWORD` | `DemoUser-Pass#2024` |
|
|||
|
|
|
|||
|
|
> Пароли из seed-скрипта обходят политику стойкости при регистрации
|
|||
|
|
|
|||
|
|
#### Сброс
|
|||
|
|
|
|||
|
|
Чтобы сбросить базу данных:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
rm -f vault.db
|
|||
|
|
uv run alembic upgrade head
|
|||
|
|
uv run python seed.py
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Структура проекта
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
.
|
|||
|
|
├── alembic/ # окружение миграций Alembic
|
|||
|
|
├── alembic.ini # конфигурация Alembic
|
|||
|
|
├── app/
|
|||
|
|
│ ├── main.py # FastAPI приложение
|
|||
|
|
│ ├── config.py # настройки из .env
|
|||
|
|
│ ├── db/ # движок, SessionLocal, get_db() и декларативная Base
|
|||
|
|
│ ├── models/ # модели SQLAlchemy: User, OAuthClient, AuditEvent
|
|||
|
|
│ ├── core/ # JWT, хеширование паролей, зависимости аутентификации
|
|||
|
|
│ ├── auth/ # REST и схемы Pydantic
|
|||
|
|
│ ├── audit/ # запись событий в базу данных
|
|||
|
|
│ ├── graphql/ # схема Ariadne, резолверы, контекст и директивы
|
|||
|
|
│ │ └── schema.graphql # SDL GraphQL
|
|||
|
|
│ └── static/ # Фронт
|
|||
|
|
├── tests/ # тесты Pytest
|
|||
|
|
├── run.py # точка входа Uvicorn
|
|||
|
|
├── seed.py # заполнение демо-данными
|
|||
|
|
├── pyproject.toml # метаданные проекта и зависимости (uv)
|
|||
|
|
└── requirements.txt # зафиксированные зависимости для установки через pip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
<!-- ## Обзор API -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- ### REST (`/auth`) -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- | Метод | Путь | Аутентификация | Описание | -->
|
|||
|
|
<!-- | --- | --- | --- | --- | -->
|
|||
|
|
<!-- | `POST` | `/auth/register` | нет | Зарегистрировать нового пользователя; возвращает access-токен | -->
|
|||
|
|
<!-- | `POST` | `/auth/login` | нет | Аутентифицировать и вернуть access-токен | -->
|
|||
|
|
<!-- | `GET` | `/auth/me` | bearer-токен | Вернуть текущего пользователя (`id`, `email`, `is_admin`) | -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- ### GraphQL (`/graphql`) -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- Запросы (queries): -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- | Query | Аутентификация | Описание | -->
|
|||
|
|
<!-- | --- | --- | --- | -->
|
|||
|
|
<!-- | `me` | аутентифицирован | Текущий пользователь | -->
|
|||
|
|
<!-- | `users(search, limit, offset)` | администратор | Список пользователей с опциональной фильтрацией по email | -->
|
|||
|
|
<!-- | `clients` | администратор | Список зарегистрированных OAuth-клиентов | -->
|
|||
|
|
<!-- | `auditEvents(eventType, limit, offset)` | администратор | Список событий аудита | -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- Мутации (mutations): -->
|
|||
|
|
<!---->
|
|||
|
|
<!-- | Mutation | Аутентификация | Описание | -->
|
|||
|
|
<!-- | --- | --- | --- | -->
|
|||
|
|
<!-- | `registerUser(email, password)` | нет | Создать пользователя; возвращает access-токен | -->
|
|||
|
|
<!-- | `createClient(name, isConfidential)` | администратор | Создать OAuth-клиент | -->
|
|||
|
|
<!-- | `disableUser(userId)` | администратор | Деактивировать пользователя | -->
|
|||
|
|
|
|||
|
|
## Пример GraphQL
|
|||
|
|
|
|||
|
|
Передайте JWT как bearer-токен:
|
|||
|
|
|
|||
|
|
```sh
|
|||
|
|
curl http://localhost:8000/graphql/ \
|
|||
|
|
-H 'Content-Type: application/json' \
|
|||
|
|
-H 'Authorization: Bearer <token>' \
|
|||
|
|
-d '{"query": "{ me { email } }"}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Тело запроса:
|
|||
|
|
|
|||
|
|
```graphql
|
|||
|
|
{ me { email } }
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Пример ответа:
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"data": {
|
|||
|
|
"me": {
|
|||
|
|
"email": "user@example.com"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Тестирование
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
uv run pytest -q
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## Примечания
|
|||
|
|
|
|||
|
|
`.env` и `*.db` не коммитятся - они перечислены в `.gitignore` вместе с `__pycache__/`, `.venv/` и другими локальными артефактами
|
|||
|
|
|
|||
|
|
Держите секреты и локальные базы данных вне системы контроля версий
|