Add deployment Docker Compose file, testing utilities, security helpers, and database initialization script
- Introduced `docker-compose.deploy.yml` for deployment scenarios with pre-built Docker images. - Added `auth_test_utils.py` to simplify authentication testing in FastAPI. - Implemented `security.py` for token-based operations like file uploads and password resets. - Created `init_db.py` for database initialization and superuser creation during startup. - Updated dependencies and tests to support optional authentication in FastAPI. - Enhanced entrypoint script to handle database initialization.
This commit is contained in:
129
backend/app/utils/auth_test_utils.py
Normal file
129
backend/app/utils/auth_test_utils.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Authentication utilities for testing.
|
||||
This module provides tools to bypass FastAPI's authentication in tests.
|
||||
"""
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.api.dependencies.auth import get_current_user, get_optional_current_user
|
||||
from app.models.user import User
|
||||
|
||||
|
||||
def create_test_auth_client(
|
||||
app: FastAPI,
|
||||
test_user: User,
|
||||
extra_overrides: Optional[Dict[Callable, Callable]] = None
|
||||
) -> TestClient:
|
||||
"""
|
||||
Create a test client with authentication pre-configured.
|
||||
|
||||
This bypasses the OAuth2 token validation and directly returns the test user.
|
||||
|
||||
Args:
|
||||
app: The FastAPI app to test
|
||||
test_user: The user object to use for authentication
|
||||
extra_overrides: Additional dependency overrides to apply
|
||||
|
||||
Returns:
|
||||
TestClient with authentication configured
|
||||
"""
|
||||
# First override the oauth2_scheme dependency to return a dummy token
|
||||
# This prevents FastAPI from trying to extract a real bearer token from the request
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
app.dependency_overrides[oauth2_scheme] = lambda: "dummy_token_for_testing"
|
||||
|
||||
# Then override the get_current_user dependency to return our test user
|
||||
app.dependency_overrides[get_current_user] = lambda: test_user
|
||||
|
||||
# Apply any extra overrides
|
||||
if extra_overrides:
|
||||
for dep, override in extra_overrides.items():
|
||||
app.dependency_overrides[dep] = override
|
||||
|
||||
# Create and return the client
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def create_test_optional_auth_client(
|
||||
app: FastAPI,
|
||||
test_user: User
|
||||
) -> TestClient:
|
||||
"""
|
||||
Create a test client with optional authentication pre-configured.
|
||||
|
||||
This is useful for testing endpoints that use get_optional_current_user.
|
||||
|
||||
Args:
|
||||
app: The FastAPI app to test
|
||||
test_user: The user object to use for authentication
|
||||
|
||||
Returns:
|
||||
TestClient with optional authentication configured
|
||||
"""
|
||||
# Override the get_optional_current_user dependency
|
||||
app.dependency_overrides[get_optional_current_user] = lambda: test_user
|
||||
|
||||
# Create and return the client
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def create_test_superuser_client(
|
||||
app: FastAPI,
|
||||
test_user: User
|
||||
) -> TestClient:
|
||||
"""
|
||||
Create a test client with superuser authentication pre-configured.
|
||||
|
||||
Args:
|
||||
app: The FastAPI app to test
|
||||
test_user: The user object to use as superuser
|
||||
|
||||
Returns:
|
||||
TestClient with superuser authentication
|
||||
"""
|
||||
# Make sure user is a superuser
|
||||
test_user.is_superuser = True
|
||||
|
||||
# Use the auth client creation with superuser
|
||||
return create_test_auth_client(app, test_user)
|
||||
|
||||
|
||||
def create_test_unauthenticated_client(app: FastAPI) -> TestClient:
|
||||
"""
|
||||
Create a test client that will fail authentication checks.
|
||||
|
||||
This is useful for testing the unauthorized case of protected endpoints.
|
||||
|
||||
Args:
|
||||
app: The FastAPI app to test
|
||||
|
||||
Returns:
|
||||
TestClient without authentication
|
||||
"""
|
||||
# Any authentication attempts will fail
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def cleanup_test_client_auth(app: FastAPI) -> None:
|
||||
"""
|
||||
Clean up authentication overrides from the FastAPI app.
|
||||
|
||||
Call this after your tests to restore normal authentication behavior.
|
||||
|
||||
Args:
|
||||
app: The FastAPI app to clean up
|
||||
"""
|
||||
# Get all auth dependencies
|
||||
auth_deps = [
|
||||
get_current_user,
|
||||
get_optional_current_user,
|
||||
OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||
]
|
||||
|
||||
# Remove overrides
|
||||
for dep in auth_deps:
|
||||
if dep in app.dependency_overrides:
|
||||
del app.dependency_overrides[dep]
|
||||
110
backend/app/utils/security.py
Normal file
110
backend/app/utils/security.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Security utilities for token-based operations.
|
||||
|
||||
This module provides utilities for creating and verifying signed tokens,
|
||||
useful for operations like file uploads, password resets, or any other
|
||||
time-limited, single-use operations.
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import secrets
|
||||
import time
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
def create_upload_token(file_path: str, content_type: str, expires_in: int = 300) -> str:
|
||||
"""
|
||||
Create a signed token for secure file uploads.
|
||||
|
||||
This generates a time-limited, single-use token that can be verified
|
||||
to ensure the upload is authorized.
|
||||
|
||||
Args:
|
||||
file_path: The destination path for the file
|
||||
content_type: The expected content type (e.g., "image/jpeg")
|
||||
expires_in: Expiration time in seconds (default: 300 = 5 minutes)
|
||||
|
||||
Returns:
|
||||
A base64 encoded token string
|
||||
|
||||
Example:
|
||||
>>> token = create_upload_token("/uploads/avatar.jpg", "image/jpeg")
|
||||
>>> # Send token to client, client includes it in upload request
|
||||
"""
|
||||
# Create the payload
|
||||
payload = {
|
||||
"path": file_path,
|
||||
"content_type": content_type,
|
||||
"exp": int(time.time()) + expires_in,
|
||||
"nonce": secrets.token_hex(8) # Add randomness to prevent token reuse
|
||||
}
|
||||
|
||||
# Convert to JSON and encode
|
||||
payload_bytes = json.dumps(payload).encode('utf-8')
|
||||
|
||||
# Create a signature using the secret key
|
||||
signature = hashlib.sha256(
|
||||
payload_bytes + settings.SECRET_KEY.encode('utf-8')
|
||||
).hexdigest()
|
||||
|
||||
# Combine payload and signature
|
||||
token_data = {
|
||||
"payload": payload,
|
||||
"signature": signature
|
||||
}
|
||||
|
||||
# Encode the final token
|
||||
token_json = json.dumps(token_data)
|
||||
token = base64.urlsafe_b64encode(token_json.encode('utf-8')).decode('utf-8')
|
||||
|
||||
return token
|
||||
|
||||
|
||||
def verify_upload_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Verify an upload token and return the payload if valid.
|
||||
|
||||
Args:
|
||||
token: The token string to verify
|
||||
|
||||
Returns:
|
||||
The payload dictionary if valid, None if invalid or expired
|
||||
|
||||
Example:
|
||||
>>> payload = verify_upload_token(token_from_client)
|
||||
>>> if payload:
|
||||
... file_path = payload["path"]
|
||||
... content_type = payload["content_type"]
|
||||
... # Proceed with upload
|
||||
... else:
|
||||
... # Token invalid or expired
|
||||
"""
|
||||
try:
|
||||
# Decode the token
|
||||
token_json = base64.urlsafe_b64decode(token.encode('utf-8')).decode('utf-8')
|
||||
token_data = json.loads(token_json)
|
||||
|
||||
# Extract payload and signature
|
||||
payload = token_data["payload"]
|
||||
signature = token_data["signature"]
|
||||
|
||||
# Verify signature
|
||||
payload_bytes = json.dumps(payload).encode('utf-8')
|
||||
expected_signature = hashlib.sha256(
|
||||
payload_bytes + settings.SECRET_KEY.encode('utf-8')
|
||||
).hexdigest()
|
||||
|
||||
if signature != expected_signature:
|
||||
return None
|
||||
|
||||
# Check expiration
|
||||
if payload["exp"] < int(time.time()):
|
||||
return None
|
||||
|
||||
return payload
|
||||
|
||||
except (ValueError, KeyError, json.JSONDecodeError):
|
||||
return None
|
||||
Reference in New Issue
Block a user