Egor Chernobai 2 månader sedan
förälder
incheckning
fd59e9f515

+ 5 - 1
README.MD

@@ -45,7 +45,11 @@
 
 1. Установите зависимости: `pip install fastapi sqlalchemy psycopg2-binary uvicorn`
 2. Настройте базу данных в `database.py`
-3. Запустите приложение: `uvicorn main:app --reload`
+3. При необходимости задайте постоянный ключ шифрования: `set ENCRYPTION_KEY=<key>` (Windows PowerShell) или `export ENCRYPTION_KEY=<key>` (Linux/macOS)
+4. Запустите приложение: `uvicorn main:app --reload`
+
+> По умолчанию приложение сохранит сгенерированный ключ в файле `encryption.key` рядом с `utils.py`.
+> Если этот файл удалён или ключ изменён, ранее зашифрованные email-адреса перестанут раскрываться.
 
 ## Безопасность и журналирование
 

BIN
__pycache__/crud.cpython-312.pyc


BIN
__pycache__/database.cpython-312.pyc


BIN
__pycache__/main.cpython-312.pyc


BIN
__pycache__/models.cpython-312.pyc


BIN
__pycache__/schemas.cpython-312.pyc


BIN
__pycache__/test_main.cpython-312-pytest-9.0.3.pyc


BIN
__pycache__/utils.cpython-312.pyc


+ 1 - 0
encryption.key

@@ -0,0 +1 @@
+05DWJYSMMsTlG0bzGBOPDRLopKVXYo4rQmcJD8TAn3U=

+ 4 - 14
test_main.py

@@ -7,7 +7,6 @@ import models, schemas
 from main import app, get_db
 from database import engine, Base
 
-# Используем основную базу данных для тестов
 TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
 
 def override_get_db():
@@ -23,7 +22,6 @@ client = TestClient(app)
 
 @pytest.fixture(scope="session", autouse=True)
 def setup_database():
-    # Удаляем и создаем таблицы заново для учета изменений схемы
     Base.metadata.drop_all(bind=engine)
     Base.metadata.create_all(bind=engine)
     yield
@@ -33,7 +31,6 @@ def clean_database():
     """Очистка записей в базе данных перед каждым тестом."""
     db = TestingSessionLocal()
     try:
-        # Удаляем все записи из таблиц
         db.execute(text("DELETE FROM posts"))
         db.execute(text("DELETE FROM users"))
         db.commit()
@@ -41,7 +38,7 @@ def clean_database():
         db.close()
     yield
 
-# 1.5.1 Тесты до начала основного сценария (проверка доступности базы данных)
+
 def test_database_connection():
     """Проверяет доступность базы данных."""
     db = TestingSessionLocal()
@@ -52,9 +49,7 @@ def test_database_connection():
     finally:
         db.close()
 
-# 1.5.2 Основные тесты для CRUD операций
 
-# Пользователи
 def test_create_user():
     """Проверяет добавление новой записи в таблицу пользователей."""
     user_data = {"name": "Test User", "email": "test@example.com", "password": "password123"}
@@ -74,7 +69,6 @@ def test_get_users():
 
 def test_get_user():
     """Проверяет просмотр конкретного пользователя."""
-    # Сначала создадим пользователя
     user_data = {"name": "Another User", "email": "another@example.com", "password": "pass456"}
     create_response = client.post("/users/", json=user_data)
     user_id = create_response.json()["id"]
@@ -85,14 +79,13 @@ def test_get_user():
     assert data["id"] == user_id
     assert data["name"] == user_data["name"]
 
+
 def test_update_user():
     """Проверяет редактирование данных пользователя."""
-    # Создаем пользователя
     user_data = {"name": "Update User", "email": "update@example.com", "password": "oldpass"}
     create_response = client.post("/users/", json=user_data)
     user_id = create_response.json()["id"]
 
-    # Обновляем
     update_data = {"name": "Updated User", "email": "updated@example.com", "password": "newpass"}
     response = client.put(f"/users/{user_id}", json=update_data)
     assert response.status_code == 200
@@ -100,22 +93,20 @@ def test_update_user():
     assert data["name"] == update_data["name"]
     assert data["email"] == update_data["email"]
 
+
 def test_delete_user():
     """Проверяет удаление записи пользователя."""
-    # Создаем пользователя
     user_data = {"name": "Delete User", "email": "delete@example.com", "password": "delpass"}
     create_response = client.post("/users/", json=user_data)
     user_id = create_response.json()["id"]
 
-    # Удаляем
     response = client.delete(f"/users/{user_id}")
     assert response.status_code == 200
 
-    # Проверяем, что больше не существует
     response = client.get(f"/users/{user_id}")
     assert response.status_code == 404
 
-# Посты
+
 def test_create_post():
     """Проверяет добавление новой записи в таблицу постов."""
     user_data = {"name": "Post User", "email": "post@example.com", "password": "postpass"}
@@ -163,7 +154,6 @@ def test_update_post():
     create_response = client.post("/posts/", json=post_data)
     post_id = create_response.json()["id"]
 
-    # Обновляем
     update_data = {"title": "Updated Post", "content": "Updated content", "user_id": user_id}
     response = client.put(f"/posts/{post_id}", json=update_data)
     assert response.status_code == 200

+ 32 - 7
utils.py

@@ -1,8 +1,9 @@
 from passlib.context import CryptContext
-from cryptography.fernet import Fernet
+from cryptography.fernet import Fernet, InvalidToken
+from pathlib import Path
 import os
 
-# Хэширование паролей
+
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
 def hash_password(password: str) -> str:
@@ -11,13 +12,37 @@ def hash_password(password: str) -> str:
 def verify_password(plain_password: str, hashed_password: str) -> bool:
     return pwd_context.verify(plain_password, hashed_password)
 
-# Шифрование (например, для email)
-# Генерируем ключ, в продакшене хранить securely
-key = os.getenv("ENCRYPTION_KEY", Fernet.generate_key())
-cipher = Fernet(key)
+
+KEY_FILE = Path(__file__).resolve().parent / "encryption.key"
+
+
+def load_key() -> bytes:
+    env_key = os.getenv("ENCRYPTION_KEY")
+    if env_key:
+        return env_key.encode() if isinstance(env_key, str) else env_key
+
+    if KEY_FILE.exists():
+        return KEY_FILE.read_bytes().strip()
+
+    key = Fernet.generate_key()
+    KEY_FILE.write_bytes(key)
+    return key
+
+
+try:
+    key = load_key()
+    cipher = Fernet(key)
+except Exception as exc:
+    raise RuntimeError(
+        "Invalid encryption key. Set ENCRYPTION_KEY or remove/replace encryption.key with a valid Fernet key."
+    ) from exc
+
 
 def encrypt_data(data: str) -> str:
     return cipher.encrypt(data.encode()).decode()
 
 def decrypt_data(encrypted_data: str) -> str:
-    return cipher.decrypt(encrypted_data.encode()).decode()
+    try:
+        return cipher.decrypt(encrypted_data.encode()).decode()
+    except InvalidToken as exc:
+        raise ValueError("Unable to decrypt data: invalid encryption token or mismatched key") from exc

BIN
~$ЛР9.docx