- Updated `ARCHITECTURE.md` with thorough explanations of OAuth Consumer and Provider modes, supported flows, security features, and endpoints. - Enhanced `.env.template` with environment variables for OAuth Provider mode setup. - Expanded `README.md` to highlight OAuth Provider mode capabilities and MCP integration features. - Added OAuth configuration section to `AGENTS.md`, including key settings for both social login and provider mode.
1250 lines
39 KiB
Markdown
1250 lines
39 KiB
Markdown
# Architecture Guide
|
|
|
|
This document provides a comprehensive overview of the backend architecture, design patterns, and structural organization.
|
|
|
|
## Table of Contents
|
|
|
|
- [Overview](#overview)
|
|
- [Technology Stack](#technology-stack)
|
|
- [Project Structure](#project-structure)
|
|
- [Layered Architecture](#layered-architecture)
|
|
- [Database Architecture](#database-architecture)
|
|
- [Authentication & Authorization](#authentication--authorization)
|
|
- [Error Handling](#error-handling)
|
|
- [API Design](#api-design)
|
|
- [Background Jobs](#background-jobs)
|
|
- [Testing Strategy](#testing-strategy)
|
|
- [Security Architecture](#security-architecture)
|
|
- [Performance Considerations](#performance-considerations)
|
|
|
|
## Overview
|
|
|
|
This FastAPI backend application follows a **clean layered architecture** pattern with clear separation of concerns. The architecture is designed to be:
|
|
|
|
- **Scalable**: Can handle growing data and user load
|
|
- **Maintainable**: Easy to understand, modify, and extend
|
|
- **Testable**: Each layer can be tested independently
|
|
- **Secure**: Security built into every layer
|
|
- **Type-safe**: Comprehensive type hints throughout
|
|
|
|
### Key Architectural Principles
|
|
|
|
1. **Separation of Concerns**: Each layer has a single, well-defined responsibility
|
|
2. **Dependency Injection**: Dependencies are injected rather than hard-coded
|
|
3. **Single Responsibility**: Each module, class, and function does one thing well
|
|
4. **Open/Closed Principle**: Open for extension, closed for modification
|
|
5. **Interface Segregation**: Clients depend only on interfaces they use
|
|
6. **Dependency Inversion**: Depend on abstractions, not concretions
|
|
|
|
## Technology Stack
|
|
|
|
### Core Framework
|
|
|
|
- **FastAPI 0.115.8+**: Modern async web framework
|
|
- Automatic OpenAPI documentation
|
|
- Built-in data validation
|
|
- High performance (based on Starlette and Pydantic)
|
|
- Type safety with Python 3.10+
|
|
|
|
- **Uvicorn**: ASGI server for production deployment
|
|
- Async request handling
|
|
- WebSocket support
|
|
- HTTP/2 support
|
|
|
|
### Database Layer
|
|
|
|
- **SQLAlchemy 2.0+**: ORM and database toolkit
|
|
- Supports async operations
|
|
- Type-safe query building
|
|
- Migration support via Alembic
|
|
|
|
- **PostgreSQL**: Primary production database
|
|
- ACID compliance
|
|
- Advanced indexing
|
|
- JSON support
|
|
- Full-text search capabilities
|
|
|
|
- **Alembic**: Database migration tool
|
|
- Version-controlled schema changes
|
|
- Automatic migration generation
|
|
- Rollback support
|
|
|
|
### Data Validation
|
|
|
|
- **Pydantic 2.10+**: Data validation using Python type hints
|
|
- Fast validation (Rust core)
|
|
- Automatic JSON schema generation
|
|
- Custom validators
|
|
- Type coercion
|
|
|
|
### Authentication & Security
|
|
|
|
- **python-jose**: JWT token generation and validation
|
|
- Cryptographic signing
|
|
- Token expiration handling
|
|
- Claims validation
|
|
|
|
- **passlib + bcrypt**: Password hashing
|
|
- Industry-standard bcrypt algorithm
|
|
- Configurable cost factor
|
|
- Salt generation
|
|
|
|
### Additional Features
|
|
|
|
- **SlowAPI**: Rate limiting
|
|
- Per-IP rate limiting
|
|
- Per-route configuration
|
|
- Redis backend support (optional)
|
|
|
|
- **APScheduler**: Background job scheduling
|
|
- Cron-style scheduling
|
|
- Interval-based jobs
|
|
- Async job support
|
|
|
|
- **starlette-csrf**: CSRF protection
|
|
- Token-based CSRF prevention
|
|
- Cookie-based tokens
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
backend/
|
|
├── app/
|
|
│ ├── alembic/ # Database migrations
|
|
│ │ ├── versions/ # Migration files
|
|
│ │ └── env.py # Migration environment
|
|
│ │
|
|
│ ├── api/ # API layer
|
|
│ │ ├── dependencies/ # Dependency injection
|
|
│ │ │ ├── auth.py # Authentication dependencies
|
|
│ │ │ └── permissions.py # Authorization dependencies
|
|
│ │ ├── routes/ # API endpoints
|
|
│ │ │ ├── auth.py # Authentication routes
|
|
│ │ │ ├── users.py # User management routes
|
|
│ │ │ ├── sessions.py # Session management routes
|
|
│ │ │ ├── organizations.py # Organization routes
|
|
│ │ │ └── admin.py # Admin routes
|
|
│ │ └── main.py # API router aggregation
|
|
│ │
|
|
│ ├── core/ # Core functionality
|
|
│ │ ├── auth.py # JWT and password utilities
|
|
│ │ ├── config.py # Application configuration
|
|
│ │ ├── database.py # Database connection
|
|
│ │ ├── exceptions.py # Custom exception classes
|
|
│ │ └── middleware.py # Custom middleware
|
|
│ │
|
|
│ ├── crud/ # Database operations
|
|
│ │ ├── base.py # Generic CRUD base class
|
|
│ │ ├── user.py # User CRUD operations
|
|
│ │ ├── session.py # Session CRUD operations
|
|
│ │ └── organization.py # Organization CRUD
|
|
│ │
|
|
│ ├── models/ # SQLAlchemy models
|
|
│ │ ├── base.py # Base model with mixins
|
|
│ │ ├── user.py # User model
|
|
│ │ ├── user_session.py # Session tracking model
|
|
│ │ ├── organization.py # Organization model
|
|
│ │ └── user_organization.py # Many-to-many relationship
|
|
│ │
|
|
│ ├── schemas/ # Pydantic schemas
|
|
│ │ ├── common.py # Common schemas (pagination, etc.)
|
|
│ │ ├── errors.py # Error response schemas
|
|
│ │ ├── users.py # User schemas
|
|
│ │ ├── sessions.py # Session schemas
|
|
│ │ └── organizations.py # Organization schemas
|
|
│ │
|
|
│ ├── services/ # Business logic
|
|
│ │ ├── auth_service.py # Authentication service
|
|
│ │ ├── email_service.py # Email service
|
|
│ │ └── session_cleanup.py # Background cleanup
|
|
│ │
|
|
│ ├── utils/ # Utility functions
|
|
│ │ ├── security.py # Security utilities
|
|
│ │ ├── device.py # Device detection
|
|
│ │ └── test_utils.py # Testing utilities
|
|
│ │
|
|
│ ├── init_db.py # Database initialization
|
|
│ └── main.py # Application entry point
|
|
│
|
|
├── tests/ # Test suite
|
|
│ ├── api/ # Integration tests
|
|
│ ├── crud/ # CRUD tests
|
|
│ ├── models/ # Model tests
|
|
│ ├── services/ # Service tests
|
|
│ └── conftest.py # Test configuration
|
|
│
|
|
├── docs/ # Documentation
|
|
│ ├── ARCHITECTURE.md # This file
|
|
│ ├── CODING_STANDARDS.md # Coding standards
|
|
│ └── FEATURE_EXAMPLE.md # Feature implementation guide
|
|
│
|
|
├── requirements.txt # Python dependencies
|
|
├── pytest.ini # Pytest configuration
|
|
├── .coveragerc # Coverage configuration
|
|
└── alembic.ini # Alembic configuration
|
|
```
|
|
|
|
## Layered Architecture
|
|
|
|
The application follows a strict 5-layer architecture:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ API Layer (routes/) │
|
|
│ - HTTP endpoints │
|
|
│ - Request/response handling │
|
|
│ - OpenAPI documentation │
|
|
│ - Rate limiting │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ calls
|
|
┌──────────────────────────▼──────────────────────────────────┐
|
|
│ Dependencies (dependencies/) │
|
|
│ - Authentication (get_current_user) │
|
|
│ - Authorization (permission checks) │
|
|
│ - Database session injection │
|
|
│ - Request context │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ injects
|
|
┌──────────────────────────▼──────────────────────────────────┐
|
|
│ Service Layer (services/) │
|
|
│ - Business logic │
|
|
│ - Multi-step operations │
|
|
│ - Cross-cutting concerns │
|
|
│ - External service integration │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ calls
|
|
┌──────────────────────────▼──────────────────────────────────┐
|
|
│ CRUD Layer (crud/) │
|
|
│ - Database operations │
|
|
│ - Query building │
|
|
│ - Transaction management │
|
|
│ - Error handling │
|
|
└──────────────────────────┬──────────────────────────────────┘
|
|
│ uses
|
|
┌──────────────────────────▼──────────────────────────────────┐
|
|
│ Data Layer (models/ + schemas/) │
|
|
│ - SQLAlchemy models (database structure) │
|
|
│ - Pydantic schemas (validation) │
|
|
│ - Type definitions │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Layer Details
|
|
|
|
#### 1. API Layer (`app/api/routes/`)
|
|
|
|
**Responsibility**: Handle HTTP requests and responses
|
|
|
|
**Key Functions**:
|
|
- Define API endpoints and routes
|
|
- Handle request validation via Pydantic schemas
|
|
- Return structured responses
|
|
- Apply rate limiting
|
|
- Generate OpenAPI documentation
|
|
- Handle file uploads/downloads
|
|
|
|
**Example**:
|
|
```python
|
|
@router.get(
|
|
"/me",
|
|
response_model=UserResponse,
|
|
summary="Get current user"
|
|
)
|
|
@limiter.limit("30/minute")
|
|
async def get_current_user_info(
|
|
request: Request,
|
|
current_user: User = Depends(get_current_active_user),
|
|
db: Session = Depends(get_db)
|
|
) -> UserResponse:
|
|
"""Get the currently authenticated user's information."""
|
|
return current_user
|
|
```
|
|
|
|
**Rules**:
|
|
- Should NOT contain business logic
|
|
- Should NOT directly perform database operations (use CRUD or services)
|
|
- Must validate all input via Pydantic schemas
|
|
- Must specify response models
|
|
- Should apply appropriate rate limits
|
|
|
|
#### 2. Dependencies Layer (`app/api/dependencies/`)
|
|
|
|
**Responsibility**: Provide reusable dependency injection functions
|
|
|
|
**Key Functions**:
|
|
- Authenticate users from JWT tokens
|
|
- Check user permissions and roles
|
|
- Inject database sessions
|
|
- Provide request context
|
|
|
|
**Example**:
|
|
```python
|
|
def get_current_user(
|
|
token: str = Depends(oauth2_scheme),
|
|
db: Session = Depends(get_db)
|
|
) -> User:
|
|
"""
|
|
Extract and validate user from JWT token.
|
|
|
|
Raises:
|
|
AuthenticationError: If token is invalid or user not found
|
|
"""
|
|
try:
|
|
payload = decode_access_token(token)
|
|
user_id = UUID(payload.get("sub"))
|
|
except Exception:
|
|
raise AuthenticationError("Invalid authentication credentials")
|
|
|
|
user = user_crud.get(db, id=user_id)
|
|
if not user:
|
|
raise AuthenticationError("User not found")
|
|
|
|
return user
|
|
```
|
|
|
|
**Rules**:
|
|
- Should be pure functions
|
|
- Should raise appropriate exceptions
|
|
- Should be reusable across multiple routes
|
|
- Must handle errors gracefully
|
|
|
|
#### 3. Service Layer (`app/services/`)
|
|
|
|
**Responsibility**: Implement complex business logic
|
|
|
|
**Key Functions**:
|
|
- Orchestrate multiple CRUD operations
|
|
- Implement business rules
|
|
- Handle external service integration
|
|
- Coordinate transactions
|
|
|
|
**Example**:
|
|
```python
|
|
class AuthService:
|
|
"""Authentication service with business logic."""
|
|
|
|
def login(
|
|
self,
|
|
db: Session,
|
|
email: str,
|
|
password: str,
|
|
request: Request
|
|
) -> dict:
|
|
"""
|
|
Authenticate user and create session.
|
|
|
|
Business logic:
|
|
1. Validate credentials
|
|
2. Create session with device info
|
|
3. Generate tokens
|
|
4. Return tokens and user info
|
|
"""
|
|
# Validate credentials
|
|
user = user_crud.get_by_email(db, email=email)
|
|
if not user or not verify_password(password, user.hashed_password):
|
|
raise AuthenticationError("Invalid credentials")
|
|
|
|
if not user.is_active:
|
|
raise AuthenticationError("Account is inactive")
|
|
|
|
# Extract device info
|
|
device_info = extract_device_info(request)
|
|
|
|
# Create session
|
|
session = session_crud.create_session(
|
|
db,
|
|
user_id=user.id,
|
|
device_info=device_info
|
|
)
|
|
|
|
# Generate tokens
|
|
access_token = create_access_token(subject=str(user.id))
|
|
refresh_token = create_refresh_token(
|
|
subject=str(user.id),
|
|
jti=str(session.refresh_token_jti)
|
|
)
|
|
|
|
return {
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token,
|
|
"user": user
|
|
}
|
|
```
|
|
|
|
**Rules**:
|
|
- Contains business logic, not just data operations
|
|
- Can call multiple CRUD operations
|
|
- Should handle complex workflows
|
|
- Must maintain data consistency
|
|
- Should use transactions when needed
|
|
|
|
#### 4. CRUD Layer (`app/crud/`)
|
|
|
|
**Responsibility**: Database operations and queries
|
|
|
|
**Key Functions**:
|
|
- Create, read, update, delete operations
|
|
- Build database queries
|
|
- Handle database errors
|
|
- Manage soft deletes
|
|
- Implement pagination and filtering
|
|
|
|
**Example**:
|
|
```python
|
|
class CRUDSession(CRUDBase[UserSession, SessionCreate, SessionUpdate]):
|
|
"""CRUD operations for user sessions."""
|
|
|
|
def get_by_jti(self, db: Session, jti: UUID) -> Optional[UserSession]:
|
|
"""Get session by refresh token JTI."""
|
|
try:
|
|
return (
|
|
db.query(UserSession)
|
|
.filter(UserSession.refresh_token_jti == jti)
|
|
.first()
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error getting session by JTI: {str(e)}")
|
|
return None
|
|
|
|
def get_active_by_jti(
|
|
self,
|
|
db: Session,
|
|
jti: UUID
|
|
) -> Optional[UserSession]:
|
|
"""Get active session by refresh token JTI."""
|
|
session = self.get_by_jti(db, jti=jti)
|
|
if session and session.is_active and not session.is_expired:
|
|
return session
|
|
return None
|
|
|
|
def deactivate(self, db: Session, session_id: UUID) -> bool:
|
|
"""Deactivate a session (logout)."""
|
|
try:
|
|
session = self.get(db, id=session_id)
|
|
if not session:
|
|
return False
|
|
|
|
session.is_active = False
|
|
db.commit()
|
|
logger.info(f"Session {session_id} deactivated")
|
|
return True
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Error deactivating session: {str(e)}")
|
|
return False
|
|
```
|
|
|
|
**Rules**:
|
|
- Should NOT contain business logic
|
|
- Must handle database exceptions
|
|
- Must use parameterized queries (SQLAlchemy does this)
|
|
- Should log all database errors
|
|
- Must rollback on errors
|
|
- Should use soft deletes when possible
|
|
|
|
#### 5. Data Layer (`app/models/` + `app/schemas/`)
|
|
|
|
**Responsibility**: Define data structures
|
|
|
|
##### Models (`app/models/`)
|
|
|
|
Database schema definition using SQLAlchemy:
|
|
|
|
```python
|
|
from app.models.base import Base, UUIDMixin, TimestampMixin
|
|
|
|
class User(Base, UUIDMixin, TimestampMixin):
|
|
"""User model."""
|
|
|
|
__tablename__ = "users"
|
|
|
|
email = Column(String(255), unique=True, nullable=False, index=True)
|
|
hashed_password = Column(String(255), nullable=False)
|
|
is_active = Column(Boolean, default=True, nullable=False)
|
|
is_superuser = Column(Boolean, default=False, nullable=False)
|
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
|
|
|
# Relationships
|
|
sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
|
|
|
|
# Indexes
|
|
__table_args__ = (
|
|
Index("idx_user_email_active", "email", "is_active"),
|
|
)
|
|
```
|
|
|
|
##### Schemas (`app/schemas/`)
|
|
|
|
Data validation and serialization using Pydantic:
|
|
|
|
```python
|
|
from pydantic import BaseModel, Field, ConfigDict
|
|
|
|
class UserBase(BaseModel):
|
|
"""Base user schema with common fields."""
|
|
|
|
email: str = Field(..., description="User's email address")
|
|
|
|
class UserCreate(UserBase):
|
|
"""Schema for creating a user."""
|
|
|
|
password: str = Field(..., min_length=8)
|
|
|
|
class UserUpdate(UserBase):
|
|
"""Schema for updating a user."""
|
|
|
|
email: Optional[str] = None
|
|
password: Optional[str] = None
|
|
|
|
class UserResponse(UserBase):
|
|
"""Schema for user API responses."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: UUID
|
|
is_active: bool
|
|
created_at: datetime
|
|
```
|
|
|
|
**Rules**:
|
|
- Models define database structure
|
|
- Schemas define API contracts
|
|
- Never expose sensitive fields (passwords, tokens)
|
|
- Use mixins for common fields
|
|
- Define appropriate indexes
|
|
|
|
## Database Architecture
|
|
|
|
### Connection Management
|
|
|
|
```python
|
|
# app/core/database.py
|
|
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import sessionmaker
|
|
|
|
# Connection pooling configuration
|
|
engine = create_engine(
|
|
DATABASE_URL,
|
|
pool_size=20, # Number of persistent connections
|
|
max_overflow=50, # Additional connections when pool exhausted
|
|
pool_timeout=30, # Seconds to wait for connection
|
|
pool_recycle=3600, # Recycle connections after 1 hour
|
|
pool_pre_ping=True, # Verify connections before use
|
|
)
|
|
|
|
SessionLocal = sessionmaker(
|
|
autocommit=False,
|
|
autoflush=False,
|
|
bind=engine
|
|
)
|
|
```
|
|
|
|
### Session Management
|
|
|
|
#### Dependency Injection Pattern
|
|
|
|
```python
|
|
def get_db() -> Generator[Session, None, None]:
|
|
"""
|
|
Database session dependency for FastAPI routes.
|
|
|
|
Automatically commits on success, rolls back on error.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
finally:
|
|
db.close()
|
|
|
|
# Usage in routes
|
|
@router.get("/users")
|
|
def list_users(db: Session = Depends(get_db)):
|
|
return user_crud.get_multi(db)
|
|
```
|
|
|
|
#### Context Manager Pattern
|
|
|
|
```python
|
|
@contextmanager
|
|
def transaction_scope() -> Generator[Session, None, None]:
|
|
"""
|
|
Context manager for database transactions.
|
|
|
|
Use for complex operations requiring multiple steps.
|
|
Automatically commits on success, rolls back on error.
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
yield db
|
|
db.commit()
|
|
except Exception:
|
|
db.rollback()
|
|
raise
|
|
finally:
|
|
db.close()
|
|
|
|
# Usage in services
|
|
def complex_operation():
|
|
with transaction_scope() as db:
|
|
user = user_crud.create(db, obj_in=user_data)
|
|
session = session_crud.create(db, session_data)
|
|
return user, session
|
|
```
|
|
|
|
### Model Mixins
|
|
|
|
Common functionality shared across models:
|
|
|
|
```python
|
|
# app/models/base.py
|
|
|
|
class UUIDMixin:
|
|
"""Add UUID primary key to model."""
|
|
|
|
id = Column(
|
|
UUID(as_uuid=True),
|
|
primary_key=True,
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
nullable=False
|
|
)
|
|
|
|
class TimestampMixin:
|
|
"""Add created_at and updated_at timestamps."""
|
|
|
|
created_at = Column(
|
|
DateTime(timezone=True),
|
|
default=lambda: datetime.now(timezone.utc),
|
|
nullable=False
|
|
)
|
|
|
|
updated_at = Column(
|
|
DateTime(timezone=True),
|
|
default=lambda: datetime.now(timezone.utc),
|
|
onupdate=lambda: datetime.now(timezone.utc),
|
|
nullable=True
|
|
)
|
|
|
|
# All models inherit both mixins
|
|
class User(Base, UUIDMixin, TimestampMixin):
|
|
__tablename__ = "users"
|
|
# ...
|
|
```
|
|
|
|
### Migration System
|
|
|
|
Database migrations managed by Alembic:
|
|
|
|
```bash
|
|
# Create a new migration
|
|
alembic revision --autogenerate -m "Add user_sessions table"
|
|
|
|
# Apply migrations
|
|
alembic upgrade head
|
|
|
|
# Rollback one migration
|
|
alembic downgrade -1
|
|
|
|
# Show migration history
|
|
alembic history
|
|
```
|
|
|
|
### Indexing Strategy
|
|
|
|
```python
|
|
class UserSession(Base):
|
|
__tablename__ = "user_sessions"
|
|
|
|
# Single-column indexes
|
|
user_id = Column(UUID, ForeignKey("users.id"), index=True)
|
|
refresh_token_jti = Column(UUID, unique=True, index=True)
|
|
|
|
# Composite indexes for common queries
|
|
__table_args__ = (
|
|
Index("idx_user_session_active", "user_id", "is_active"),
|
|
Index("idx_session_expiry", "expires_at", "is_active"),
|
|
)
|
|
```
|
|
|
|
**Indexing Guidelines**:
|
|
- Index foreign keys
|
|
- Index columns used in WHERE clauses
|
|
- Index columns used in JOIN conditions
|
|
- Use composite indexes for multi-column queries
|
|
- Monitor query performance with EXPLAIN
|
|
|
|
## Authentication & Authorization
|
|
|
|
### JWT Token System
|
|
|
|
Two-token strategy for security:
|
|
|
|
```python
|
|
# Access Token (short-lived)
|
|
access_token = create_access_token(
|
|
subject=str(user.id),
|
|
additional_claims={"is_superuser": user.is_superuser}
|
|
)
|
|
# Expiry: 15 minutes
|
|
# Used for: API authentication
|
|
# Stored: Client-side (memory or secure storage)
|
|
|
|
# Refresh Token (long-lived)
|
|
refresh_token = create_refresh_token(
|
|
subject=str(user.id),
|
|
jti=str(session.refresh_token_jti) # Session tracking
|
|
)
|
|
# Expiry: 7 days
|
|
# Used for: Getting new access tokens
|
|
# Stored: HttpOnly cookie or secure storage
|
|
```
|
|
|
|
### Token Claims
|
|
|
|
```python
|
|
{
|
|
"sub": "user-uuid-here", # Subject (user ID)
|
|
"type": "access", # Token type
|
|
"exp": 1234567890, # Expiration timestamp
|
|
"iat": 1234567800, # Issued at timestamp
|
|
"is_superuser": false, # User role
|
|
"jti": "session-uuid-here" # JWT ID (for refresh tokens)
|
|
}
|
|
```
|
|
|
|
### Authentication Flow
|
|
|
|
```
|
|
┌─────────┐ ┌─────────┐
|
|
│ Client │ │ Backend │
|
|
└────┬────┘ └────┬────┘
|
|
│ │
|
|
│ POST /auth/login │
|
|
│ {email, password} │
|
|
│───────────────────────────────────────────────>│
|
|
│ │
|
|
│ Verify credentials
|
|
│ Create session
|
|
│ Generate tokens
|
|
│ │
|
|
│ {access_token, refresh_token, user} │
|
|
│<───────────────────────────────────────────────│
|
|
│ │
|
|
│ GET /api/v1/users/me │
|
|
│ Authorization: Bearer {access_token} │
|
|
│───────────────────────────────────────────────>│
|
|
│ │
|
|
│ Validate token
|
|
│ Get user
|
|
│ │
|
|
│ {user data} │
|
|
│<───────────────────────────────────────────────│
|
|
│ │
|
|
│ (after 15 minutes) │
|
|
│ POST /auth/refresh │
|
|
│ {refresh_token} │
|
|
│───────────────────────────────────────────────>│
|
|
│ │
|
|
│ Validate refresh token
|
|
│ Check session active
|
|
│ Generate new tokens
|
|
│ │
|
|
│ {access_token, refresh_token} │
|
|
│<───────────────────────────────────────────────│
|
|
│ │
|
|
```
|
|
|
|
### Authorization Patterns
|
|
|
|
#### Role-Based Access Control (RBAC)
|
|
|
|
```python
|
|
# Superuser check
|
|
@router.post("/admin/users")
|
|
def admin_endpoint(
|
|
current_user: User = Depends(get_current_superuser)
|
|
):
|
|
"""Only superusers can access this endpoint."""
|
|
pass
|
|
|
|
# Active user check
|
|
@router.get("/users/me")
|
|
def get_profile(
|
|
current_user: User = Depends(get_current_active_user)
|
|
):
|
|
"""Only active users can access this endpoint."""
|
|
pass
|
|
```
|
|
|
|
#### Resource Ownership
|
|
|
|
```python
|
|
@router.delete("/sessions/{session_id}")
|
|
def revoke_session(
|
|
session_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Users can only revoke their own sessions."""
|
|
session = session_crud.get(db, id=session_id)
|
|
|
|
if not session:
|
|
raise NotFoundError("Session not found")
|
|
|
|
# Check ownership
|
|
if session.user_id != current_user.id:
|
|
raise AuthorizationError("You can only revoke your own sessions")
|
|
|
|
session_crud.deactivate(db, session_id=session_id)
|
|
return MessageResponse(success=True, message="Session revoked")
|
|
```
|
|
|
|
#### Organization-Based Permissions
|
|
|
|
```python
|
|
from app.api.dependencies.permissions import require_org_admin
|
|
|
|
@router.post("/organizations/{org_id}/members")
|
|
def add_member(
|
|
org_id: UUID,
|
|
member_data: MemberCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: Session = Depends(get_db),
|
|
_: None = Depends(require_org_admin(org_id)) # Permission check
|
|
):
|
|
"""Only organization admins can add members."""
|
|
pass
|
|
```
|
|
|
|
### OAuth Integration
|
|
|
|
The system supports two OAuth modes:
|
|
|
|
#### OAuth Consumer Mode (Social Login)
|
|
|
|
Users can authenticate via Google or GitHub OAuth providers:
|
|
|
|
```python
|
|
# Get authorization URL with PKCE support
|
|
GET /oauth/authorize/{provider}?redirect_uri=https://yourapp.com/callback
|
|
|
|
# Handle callback and exchange code for tokens
|
|
POST /oauth/callback/{provider}
|
|
{
|
|
"code": "authorization_code_from_provider",
|
|
"state": "csrf_state_token"
|
|
}
|
|
```
|
|
|
|
**Security Features:**
|
|
- PKCE (S256) for Google
|
|
- State parameter for CSRF protection
|
|
- Nonce for Google OIDC replay attack prevention
|
|
- Google ID token signature verification via JWKS
|
|
- Email normalization to prevent account duplication
|
|
- Auto-linking by email (configurable)
|
|
|
|
#### OAuth Provider Mode (MCP Integration)
|
|
|
|
Full OAuth 2.0 Authorization Server for third-party clients (RFC compliant):
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────┐
|
|
│ MCP Client │ │ Backend │
|
|
└──────┬──────┘ └──────┬──────┘
|
|
│ │
|
|
│ GET /.well-known/oauth-authorization-server│
|
|
│─────────────────────────────────────────────>│
|
|
│ {metadata} │
|
|
│<─────────────────────────────────────────────│
|
|
│ │
|
|
│ GET /oauth/provider/authorize │
|
|
│ ?response_type=code&client_id=... │
|
|
│ &redirect_uri=...&code_challenge=... │
|
|
│─────────────────────────────────────────────>│
|
|
│ │
|
|
│ (User consents) │
|
|
│ │
|
|
│ 302 redirect_uri?code=AUTH_CODE&state=... │
|
|
│<─────────────────────────────────────────────│
|
|
│ │
|
|
│ POST /oauth/provider/token │
|
|
│ {grant_type=authorization_code, │
|
|
│ code=AUTH_CODE, code_verifier=...} │
|
|
│─────────────────────────────────────────────>│
|
|
│ │
|
|
│ {access_token, refresh_token, expires_in} │
|
|
│<─────────────────────────────────────────────│
|
|
│ │
|
|
```
|
|
|
|
**Endpoints:**
|
|
- `GET /.well-known/oauth-authorization-server` - RFC 8414 metadata
|
|
- `GET /oauth/provider/authorize` - Authorization endpoint
|
|
- `POST /oauth/provider/token` - Token endpoint (authorization_code, refresh_token)
|
|
- `POST /oauth/provider/revoke` - RFC 7009 token revocation
|
|
- `POST /oauth/provider/introspect` - RFC 7662 token introspection
|
|
|
|
**Security Features:**
|
|
- PKCE S256 required for public clients (plain method rejected)
|
|
- Authorization codes are single-use with 10-minute expiry
|
|
- Code reuse detection triggers security incident (all tokens revoked)
|
|
- Refresh token rotation on use
|
|
- Opaque refresh tokens (hashed in database)
|
|
- JWT access tokens with standard claims
|
|
- Consent management per client
|
|
|
|
## Error Handling
|
|
|
|
### Exception Hierarchy
|
|
|
|
```python
|
|
class APIException(Exception):
|
|
"""Base exception for all API errors."""
|
|
|
|
def __init__(
|
|
self,
|
|
message: str,
|
|
status_code: int,
|
|
error_code: str,
|
|
field: Optional[str] = None
|
|
):
|
|
self.message = message
|
|
self.status_code = status_code
|
|
self.error_code = error_code
|
|
self.field = field
|
|
|
|
# Specific exceptions
|
|
class AuthenticationError(APIException):
|
|
"""401 Unauthorized"""
|
|
def __init__(self, message: str, error_code: str = "AUTH_001", field: Optional[str] = None):
|
|
super().__init__(message, 401, error_code, field)
|
|
|
|
class AuthorizationError(APIException):
|
|
"""403 Forbidden"""
|
|
def __init__(self, message: str, error_code: str = "AUTH_002", field: Optional[str] = None):
|
|
super().__init__(message, 403, error_code, field)
|
|
|
|
class NotFoundError(APIException):
|
|
"""404 Not Found"""
|
|
def __init__(self, message: str, error_code: str = "NOT_001", field: Optional[str] = None):
|
|
super().__init__(message, 404, error_code, field)
|
|
|
|
class DuplicateError(APIException):
|
|
"""409 Conflict"""
|
|
def __init__(self, message: str, error_code: str = "DUP_001", field: Optional[str] = None):
|
|
super().__init__(message, 409, error_code, field)
|
|
```
|
|
|
|
### Global Exception Handlers
|
|
|
|
Registered in `app/main.py`:
|
|
|
|
```python
|
|
@app.exception_handler(APIException)
|
|
async def api_exception_handler(request: Request, exc: APIException):
|
|
"""Handle custom API exceptions."""
|
|
return JSONResponse(
|
|
status_code=exc.status_code,
|
|
content={
|
|
"success": False,
|
|
"errors": [
|
|
{
|
|
"code": exc.error_code,
|
|
"message": exc.message,
|
|
"field": exc.field
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
|
"""Handle Pydantic validation errors."""
|
|
errors = []
|
|
for error in exc.errors():
|
|
errors.append({
|
|
"code": "VAL_001",
|
|
"message": error["msg"],
|
|
"field": ".".join(str(x) for x in error["loc"])
|
|
})
|
|
return JSONResponse(
|
|
status_code=422,
|
|
content={"success": False, "errors": errors}
|
|
)
|
|
```
|
|
|
|
### Error Response Format
|
|
|
|
All errors follow this structure:
|
|
|
|
```json
|
|
{
|
|
"success": false,
|
|
"errors": [
|
|
{
|
|
"code": "AUTH_001",
|
|
"message": "Invalid credentials",
|
|
"field": "email"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## API Design
|
|
|
|
### Versioning
|
|
|
|
API versioned via URL path:
|
|
|
|
```python
|
|
# app/api/main.py
|
|
|
|
api_router = APIRouter(prefix="/api/v1")
|
|
|
|
api_router.include_router(auth_router, tags=["auth"])
|
|
api_router.include_router(users_router, tags=["users"])
|
|
api_router.include_router(sessions_router, tags=["sessions"])
|
|
```
|
|
|
|
### Pagination
|
|
|
|
Consistent pagination across all list endpoints:
|
|
|
|
```python
|
|
# Request
|
|
GET /api/v1/users?page=1&limit=20
|
|
|
|
# Response
|
|
{
|
|
"data": [...],
|
|
"pagination": {
|
|
"total": 100,
|
|
"page": 1,
|
|
"page_size": 20,
|
|
"total_pages": 5,
|
|
"has_next": true,
|
|
"has_prev": false
|
|
}
|
|
}
|
|
```
|
|
|
|
### Rate Limiting
|
|
|
|
Applied per-endpoint based on sensitivity:
|
|
|
|
```python
|
|
# Read operations - 60/minute
|
|
@limiter.limit("60/minute")
|
|
@router.get("/users")
|
|
|
|
# Write operations - 10/minute
|
|
@limiter.limit("10/minute")
|
|
@router.post("/users")
|
|
|
|
# Authentication - 5/minute
|
|
@limiter.limit("5/minute")
|
|
@router.post("/auth/login")
|
|
```
|
|
|
|
## Background Jobs
|
|
|
|
### APScheduler Integration
|
|
|
|
```python
|
|
# app/main.py
|
|
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
from app.services.session_cleanup import cleanup_expired_sessions
|
|
|
|
scheduler = AsyncIOScheduler()
|
|
|
|
@app.on_event("startup")
|
|
async def startup_event():
|
|
"""Start background jobs on application startup."""
|
|
if not settings.IS_TEST: # Don't run in tests
|
|
scheduler.add_job(
|
|
cleanup_expired_sessions,
|
|
"cron",
|
|
hour=2, # Run at 2 AM daily
|
|
id="cleanup_expired_sessions"
|
|
)
|
|
scheduler.start()
|
|
logger.info("Background jobs started")
|
|
|
|
@app.on_event("shutdown")
|
|
async def shutdown_event():
|
|
"""Stop background jobs on application shutdown."""
|
|
scheduler.shutdown()
|
|
```
|
|
|
|
### Job Implementation
|
|
|
|
```python
|
|
# app/services/session_cleanup.py
|
|
|
|
async def cleanup_expired_sessions():
|
|
"""
|
|
Clean up expired sessions.
|
|
|
|
Runs daily at 2 AM. Removes sessions expired for more than 30 days.
|
|
"""
|
|
try:
|
|
with transaction_scope() as db:
|
|
count = session_crud.cleanup_expired(db, keep_days=30)
|
|
logger.info(f"Cleaned up {count} expired sessions")
|
|
except Exception as e:
|
|
logger.error(f"Error cleaning up sessions: {str(e)}", exc_info=True)
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Test Pyramid
|
|
|
|
```
|
|
┌─────────────┐
|
|
│ E2E Tests │ ← Few, high-level
|
|
├─────────────┤
|
|
│Integration │ ← API endpoint tests
|
|
│ Tests │
|
|
├─────────────┤
|
|
│ Unit │ ← CRUD, services, utilities
|
|
│ Tests │
|
|
└─────────────┘
|
|
```
|
|
|
|
### Test Database
|
|
|
|
Use SQLite in-memory for fast tests:
|
|
|
|
```python
|
|
# tests/conftest.py
|
|
|
|
@pytest.fixture(scope="session")
|
|
def test_engine():
|
|
"""Create test database engine."""
|
|
return create_engine(
|
|
"sqlite:///:memory:",
|
|
connect_args={"check_same_thread": False}
|
|
)
|
|
|
|
@pytest.fixture
|
|
def db_session(test_engine):
|
|
"""Create a fresh database session for each test."""
|
|
Base.metadata.create_all(bind=test_engine)
|
|
Session = sessionmaker(bind=test_engine)
|
|
session = Session()
|
|
yield session
|
|
session.close()
|
|
Base.metadata.drop_all(bind=test_engine)
|
|
```
|
|
|
|
### Test Coverage
|
|
|
|
Aim for 80%+ coverage:
|
|
|
|
```bash
|
|
# Run tests with coverage
|
|
pytest --cov=app --cov-report=html --cov-report=term
|
|
|
|
# View coverage report
|
|
open htmlcov/index.html
|
|
```
|
|
|
|
## Security Architecture
|
|
|
|
### Security Headers
|
|
|
|
```python
|
|
# Content Security Policy
|
|
@app.middleware("http")
|
|
async def add_security_headers(request: Request, call_next):
|
|
response = await call_next(request)
|
|
|
|
# CSP
|
|
response.headers["Content-Security-Policy"] = "default-src 'self'"
|
|
|
|
# Other security headers
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
|
response.headers["X-Frame-Options"] = "DENY"
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
|
|
return response
|
|
```
|
|
|
|
### CORS Configuration
|
|
|
|
```python
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"http://localhost:3000",
|
|
"https://yourdomain.com"
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
allow_headers=["*"],
|
|
expose_headers=["X-Total-Count"]
|
|
)
|
|
```
|
|
|
|
### Password Requirements
|
|
|
|
- Minimum 8 characters
|
|
- Bcrypt hashing with cost factor 12
|
|
- No password history (can be added if needed)
|
|
|
|
### Session Security
|
|
|
|
- Per-device session tracking
|
|
- Automatic session expiration
|
|
- Manual session revocation
|
|
- Token rotation on refresh
|
|
|
|
## Performance Considerations
|
|
|
|
### Database Connection Pooling
|
|
|
|
- Pool size: 20 connections
|
|
- Max overflow: 50 connections
|
|
- Connection recycling every hour
|
|
- Pre-ping for connection health
|
|
|
|
### Query Optimization
|
|
|
|
- Eager loading for relationships
|
|
- Appropriate indexes on frequently queried columns
|
|
- Query result pagination
|
|
- Avoid N+1 queries
|
|
|
|
### Caching Strategy
|
|
|
|
Currently no caching implemented. Consider adding:
|
|
- Redis for session storage
|
|
- Response caching for read-heavy endpoints
|
|
- Query result caching
|
|
|
|
### Rate Limiting
|
|
|
|
Protects against abuse and DoS attacks:
|
|
- IP-based rate limiting
|
|
- Per-endpoint limits
|
|
- Can be extended with Redis for distributed systems
|
|
|
|
## Conclusion
|
|
|
|
This architecture provides a solid foundation for a scalable, maintainable, and secure FastAPI application. Key benefits:
|
|
|
|
- **Clear separation of concerns**: Each layer has a specific responsibility
|
|
- **Type safety**: Comprehensive type hints throughout
|
|
- **Security**: Built-in authentication, authorization, and security best practices
|
|
- **Testability**: Each layer can be tested independently
|
|
- **Maintainability**: Clean code structure and comprehensive documentation
|
|
- **Scalability**: Connection pooling, rate limiting, and efficient queries
|
|
|
|
For implementation examples, see:
|
|
- **Coding Standards**: `backend/docs/CODING_STANDARDS.md`
|
|
- **Feature Example**: `backend/docs/FEATURE_EXAMPLE.md`
|